{
 "approach": "linear-cma: kept YQ's estimators (pHat with arb-implied price and adaptive gate, sigmaHat, lambdaHat, toxEma, step trade count) and replaced its whole fee formula with an 8-coefficient linear policy per side: fee = c0 + c1*d_protect + c2*d_attract + c3*tradeRatio(on the side just traded) + c4*min(elapsed,8) + c5*firstInStep + c6*sigmaHat + c7*lambdaHat, clamped to [0,10%], with d = toxEma (~|spot-pHat|/pHat) assigned to the bid (protect) side when spot>=pHat and to the ask (attract) side, and vice versa. Coefficients fit by CMA-ES (pip cma 4.5.0, popsize 12, ~330 evaluations in 3 restarts), each evaluation 300 paired sims on tuning seeds 1000..1299 vs YQ. Result: the best linear policy is still clearly worse than YQ (-3.16 +/- 0.03 on fresh seeds), so NOT a promotion. The gap is consistent with YQ's nonlinear terms (tox^2, tox^3, actEma, tail compression) which a linear policy in d cannot express.",
 "tried": [
  {
   "variant": "hand start x0 (c0=25.00bps c1=0.700 c2=-0.800 c3=0.250 c4=0.00bps/step c5=0.00bps c6=0.200 c7=12.00bps/lambda)",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -24.858,
   "se": 0.237
  },
  {
   "variant": "hand: larger trade-ratio coef c3=2.0",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -53.768,
   "se": 0.51
  },
  {
   "variant": "hand: c0=30bps,c1=1.0,c2=-0.5,c3=1.0,c6=0.5,c7=5bps",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -265.995,
   "se": 2.946
  },
  {
   "variant": "CMA-ES eval 1: c0=31.50bps c1=0.455 c2=-1.011 c3=-0.179 c4=0.69bps/step c5=-9.21bps c6=0.898 c7=8.96bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -237.226,
   "se": 3.421
  },
  {
   "variant": "CMA-ES eval 2: c0=26.28bps c1=0.600 c2=-0.215 c3=-0.574 c4=-0.26bps/step c5=-1.54bps c6=0.654 c7=7.60bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -107.627,
   "se": 0.998
  },
  {
   "variant": "CMA-ES eval 3: c0=24.31bps c1=0.349 c2=-0.783 c3=0.483 c4=-0.88bps/step c5=4.58bps c6=0.561 c7=14.01bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -83.285,
   "se": 0.759
  },
  {
   "variant": "CMA-ES eval 4: c0=28.60bps c1=0.427 c2=-0.849 c3=-0.124 c4=-0.21bps/step c5=2.12bps c6=-0.077 c7=10.41bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -55.693,
   "se": 0.708
  },
  {
   "variant": "CMA-ES eval 7: c0=25.48bps c1=0.947 c2=-0.680 c3=0.109 c4=-0.91bps/step c5=-1.40bps c6=0.116 c7=14.35bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -49.926,
   "se": 1.039
  },
  {
   "variant": "CMA-ES eval 9: c0=26.95bps c1=0.670 c2=-0.347 c3=0.858 c4=1.75bps/step c5=-5.59bps c6=-0.378 c7=9.98bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -31.876,
   "se": 0.34
  },
  {
   "variant": "CMA-ES eval 11: c0=24.11bps c1=0.620 c2=-0.725 c3=0.414 c4=0.16bps/step c5=0.48bps c6=-0.068 c7=13.51bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -25.216,
   "se": 0.276
  },
  {
   "variant": "CMA-ES eval 19: c0=23.98bps c1=0.719 c2=-0.741 c3=0.047 c4=-1.28bps/step c5=0.53bps c6=0.250 c7=10.66bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -23.106,
   "se": 0.233
  },
  {
   "variant": "CMA-ES eval 21: c0=24.13bps c1=0.652 c2=-0.950 c3=0.621 c4=0.88bps/step c5=0.67bps c6=-0.179 c7=10.19bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -18.368,
   "se": 0.258
  },
  {
   "variant": "CMA-ES eval 32: c0=32.33bps c1=0.842 c2=-0.712 c3=0.960 c4=-0.37bps/step c5=-5.30bps c6=-0.179 c7=5.19bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -11.848,
   "se": 0.15
  },
  {
   "variant": "CMA-ES eval 67: c0=24.00bps c1=0.830 c2=-0.606 c3=0.273 c4=-0.44bps/step c5=-2.80bps c6=-0.146 c7=8.31bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -7.559,
   "se": 0.118
  },
  {
   "variant": "CMA-ES eval 193: c0=12.67bps c1=0.837 c2=-0.896 c3=0.667 c4=-0.17bps/step c5=-2.78bps c6=0.088 c7=9.80bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -5.172,
   "se": 0.104
  },
  {
   "variant": "CMA-ES eval 201: c0=13.17bps c1=0.777 c2=-0.699 c3=0.656 c4=0.29bps/step c5=-4.75bps c6=0.244 c7=3.87bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -3.964,
   "se": 0.08
  },
  {
   "variant": "CMA-ES eval 239: c0=11.78bps c1=0.810 c2=-0.803 c3=0.611 c4=0.51bps/step c5=-2.21bps c6=0.166 c7=5.72bps/lambda",
   "tuning_seeds": "1000..1299",
   "n": 300,
   "mean_diff": -3.322,
   "se": 0.073
  },
  {
   "variant": "final (CMA best) re-measured on tuning block 10000..11999",
   "tuning_seeds": "10000..11999",
   "n": 2000,
   "mean_diff": -3.215,
   "se": 0.033
  }
 ],
 "next": [
  "Add the nonlinear features YQ relies on (toxEma^2, toxEma^3, actEma, flow size) as extra linear features and refit: tests whether the gap is in features or in fitting.",
  "Use CMA-ES as a residual tuner around YQ itself (perturb YQ's ~25 constants) rather than replacing its formula; 300-seed CRN objective had se~0.03-0.2 and was usable.",
  "The fitted c5 (firstInStep, -2.2bps) and c4 (+0.5bps per idle step) suggest a small timing term; try adding just those two terms to YQ, paired on 2000+ seeds."
 ],
 "code": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\n\nimport {AMMStrategyBase} from \"./AMMStrategyBase.sol\";\nimport {TradeInfo} from \"./IAMMStrategy.sol\";\n\n/// Linear fee policy on YQ's estimators (pHat, sigmaHat, lambdaHat, toxEma).\n/// fee(side) = C0 + C1*dProtect + C2*dAttract + C3*tradeRatio(trade side)\n///           + C4*min(elapsed,8) + C5*firstInStep + C6*sigmaHat + C7*lambdaHat, clamped to [0, 10%].\ncontract Strategy is AMMStrategyBase {\n    uint256 constant ELAPSED_CAP = 8;\n    uint256 constant TOX_DECAY = 910000000000000000;\n    uint256 constant SIGMA_DECAY = 824000000000000000;\n    uint256 constant LAMBDA_DECAY = 990000000000000000;\n    uint256 constant TOX_BLEND_DECAY = 51000000000000000;\n    uint256 constant PHAT_ALPHA = 260000000000000000;\n    uint256 constant PHAT_ALPHA_RETAIL = 50000000000000000;\n    uint256 constant GATE_SIGMA_MULT = 10 * WAD;\n    uint256 constant MIN_GATE = 30000000000000000;\n    uint256 constant RET_CAP = WAD / 10;\n    uint256 constant TOX_CAP = WAD / 5;\n    uint256 constant TRADE_RATIO_CAP = WAD / 5;\n    uint256 constant LAMBDA_CAP = 5 * WAD;\n    uint256 constant STEP_COUNT_CAP = 64;\n    uint256 constant INIT_FEE = 3 * BPS;\n\n    // fitted coefficients (WAD-scaled, signed)\n    int256 constant C0 = 1177997171676552;\n    int256 constant C1 = 809633938841529856;\n    int256 constant C2 = -802761400453001600;\n    int256 constant C3 = 610862948731824000;\n    int256 constant C4 = 50666111563941;\n    int256 constant C5 = -220706630643119;\n    int256 constant C6 = 165832367950618048;\n    int256 constant C7 = 572143570109522;\n\n    // slots: 0 bid, 1 ask, 2 lastTs, 5 pHat, 6 sigmaHat, 7 lambdaHat, 9 toxEma, 10 stepTradeCount\n\n    function afterInitialize(uint256 initialX, uint256 initialY) external override returns (uint256, uint256) {\n        slots[0] = INIT_FEE;\n        slots[1] = INIT_FEE;\n        slots[2] = 0;\n        slots[5] = initialX > 0 ? wdiv(initialY, initialX) : 100 * WAD;\n        slots[6] = 950000000000000;\n        slots[7] = 800000000000000000;\n        slots[9] = 0;\n        slots[10] = 0;\n        return (INIT_FEE, INIT_FEE);\n    }\n\n    function afterSwap(TradeInfo calldata trade) external override returns (uint256, uint256) {\n        uint256 lastTs = slots[2];\n        uint256 pHat = slots[5];\n        uint256 sigmaHat = slots[6];\n        uint256 lambdaHat = slots[7];\n        uint256 toxEma = slots[9];\n        uint256 stepTradeCount = slots[10];\n\n        uint256 elapsed = 0;\n        if (trade.timestamp > lastTs) {\n            uint256 elapsedRaw = trade.timestamp - lastTs;\n            elapsed = elapsedRaw > ELAPSED_CAP ? ELAPSED_CAP : elapsedRaw;\n            toxEma = wmul(toxEma, _powWad(TOX_DECAY, elapsed));\n            if (stepTradeCount > 0) {\n                uint256 lambdaInst = (stepTradeCount * WAD) / elapsedRaw;\n                if (lambdaInst > LAMBDA_CAP) lambdaInst = LAMBDA_CAP;\n                lambdaHat = wmul(lambdaHat, LAMBDA_DECAY) + wmul(lambdaInst, WAD - LAMBDA_DECAY);\n            }\n            stepTradeCount = 0;\n        }\n        bool firstInStep = stepTradeCount == 0;\n\n        uint256 spot = trade.reserveX > 0 ? wdiv(trade.reserveY, trade.reserveX) : pHat;\n        if (pHat == 0) pHat = spot;\n\n        {\n            uint256 feeUsed = trade.isBuy ? slots[0] : slots[1];\n            uint256 gamma = feeUsed < WAD ? WAD - feeUsed : 0;\n            uint256 pImplied = gamma == 0 ? spot : (trade.isBuy ? wmul(spot, gamma) : wdiv(spot, gamma));\n            uint256 ret = pHat > 0 ? wdiv(absDiff(pImplied, pHat), pHat) : 0;\n            uint256 gate = wmul(sigmaHat, GATE_SIGMA_MULT);\n            if (gate < MIN_GATE) gate = MIN_GATE;\n            if (ret <= gate) {\n                uint256 alpha = firstInStep ? PHAT_ALPHA : PHAT_ALPHA_RETAIL;\n                pHat = wmul(pHat, WAD - alpha) + wmul(pImplied, alpha);\n            }\n            if (firstInStep) {\n                if (ret > RET_CAP) ret = RET_CAP;\n                sigmaHat = wmul(sigmaHat, SIGMA_DECAY) + wmul(ret, WAD - SIGMA_DECAY);\n            }\n        }\n\n        uint256 tradeRatio = trade.reserveY > 0 ? wdiv(trade.amountY, trade.reserveY) : 0;\n        if (tradeRatio > TRADE_RATIO_CAP) tradeRatio = TRADE_RATIO_CAP;\n\n        {\n            uint256 tox = pHat > 0 ? wdiv(absDiff(spot, pHat), pHat) : 0;\n            if (tox > TOX_CAP) tox = TOX_CAP;\n            toxEma = wmul(toxEma, TOX_BLEND_DECAY) + wmul(tox, WAD - TOX_BLEND_DECAY);\n        }\n\n        stepTradeCount = stepTradeCount + 1;\n        if (stepTradeCount > STEP_COUNT_CAP) stepTradeCount = STEP_COUNT_CAP;\n\n        // shared part\n        int256 common = C0 + (C4 * int256(elapsed)) + (firstInStep ? C5 : int256(0))\n            + (C6 * int256(sigmaHat)) / int256(WAD) + (C7 * int256(lambdaHat)) / int256(WAD);\n        int256 d = int256(toxEma);\n        int256 pr = (C1 * d) / int256(WAD);\n        int256 at = (C2 * d) / int256(WAD);\n        int256 tr = (C3 * int256(tradeRatio)) / int256(WAD);\n\n        // spot above pHat: arbs sell X to us (bid side) -> bid protects, ask attracts\n        bool above = spot >= pHat;\n        int256 b = common + (above ? pr : at) + (trade.isBuy ? tr : int256(0));\n        int256 a = common + (above ? at : pr) + (trade.isBuy ? int256(0) : tr);\n        uint256 bidFee = b <= 0 ? 0 : clampFee(uint256(b));\n        uint256 askFee = a <= 0 ? 0 : clampFee(uint256(a));\n\n        slots[0] = bidFee;\n        slots[1] = askFee;\n        slots[2] = trade.timestamp;\n        slots[5] = pHat;\n        slots[6] = sigmaHat;\n        slots[7] = lambdaHat;\n        slots[9] = toxEma;\n        slots[10] = stepTradeCount;\n        return (bidFee, askFee);\n    }\n\n    function _powWad(uint256 factor, uint256 exp) internal pure returns (uint256 result) {\n        result = WAD;\n        while (exp > 0) {\n            if (exp & 1 == 1) result = wmul(result, factor);\n            factor = wmul(factor, factor);\n            exp >>= 1;\n        }\n    }\n\n    function getName() external pure override returns (string memory) {\n        return \"linear-cma\";\n    }\n}\n",
 "code_sha256": "c77cbca2495ebcdd178dfc4921febad6c7a741d663a1422fce6af66874f09f08",
 "delta": "Start from YQ. Delete constants DIR_*/ACT_*/SIZE_*/SIGNAL_THRESHOLD/TOX_*COEF/fee-model/TAIL_*/STALE_*/TRADE_TOX_BOOST and slots 3,4,8 with their updates; keep pHat/sigma/lambda/toxEma/stepCount updates unchanged. Replace everything after stepTradeCount++ with: elapsed=min(ts-lastTs,8) (0 if same step); common=C0+C4*elapsed+(firstInStep?C5:0)+C6*sigmaHat/WAD+C7*lambdaHat/WAD; pr=C1*toxEma/WAD; at=C2*toxEma/WAD; tr=C3*tradeRatio/WAD; above=spot>=pHat; bid=common+(above?pr:at)+(isBuy?tr:0); ask=common+(above?at:pr)+(isBuy?0:tr); each <=0 -> 0 else clampFee. int256 C0=1177997171676552, C1=809633938841529856, C2=-802761400453001600, C3=610862948731824000, C4=50666111563941, C5=-220706630643119, C6=165832367950618048, C7=572143570109522. Name 'linear-cma'.",
 "score": {
  "n": 1000,
  "mean_edge": 521.42,
  "se": 2.777,
  "seed_start": 0
 },
 "paired": {
  "reference": "YQ 00fae418",
  "seed_start": 400000,
  "n": 2000,
  "mean_diff": -3.163,
  "se": 0.033,
  "worst": [
   {
    "seed": 400688,
    "diff": -9.284
   },
   {
    "seed": 400741,
    "diff": -9.19
   },
   {
    "seed": 400375,
    "diff": -8.761
   },
   {
    "seed": 400494,
    "diff": -8.406
   },
   {
    "seed": 400892,
    "diff": -8.182
   }
  ]
 },
 "seconds_per_simulation": 0.555,
 "validate_ok": true,
 "blocked": null,
 "promotion": false,
 "notes": "amm-match run final.sol --simulations 1000 printed 'linear-cma Edge: 521.42' (YQ printed 524.63 on the same run, reproducing the brief). Toolchain: repo at 6b6921f, rustup stable, no system C linker so zig (pip ziglang) used as cc, maturin develop --release. Seeded runs use amm_sim_rs.run_batch with configs built exactly as MatchRunner._build_configs (numpy default_rng(seed) draws size, rate, sigma). seconds_per_simulation is single-core time (32 workers). Seeds 900000..909999 never run.",
 "citations": [
  "https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/README.md",
  "https://raw.githubusercontent.com/jiayaoqijia/amm-challenge-yq/3e9dc7d5a6192c88b562a0f8d9c172ca566ec94d/contracts/src/Strategy.sol"
 ]
}