# evfee-b: EV-FEE on NP2 **Outcome:** EV-FEE gains +0.2109 ± 0.0085 on the confirm block. The gain is real, but far short of the +5.8 needed for top 10. It only appeared after restricting the blend to a band of NP2 fees (see Deviations). ## delta NP2's external body becomes internal `_npSwap`; dead `REVERT_U`/`oppDir`/`prePhat` code removed so it fits the stack (w=0 gave identical edges on 16 seeds). New `afterSwap` calls `_npSwap`, then for each side whose NP2 fee is in 20..50 bps: f* = argmax over 24,30..66 bps of 0.4*(f-g)*input(f) - loss*(ry/4)*s2*G((g-f)/sig), with input(f) the router split for a 20 Y order vs the 30 bps pool at pHat, g the spot-to-pHat gap on that side, s2 = sigmaHat^2 + (5 bps)^2, G(a) = E[(a+Z)+^2] (P(arb)*E[arb loss] in one closed form). Fee = 0.85*NP2 + 0.15*f*; slots[0..1] rewritten. Added: EV_* constants, `_evFee`, `_evInput`, `_evValue`, `_evG`, `_evT`. ## 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 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; // EV-FEE: per side, argmax f*share(f)*flow - P(arb)*E[arb loss], blended into NP2 by EV_W. uint256 constant EV_W = 1500 * BPS; uint256 constant EV_LOSS = 300 * BPS; uint256 constant EV_NP_MIN = 20 * BPS; uint256 constant EV_NP_MAX = 50 * BPS; uint256 constant EV_RATE = 400000000000000000; uint256 constant EV_SIZE = 20 * WAD; uint256 constant EV_SIG_FLOOR = 5 * BPS; uint256 constant EV_GN = WAD - 30 * BPS; int256 constant EV_IW = 1e18; uint256 constant EV_G0 = 155531932593661535509439217633609764404057129223931873107340792336220163; uint256 constant EV_G1 = 19756060248278132793504906396444574602741627683706482581582301539109711339; uint256 constant EV_G2 = 195427288969396225601208343948908710188678607846069930557563910332624134121; uint256 constant EV_G3 = 23694537670257982128393850425900606629390772774217; 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 bidFee, uint256 askFee) = _npSwap(trade); bool evAsk = askFee >= EV_NP_MIN && askFee <= EV_NP_MAX; bool evBid = bidFee >= EV_NP_MIN && bidFee <= EV_NP_MAX; if (evAsk || evBid) { uint256 pHat = slots[5]; uint256 sigmaHat = slots[6]; uint256 spot = wdiv(trade.reserveY, trade.reserveX); uint256 s2 = wmul(sigmaHat, sigmaHat) + wmul(EV_SIG_FLOOR, EV_SIG_FLOOR); uint256 sig = sqrt(s2 * WAD); uint256 lossK = wmul(wmul(EV_LOSS, trade.reserveY / 4), s2); uint256 k = wmul(trade.reserveX, trade.reserveY); int256 g = (2 * (int256(pHat) - int256(spot)) * EV_IW) / int256(pHat + spot); if (evAsk) { uint256 fa = _evFee(g, trade.reserveY, sqrt(wmul(k, pHat) * WAD), EV_SIZE, WAD, lossK, sig); askFee = wmul(askFee, WAD - EV_W) + wmul(fa, EV_W); slots[1] = askFee; } if (evBid) { uint256 fb = _evFee(-g, trade.reserveX, sqrt(wdiv(k, pHat) * WAD), wdiv(EV_SIZE, pHat), pHat, lossK, sig); bidFee = wmul(bidFee, WAD - EV_W) + wmul(fb, EV_W); slots[0] = bidFee; } } return (bidFee, askFee); } function _npSwap(TradeInfo calldata trade) internal 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; bool secondPrint = !firstInStep && stepTradeCount == 1; bool sameDir = false; if (secondPrint) { bool up2 = pImplied >= pHat; uint256 d1 = slots[12]; if (d1 != 0) { sameDir = (d1 == 1) == up2; } 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 (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)); } 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); } /// Grid argmax of EV per step. own/other are this side's input reserve in the two pools /// (other = 30 bps pool at pHat, same k); size is the mean order in input units, px converts to Y. /// The router split is exact at both grid ends and linear between them (error under 0.01 Y). function _evFee(int256 g, uint256 own, uint256 other, uint256 size, uint256 px, uint256 lossK, uint256 sig) internal pure returns (uint256 bestFee) { int256 d0 = _evInput(24 * BPS, own, other, size); int256 step = (_evInput(66 * BPS, own, other, size) - d0) / 7; int256 best = type(int256).min; unchecked { for (uint256 i = 0; i < 8; i++) { uint256 f = (24 + i * 6) * BPS; int256 o = _evValue(f, g, d0 + int256(i) * step, size, px) - int256((lossK * _evG(((g - int256(f)) * EV_IW) / int256(sig))) / WAD); if (o > best) { best = o; bestFee = f; } } } } /// Exact unclipped router split, r = sqrt(gm / EV_GN) as a third-order series in f - 30 bps. function _evInput(uint256 f, uint256 own, uint256 other, uint256 size) internal pure returns (int256) { uint256 gm = WAD - f; int256 d = ((int256(gm) - int256(EV_GN)) * EV_IW) / int256(EV_GN); int256 d2 = (d * d) / EV_IW; uint256 r = uint256(EV_IW + d / 2 - d2 / 8 + ((d2 * d) / EV_IW) / 16); return ((int256(wmul(r, other + wmul(EV_GN, size))) - int256(own)) * EV_IW) / int256(gm + wmul(r, EV_GN)); } function _evValue(uint256 f, int256 g, int256 inp, uint256 size, uint256 px) internal pure returns (int256) { unchecked { if (inp < 0) inp = 0; if (inp > int256(size)) inp = int256(size); return ((((int256(f) - g) * ((inp * int256(px)) / EV_IW)) / EV_IW) * int256(EV_RATE)) / EV_IW; } } /// G(a) = E[(a + Z)_+^2] for standard normal Z: 0.25-step table, linear interpolation. function _evG(int256 a) internal pure returns (uint256) { if (a <= -4e18) return 0; if (a >= 4e18) return WAD + uint256((a * a) / int256(WAD)); unchecked { uint256 u = uint256(a + 4e18); uint256 i = u / 25e16; uint256 t = (u % 25e16) * 4; return ((WAD - t) * _evT(i) + t * _evT(i + 1)) / 1e6; } } function _evT(uint256 i) internal pure returns (uint256) { uint256 w = i / 9; uint256 word = w == 0 ? EV_G0 : w == 1 ? EV_G1 : w == 2 ? EV_G2 : EV_G3; return (word >> (28 * (i % 9))) & 0xFFFFFFF; } 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 `39b8552b89e220bda4cde6a2a64d9feee3544d07a9b41fa134cbdc54e741c743` (sha256 of the code text above, without the fence; the file ends with one newline) ## tuning Frozen: **w = 0.15, loss = 0.03, NP2 band 20..50 bps**. Flow is 0.4 orders per step per side at 20 Y, sigma floor 5 bps. Peak gas is between 190k and 210k of the 250k limit, measured with sticky `gasleft()` test variants on 16 seeds. Official method (`run_batch` against Vanilla separately, 16 seeds a call), seeds 20000..20159, diff vs NP2. NP2 mean on this block was 527.3527. | w | loss | band (bps) | n | mean_diff | se | worst | |---|---|---|---:|---:|---:|---:| | 0.2 | 0.03 | 20..45 | 160 | +0.143 | 0.031 | -1.19 | | 0.2 | 0.03 | 20..50 | 160 | +0.180 | 0.036 | -1.07 | | 0.2 | 0.1 | 20..50 | 160 | +0.147 | 0.036 | -1.20 | | 0.15 | 0.03 | 20..50 | 160 | +0.190 | 0.031 | -1.12 | | 0.3 | 0.03 | 20..50 | 160 | +0.035 | 0.042 | -1.50 | w = 0.15 and w = 0.2 at loss 0.03 are within noise of each other. Differences between these points are not significant at n = 160, so the frozen choice is the highest-scoring point of a nearly flat region. It comes from only 5 tuned points, because the 2,000-seed confirm took most of the remaining time. Pre-screen only: these came from my own numba port of the simulator and strategy, not from the official method. It uses its own RNG, labelled with seeds in 10000..12000, 400..800 seeds per point. The port and `run_batch` gave similar gains at w=0.2, band 20..50 (+0.235 vs +0.180), but this was not a formal fidelity check. All port figures are diff vs NP2. | Variant | port mean_diff | |---|---:| | Unmasked blend, 7-node lognormal size, w=0.1, loss 0.5..8 | -3.8 to -19 | | Unmasked blend, single 20 Y size, w=0.1, loss 0 | -1.2 | | Same, w=0.1, loss 0.25..4 | -6.6 to -32 | | Same, w=0.3 | -7 to -102 | | Unmasked, fee capped, w=0.03 | -0.5 to -2 | | Local window ±7 bps around NP2, w=0.3 | -0.5 to -1.1 | | Local window ±7 bps around NP2, w=1 | about -4 to -6 | | One-sided, only cutting fees | about -2.2 | | One-sided, only raising fees, no band | -2.7 to -4.6 | | Band 20..60, w=0.1, loss 0.1 | +0.19 | | Band 20..50, w=0.2, loss 0.03, 8-point grid | +0.235 | A 45-point and then a 15-point fee grid ran too close to the 250k gas limit. The 45-point grid lost 14 on 32 seeds, which I attribute to out-of-gas reverts (not measured directly). That is why the grid is 8 points with the split interpolated. ### Deviations from the brief - **Band mask.** The brief says blend by w. A plain blend lost at every w tested on the port, mainly by raising NP2's near-zero attract fees and cutting its high protective fees. So the blend applies only where NP2's fee is in 20..50 bps. This condition is my addition. - **Value term.** Value uses (f-g), not f, so the retail edge includes the expected spot-to-fair offset. On the port this was slightly better than f alone at most points. - **Flow.** A fixed 20 Y order, not the full lognormal size distribution. The heavy tail pushed the EV fee far above NP2 (7-node row above). The single size reproduces the brief's shares (10 bps 75%, 60 bps 12%). - **Split accuracy.** The split is exact at 24 and 66 bps and linear between them, with an estimated error under 0.01 Y. The square root is a third-order series. - **pHat proxy.** The 30 bps pool's spot is taken as pHat, since it is not observable. Both pools have the same k. ## paired Confirm block 2660000..2661999, run once, frozen before the first run, one script resumed across two commands. No constants changed. ```json {"n": 2000, "mean_diff": 0.2109, "se": 0.0085, "worst": -2.1987} ``` Checks: NP2 assembled to sha256 51f1c1a9d4999ee4…, and scored 526.1155722 on seeds 0..999 vs Vanilla. `amm-match validate` passed on the final file. The CH reference (525.525882178) was not run, because this item does not supply CH's code. ## blocked null ## next The gain sits in a mid-fee band where NP2's fee is close to the myopic optimum, and my guess is that plain EV is wrong at both extremes because it ignores what happens after the next step. A two-step EV would value how a retail fill moves spot back toward pHat, and so what the following arb costs. That could replace the band mask with a modelled reason for it.