# evfee-a: EV-FEE on NP2 — result: NO GAIN (negative), do not adopt ## delta Added to NP2 (sha256 51f1c1a9d499…, reproduced; 526.1156 on seeds 0..999 here): constants EV_*, helpers `_tab` (packed uint16 interpolation tables: lognormal(mean 20, s=1.2) order-share H(u), u=R(30bps-f)/(4*20); and N2(d)=E[(Z+d)+^2]), `_nv`, `_j`, `_hf`, `_shift`, `_evFee` (8-point fee grid, 3bps+i^2*1.4bps, per side argmax of (f-mu)*lam/2*H - LOSS*R/4*sig^2*N2((mu-f)/sig)), plus one block before the slot stores: fee += W*(fEV(side gap) - fEV(gap 0)), clamped. Delta form (not a straight blend) because a straight blend with NP2's fee was far worse. Frozen: W=0.3, LOSS=8, SIGSC=2, lam=0.8, grid 8x1.4bps. Gas: grid >8 points hit the 250k trade gas limit on rare paths (W=0 deviated from NP2), so 8. ## code ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {AMMStrategyBase} from "./AMMStrategyBase.sol"; import {TradeInfo} from "./IAMMStrategy.sol"; contract Strategy is AMMStrategyBase { uint256 constant ELAPSED_CAP = 8; uint256 constant SIGNAL_THRESHOLD = WAD / 650; uint256 constant DIR_DECAY = 750000000000000000; uint256 constant ACT_DECAY = 530000000000000000; uint256 constant SIZE_DECAY = 530000000000000000; uint256 constant TOX_DECAY = 910000000000000000; uint256 constant SIGMA_DECAY = 690000000000000000; uint256 constant LAMBDA_DECAY = 990000000000000000; uint256 constant SIZE_BLEND_DECAY = 828000000000000000; uint256 constant TOX_BLEND_DECAY = 51000000000000000; uint256 constant ACT_BLEND_DECAY = 985000000000000000; uint256 constant PHAT_ALPHA = 270000000000000000; uint256 constant PHAT_ALPHA_RETAIL = 60000000000000000; uint256 constant PHAT_ALPHA_SAME = 120000000000000000; uint256 constant REVERT_U = 0; uint256 constant DIR_IMPACT_MULT = 4; uint256 constant GATE_SIGMA_MULT = 7200000000000000000; uint256 constant MIN_GATE = 30000000000000000; uint256 constant TRADE_TOX_BOOST = 3500 * BPS; uint256 constant STALE_ATTRACT_FRAC = 1050000000000000000; uint256 constant RET_CAP = WAD / 35; uint256 constant TOX_CAP = WAD / 5; uint256 constant TRADE_RATIO_CAP = WAD / 5; uint256 constant LAMBDA_CAP = 1820000000000000000; uint256 constant STEP_COUNT_CAP = 64; uint256 constant BASE_FEE = 4 * BPS; uint256 constant SIGMA_COEF = 200000000000000000; uint256 constant LAMBDA_COEF = 12 * BPS; uint256 constant FLOW_SIZE_COEF = 4842 * BPS; uint256 constant TOX_COEF = 300 * BPS; uint256 constant TOX_QUAD_COEF = 15000 * BPS; uint256 constant ACT_COEF = 91843 * BPS; uint256 constant DIR_COEF = 20 * BPS; uint256 constant DIR_TOX_COEF = 100 * BPS; uint256 constant STALE_DIR_COEF = 6850 * BPS; uint256 constant SIGMA_TOX_COEF = 500 * BPS; uint256 constant TAIL_KNEE = 600 * BPS; uint256 constant TAIL_SLOPE_PROTECT = 930000000000000000; uint256 constant TAIL_SLOPE_ATTRACT = 850000000000000000; function afterInitialize(uint256 initialX, uint256 initialY) external override returns (uint256, uint256) { slots[0] = BASE_FEE; slots[1] = BASE_FEE; slots[2] = 0; slots[3] = WAD; slots[4] = 0; slots[5] = initialX > 0 ? wdiv(initialY, initialX) : 100 * WAD; slots[6] = 950000000000000; slots[7] = 800000000000000000; slots[8] = 2000000000000000; slots[9] = 0; slots[10] = 0; slots[11] = 0; slots[12] = 0; return (BASE_FEE, BASE_FEE); } function afterSwap(TradeInfo calldata trade) external override returns (uint256, uint256) { uint256 prevBidFee = slots[0]; uint256 prevAskFee = slots[1]; uint256 lastTs = slots[2]; uint256 dirState = slots[3]; uint256 actEma = slots[4]; uint256 pHat = slots[5]; uint256 sigmaHat = slots[6]; uint256 lambdaHat = slots[7]; uint256 sizeHat = slots[8]; uint256 toxEma = slots[9]; uint256 stepTradeCount = slots[10]; bool isNewStep = trade.timestamp > lastTs; if (isNewStep) { uint256 elapsedRaw = trade.timestamp - lastTs; uint256 elapsed = elapsedRaw > ELAPSED_CAP ? ELAPSED_CAP : elapsedRaw; dirState = _decayCentered(dirState, DIR_DECAY, elapsed); actEma = wmul(actEma, _powWad(ACT_DECAY, elapsed)); sizeHat = wmul(sizeHat, _powWad(SIZE_DECAY, elapsed)); toxEma = wmul(toxEma, _powWad(TOX_DECAY, elapsed)); if (stepTradeCount > 0 && elapsedRaw > 0) { uint256 lambdaInst = (stepTradeCount * WAD) / elapsedRaw; if (lambdaInst > LAMBDA_CAP) lambdaInst = LAMBDA_CAP; lambdaHat = wmul(lambdaHat, LAMBDA_DECAY) + wmul(lambdaInst, WAD - LAMBDA_DECAY); } stepTradeCount = 0; } bool firstInStep = stepTradeCount == 0; uint256 spot = trade.reserveX > 0 ? wdiv(trade.reserveY, trade.reserveX) : pHat; if (pHat == 0) pHat = spot; uint256 feeUsed = trade.isBuy ? prevBidFee : prevAskFee; uint256 gamma = feeUsed < WAD ? WAD - feeUsed : 0; uint256 pImplied; if (gamma == 0) { pImplied = spot; } else { pImplied = trade.isBuy ? wmul(spot, gamma) : wdiv(spot, gamma); } { uint256 ret = pHat > 0 ? wdiv(absDiff(pImplied, pHat), pHat) : 0; uint256 alpha = firstInStep ? PHAT_ALPHA : PHAT_ALPHA_RETAIL; uint256 prePhat = pHat; bool secondPrint = !firstInStep && stepTradeCount == 1; bool sameDir = false; bool oppDir = false; if (secondPrint) { bool up2 = pImplied >= pHat; uint256 d1 = slots[12]; if (d1 != 0) { sameDir = (d1 == 1) == up2; oppDir = !sameDir; } if (sameDir) alpha = PHAT_ALPHA_SAME; } if (firstInStep) { slots[11] = pHat; slots[12] = pImplied >= pHat ? 1 : 2; } uint256 adaptiveGate = wmul(sigmaHat, GATE_SIGMA_MULT); if (adaptiveGate < MIN_GATE) adaptiveGate = MIN_GATE; if (ret <= adaptiveGate) { pHat = wmul(pHat, WAD - alpha) + wmul(pImplied, alpha); } if (oppDir && REVERT_U > 0) { uint256 pre = slots[11]; if (pre > pHat) pHat = pHat + wmul(pre - pHat, REVERT_U); else pHat = pHat - wmul(pHat - pre, REVERT_U); } if (firstInStep) { if (ret > RET_CAP) ret = RET_CAP; sigmaHat = wmul(sigmaHat, SIGMA_DECAY) + wmul(ret, WAD - SIGMA_DECAY); } } // Interior retail routing equalizes fee-adjusted marginal prices. // Subsequent trades in a step cannot be the initial arbitrage trade. if (!firstInStep && feeUsed > 30 * BPS && gamma > 0) { uint256 normalSpot = trade.isBuy ? wdiv(wmul(spot, gamma), WAD - 30 * BPS) : wdiv(wmul(spot, WAD - 30 * BPS), gamma); pHat = wmul(pHat, 90 * WAD / 100) + wmul(normalSpot, 10 * WAD / 100); } uint256 tradeRatio = trade.reserveY > 0 ? wdiv(trade.amountY, trade.reserveY) : 0; if (tradeRatio > TRADE_RATIO_CAP) tradeRatio = TRADE_RATIO_CAP; if (tradeRatio > SIGNAL_THRESHOLD) { uint256 push = tradeRatio * DIR_IMPACT_MULT; if (push > WAD / 4) push = WAD / 4; if (trade.isBuy) { dirState = dirState + push; if (dirState > 2 * WAD) dirState = 2 * WAD; } else { dirState = dirState > push ? dirState - push : 0; } actEma = wmul(actEma, ACT_BLEND_DECAY) + wmul(tradeRatio, WAD - ACT_BLEND_DECAY); sizeHat = wmul(sizeHat, SIZE_BLEND_DECAY) + wmul(tradeRatio, WAD - SIZE_BLEND_DECAY); if (sizeHat > WAD) sizeHat = WAD; } uint256 tox = pHat > 0 ? wdiv(absDiff(spot, pHat), pHat) : 0; if (tox > TOX_CAP) tox = TOX_CAP; toxEma = wmul(toxEma, TOX_BLEND_DECAY) + wmul(tox, WAD - TOX_BLEND_DECAY); uint256 toxSignal = toxEma; stepTradeCount = stepTradeCount + 1; if (stepTradeCount > STEP_COUNT_CAP) stepTradeCount = STEP_COUNT_CAP; uint256 flowSize = wmul(lambdaHat, sizeHat); uint256 fBase = BASE_FEE + wmul(SIGMA_COEF, sigmaHat) + wmul(LAMBDA_COEF, lambdaHat) + wmul(FLOW_SIZE_COEF, flowSize); uint256 fMid = fBase + wmul(TOX_COEF, toxSignal) + wmul(TOX_QUAD_COEF, wmul(toxSignal, toxSignal)) + wmul(ACT_COEF, actEma); fMid = fMid + wmul(SIGMA_TOX_COEF, wmul(sigmaHat, toxSignal)); uint256 dirDev; bool sellPressure; if (dirState >= WAD) { dirDev = dirState - WAD; sellPressure = true; } else { dirDev = WAD - dirState; sellPressure = false; } uint256 skew = wmul(DIR_COEF, dirDev) + wmul(DIR_TOX_COEF, wmul(dirDev, toxSignal)); uint256 bidFee; uint256 askFee; if (sellPressure) { bidFee = fMid + skew; askFee = fMid > skew ? fMid - skew : 0; } else { askFee = fMid + skew; bidFee = fMid > skew ? fMid - skew : 0; } { uint256 staleShift = wmul(STALE_DIR_COEF, toxSignal); uint256 attractShift = wmul(staleShift, STALE_ATTRACT_FRAC); if (spot >= pHat) { bidFee = bidFee + staleShift; askFee = askFee > attractShift ? askFee - attractShift : 0; } else { askFee = askFee + staleShift; bidFee = bidFee > attractShift ? bidFee - attractShift : 0; } } { bool tradeAligned = (trade.isBuy && spot >= pHat) || (!trade.isBuy && spot < pHat); if (tradeAligned) { uint256 tradeBoost = wmul(TRADE_TOX_BOOST, tradeRatio); if (trade.isBuy) { bidFee = bidFee + tradeBoost; } else { askFee = askFee + tradeBoost; } } } if (sellPressure) { bidFee = clampFee(_compressTailWithSlope(bidFee, TAIL_SLOPE_PROTECT)); askFee = clampFee(_compressTailWithSlope(askFee, TAIL_SLOPE_ATTRACT)); } else { askFee = clampFee(_compressTailWithSlope(askFee, TAIL_SLOPE_PROTECT)); bidFee = clampFee(_compressTailWithSlope(bidFee, TAIL_SLOPE_ATTRACT)); } { uint256 pRef = pHat > 0 ? pHat : spot; int256 gapAsk = (int256(pRef) - int256(spot)) * int256(WAD) / int256(spot); uint256 sig = wmul(sigmaHat, EV_SIGSC); if (sig < EV_SIGMIN) sig = EV_SIGMIN; uint256 R = trade.reserveY; (uint256 evAsk, uint256 evBid, uint256 evRef) = _evFee(gapAsk, sig, R); askFee = clampFee(_shift(askFee, evAsk, evRef)); bidFee = clampFee(_shift(bidFee, evBid, evRef)); } slots[0] = bidFee; slots[1] = askFee; slots[2] = trade.timestamp; slots[3] = dirState; slots[4] = actEma; slots[5] = pHat; slots[6] = sigmaHat; slots[7] = lambdaHat; slots[8] = sizeHat; slots[9] = toxEma; slots[10] = stepTradeCount; return (bidFee, askFee); } uint256 constant EV_H = 0x21c7205f1e3c1aa813860c6508d006ae05460448038d02ff028f; uint256 constant EV_N = 0xc7389b46752f54f23a8725d1168e0c3105dc027500e200450011000400010000; uint256 constant EV_W = 300000000000000000; uint256 constant EV_LOSS = 8000000000000000000; uint256 constant EV_LAM = 800000000000000000; uint256 constant EV_SIGSC = 2000000000000000000; uint256 constant EV_SIGMIN = 400000000000000; uint256 constant EV_FMAX = 20000000000000000; uint256 constant EV_GS = 14; uint256 constant EV_MODE = 0; uint256 constant EV_NG = 8; uint256 constant EV_MEANSIZE = 20 * WAD; function _tab(uint256 word, int256 pos, uint256 n) internal pure returns (uint256) { if (pos <= 0) return word & 0xFFFF; uint256 p = uint256(pos); uint256 idx = p / WAD; if (idx >= n - 1) return (word >> (16 * (n - 1))) & 0xFFFF; uint256 fr = p - idx * WAD; uint256 a = (word >> (16 * idx)) & 0xFFFF; uint256 b = (word >> (16 * (idx + 1))) & 0xFFFF; return (a * (WAD - fr) + b * fr) / WAD; } function _shift(uint256 fee, uint256 ev, uint256 ref_) internal pure returns (uint256) { if (ev >= ref_) return EV_MODE == 2 ? fee : fee + wmul(ev - ref_, EV_W); if (EV_MODE == 1) return fee; uint256 dn = wmul(ref_ - ev, EV_W); return fee > dn ? fee - dn : 0; } function _nv(int256 d) internal pure returns (uint256) { if (d > 4 * int256(WAD)) { uint256 dd = uint256(d); return (WAD + wmul(dd, dd)) * 3000 / WAD; } return _tab(EV_N, (d + int256(7 * WAD / 2)) * 2, 16); } function _j(int256 fi, int256 mu, int256 hf, uint256 lossK, int256 sg) internal pure returns (int256) { return (fi - mu) * hf / int256(WAD) - int256(lossK * _nv((mu - fi) * int256(WAD) / sg)); } function _hf(uint256 f, uint256 R) internal pure returns (int256) { int256 u = (int256(R) * (int256(30 * BPS) - int256(f))) / int256(4 * EV_MEANSIZE); return int256(wmul(EV_LAM / 2, EV_MEANSIZE)) * int256(_tab(EV_H, (u + int256(2 * WAD)) * 4, 13)) / 10000; } // EV-FEE: per side argmax_f (f-mu)*lam/2*E[order share(f)] - L*R/4*E[(z-f)+^2] function _evFee(int256 mu, uint256 sig, uint256 R) internal pure returns (uint256 evAsk, uint256 evBid, uint256 evRef) { int256 b0 = type(int256).min; int256 b1 = b0; int256 b2 = b0; evAsk = BASE_FEE; evBid = BASE_FEE; evRef = BASE_FEE; uint256 lossK = wmul(wmul(EV_LOSS, R / 4), wmul(sig, sig)) / 3000; for (uint256 i = 0; i < EV_NG; i++) { uint256 f = 3 * BPS + i * i * EV_GS * BPS / 10; int256 hf = _hf(f, R); int256 ja = _j(int256(f), mu, hf, lossK, int256(sig)); if (ja > b0) { b0 = ja; evAsk = f; } ja = _j(int256(f), -mu, hf, lossK, int256(sig)); if (ja > b1) { b1 = ja; evBid = f; } ja = _j(int256(f), 0, hf, lossK, int256(sig)); if (ja > b2) { b2 = ja; evRef = f; } } } function _compressTailWithSlope(uint256 fee, uint256 slope) internal pure returns (uint256) { if (fee <= TAIL_KNEE) return fee; return TAIL_KNEE + wmul(fee - TAIL_KNEE, slope); } function _powWad(uint256 factor, uint256 exp) internal pure returns (uint256 result) { result = WAD; while (exp > 0) { if (exp & 1 == 1) result = wmul(result, factor); factor = wmul(factor, factor); exp >>= 1; } } function _decayCentered(uint256 centered, uint256 decayFactor, uint256 elapsed) internal pure returns (uint256) { uint256 mul = _powWad(decayFactor, elapsed); if (centered >= WAD) { return WAD + wmul(centered - WAD, mul); } uint256 below = wmul(WAD - centered, mul); return below < WAD ? WAD - below : 0; } function getName() external pure override returns (string memory) { return "no_cubic"; } } ``` ## sha256 252d2fc7a97902f244057faf7f62fb90f9dd08544107bd950bafbb0984d46dec (of the code text above) ## tuning Seeds 10000..10299 (n=300 unless noted); mean_diff vs NP2 (paired, NP2 run separately; se in brackets). Straight blend of fEV with NP2 fee (n=200): W=.1 -6.53 (.18); W=.3 -34.2 (.54); W=.6 -83.1 (1.2); W=1 -147.8 (2.3); W=.3,LOSS=4 -40.7; LOSS=.25 -27.7. (EV fee alone ~56bps at zero gap, NP2 averages ~35bps, so blending raises the level and hurts.) Delta form, 8-pt grid: W=0 0.0000 (0) [sanity]; W=.2 -1.51 (.046); W=.5 -5.40 (.082); W=.3,LOSS=8,SIGSC=2 -0.082 (.011); W=.3,LOSS=3,lam=.4 -1.67 (.046); W=.2,LOSS=.1 -1.21 (.059); W=.2,lam=1.6 -1.37 (.050); W=.2,SIGSC=.5 -1.51 (.064); W=.2,LOSS=3,SIGSC=2 -0.089 (.011); W=.1,LOSS=2 -0.43 (.029); raise-only W=.3 -0.79, W=.1 -0.12 (.020); lower-only W=.3 -2.18, W=.1 -0.34. Every point with real EV influence is negative; the least negative ones are those where the EV term nearly cancels. Tuning used only 10000..10299 (some 10000..10199); not the full 10000..99999 range (time). ## paired Confirm block seeds 2600000..2601999 (fresh, run once), frozen point: {"n": 2000, "mean_diff": -0.07057953261128873, "se": 0.004943029783569504, "worst": -1.855760044241606} (NP2 523.016, candidate 522.945 mean edge vs Vanilla.) Does not reach top 10 (+5.8) or 1st (+9.6); it is -0.07 (14 se below zero). ## blocked null ## next EV-FEE is myopic: it ignores that gap persists across steps and that NP2's stale/attract shifts already react ~7x per unit gap, far above the ~1x the one-step EV model suggests. Try a multi-step (hold-time) formulation, or use fEV only to gate NP2's existing shifts, calibrating the arb term against measured arb loss per side from the simulator's arb_volume_y rather than the analytic R/4*m^2.