{
"topic": "Offline policy search over a compact, observable-feature fee-policy family with cheap Solidity distillation and STACK fallback (policy-distillation)",
"sources": [
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/README.md",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/contracts/src/IAMMStrategy.sol",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/contracts/src/AMMStrategyBase.sol",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/evm/executor.py",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/market/arbitrageur.py",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/market/router.py",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/market/retail.py",
"https://github.com/benedictbrady/amm-challenge/blob/6b6921f8d463314aa7c96234e1be25a25245f704/amm_competition/competition/match.py",
"https://api.imdnotdead.fun/artifacts/c33720874aa1285e9dc766540b8a90410118a8e8b840d949ea0dcc211bdb27c1 (STACK; sha256 2eaafc7960e520b8a7fd322ec8afaff8fa677c730228a961efb509cc30b25719 verified locally on the 'code' field)"
],
"source_facts": {
"step_order": "README.md 'Simulation': each step = GBM price move, then arbitrageurs push spot toward p, then Poisson retail orders routed across both AMMs. Hence first callback in a step is usually arb (if any), later ones retail.",
"callback": "IAMMStrategy.sol TradeInfo{isBuy,amountX,amountY,timestamp,reserveX,reserveY}: post-trade reserves, step number; no fair price, no competitor reserves, no trade-type label. Fees returned persist until next afterSwap (no call on silent steps).",
"limits": "AMMStrategyBase.sol uint256[32] slots, MAX_FEE=WAD/10; executor.py GAS_LIMIT_TRADE = 250_000.",
"arb": "arbitrageur.py closed-form optimal arb for fee-on-input gamma=1-f, so after an arb the post-trade spot sits at p*gamma (AMM bought X) or p/gamma (AMM sold X) – the identity STACK already uses for pImplied.",
"router": "router.py equalizes marginal prices across AMMs; README: lower fees attract more volume (nonlinear).",
"configs": "match.py _build_configs: rng=np.random.default_rng(seed=i) draws retail size, rate, sigma per seed; README ranges sigma U[0.088%,0.101%], lambda U[0.6,1.0], size mean U[19,21]."
},
"current_limitation": "STACK (verified hash) is a hand-built additive fee: fBase(sigmaHat,lambdaHat,flowSize)+tox terms+actEma, dirState skew, stale-shift around blended pHat/fastHat, tradeBoost, tail compression (TAIL_KNEE 600bps) and first-step/36bps caps. Its ~40 constants were tuned jointly by hand/coordinate search; prior notes say re-tuning is capped near +1.2 and from-scratch rules lost 18-83, mostly to arb. The limitation is structural: (a) fee is a fixed sum of features, with no learned interactions (e.g. how much to cut the attract-side fee as a function of estimated gap size AND time since last callback); (b) every decision is myopic to the next callback, though the fee persists through silent steps whose arb cost depends on sigma*sqrt(silence); (c) no objective-driven search over the functional form, only over constants. Nothing in STACK's code (I read it) maps a state to a fee via a table or learned residual.",
"mechanism": {
"name": "STACK + learned residual table (RES-TABLE), trained by counterfactual policy search on full simulator rollouts, distilled into a <=64-cell piecewise-constant lookup packed into 2-4 slots",
"description": "Keep STACK's state estimators (pHat, fastHat, sigmaHat, lambdaHat, toxEma, dirState) unchanged as the observable feature pipeline. Replace nothing at first: add a residual delta_bid, delta_ask = T[cell(z)] applied after STACK's fee computation and before its final caps, where z is a 3-feature discretization of legal observables. T is found offline by maximizing mean paired edge over STACK on training seeds via black-box search (CMA-ES / coordinate descent on table cells) where every evaluation is a full amm_sim_rs.run_batch rollout vs VanillaStrategy – i.e. counterfactual rollouts, not imitation or supervised labels. Because the fitness is the true simulator edge, arb response and router reallocation are included automatically and no fair-price label ever enters the deployed contract. T=0 reproduces STACK exactly (built-in fallback/ablation).",
"what_is_new": "Not a new constant inside STACK's family: (1) a nonparametric, interaction-capturing correction indexed jointly by gap sign/magnitude, step-position and silence regime; (2) training objective is end-to-end simulated edge with rollouts, not hand reasoning; (3) explicit observability audit and leakage controls; (4) a search over policy structures (residual table vs gated mixture vs horizon-aware fee) rather than constants."
},
"equations": {
"features": "g = (spot - fastHat)/fastHat (signed stale gap, from post-trade reserves and STACK's fastHat); s = sigmaHat; first = (stepTradeCount==0); q = min(trade.timestamp - lastTs, 8) (elapsed steps since previous callback, legal: both timestamps are observed)",
"cells": "i_g = bucket(g/s; edges {-3,-1.5,-0.5,0.5,1.5,3}) -> 7 bins (scale-free via s); i_f = first?1:0; i_q = q==1?0:(q<=3?1:2); cell = i_g*6 + i_f*3 + i_q  (42 cells)",
"fee": "bid = clamp(bid_STACK + Db[cell]); ask = clamp(ask_STACK + Da[cell]); Db,Da in int8 units of 0.5bps, |D|<=63.5bps; antisymmetry option Da[i_g]=Db[6-i_g] halves parameters",
"objective": "J(T) = mean_seeds[ edge(STACK+T, seed) - edge(STACK, seed) ] - rho*||T||_1 on tune seeds 10000..999999 (mini-batches of 500-2000 seeds, common random numbers: same seeds for all candidates in a generation)",
"edge": "edge per README: retail edge positive, arb edge negative; report decomposition Delta_retail, Delta_arb per candidate",
"horizon_alt": "fee_ask* = argmin_f E[arb loss over persistence H(lambdaHat)] - E[retail capture]; H ~ 1/(1-exp(-lambdaHat)) expected steps until next callback – searched as a parametric alternative, see rule_family_search"
},
"causal_features": [
{
"feature": "g/sigmaHat (stale gap in sigma units)",
"legal_source": "post-trade reserveY/reserveX and STACK fastHat (built only from past callbacks)",
"hypothesis": "large |g| means next arb is likely in the direction of the gap; raising protect-side fee reduces arb edge loss (arb Delta >0) but STACK already does this linearly – residual captures saturation/nonlinearity"
},
{
"feature": "first-in-step flag",
"legal_source": "timestamp equality with stored lastTs",
"hypothesis": "README step order makes first callbacks arb-heavy; residual after a first trade sets fees for the remaining retail of that step and the next step's arb"
},
{
"feature": "elapsed since last callback q",
"legal_source": "timestamp difference",
"hypothesis": "long silence => higher price uncertainty at next callback; optimal fee should widen symmetric component (retail Delta <0 small, arb Delta >0)"
},
{
"feature": "lambdaHat (optional 4th axis in alternatives)",
"legal_source": "STACK count of trades per elapsed step",
"hypothesis": "proxy for persistence horizon of the current quote"
}
],
"observability_checks": [
"Static audit: the distilled contract may read only TradeInfo fields and slots; grep the compiled source for any parameter beyond TradeInfo (there is none in the ABI per IAMMStrategy.sol).",
"Training-time leakage guard: Python feature extractor used for analysis must be a byte-for-byte port of the Solidity feature code and run on the callback stream only; compare Python vs EVM features on 20 seeds (max abs diff <1e-12 relative) before any search.",
"No future labels: table cells are fit only via rollout fitness; never regress fees on realized fair price, next trade type or arb/retail flags. If any diagnostic uses simulator labels (e.g. to estimate P(arb|cell)), it may inform which cells to search, never cell values.",
"Shuffle test: permute T across cells; if gains persist equally, the gain is a level shift not state dependence -> report as constant tuning, not a new mechanism.",
"Anti-imitation: T is a residual from STACK, initialized at 0, regularized by L1; do not fit a model to reproduce STACK outputs. Report correlation of learned D with STACK's own terms (staleShift, skew) per cell; if |corr|>0.9 the residual is just rescaling STACK constants (falls under the +1.2 cap)."
],
"state_slots": {
"used_by_STACK": "0-15 (slots 0-14 read/written in afterSwap; 15 direction flag)",
"new": {
"16-17": "packed int8 table Db (42 bytes -> 2 slots, 32 bytes each)",
"18-19": "packed int8 table Da",
"alternative": "tables can instead be compile-time constants (bytes constant), using 0 slots and ~ cheaper gas; slots only needed if owner-settable"
},
"free_after": "20-31 for alternatives (gated mixture weights, horizon state)"
},
"gas_plan": "STACK peaks at 214k of 250,000 (brief figure). Residual cost: 3 bucket comparisons chains (~7 compares) ~200 gas, cell index ~50, one bytes32 constant read + byte extract + sign-extend x2 ~150-300 gas (constants) or 2 cold SLOAD 2100 each = 4200 (slots). Prefer compile-time constants: <1k total, keeping peak ~<216k. Hard rule: run amm_sim_rs.gas_stats() on every final candidate; any OOG => reject. No loops, no new _powWad calls.",
"pseudocode": "bytes constant DB = hex\"...42 bytes...\"; bytes constant DA = hex\"...\";\nfunction _cell(uint256 spot,uint256 fastHat,uint256 sig,bool first,uint256 q) internal pure returns(uint256){\n  int256 g = int256(spot)-int256(fastHat); uint256 a = wdiv(uint256(g<0?-g:g), fastHat);\n  uint256 u = sig>0 ? wdiv(a,sig) : 0; // gap in sigma units\n  uint256 m = u< WAD/2?0:(u<3*WAD/2?1:(u<3*WAD?2:3));\n  uint256 ig = g<0 ? 3-m : 3+m; // 0..6\n  uint256 iq = q<=1?0:(q<=3?1:2);\n  return ig*6 + (first?3:0) + iq; }\nfunction _d(bytes memory t,uint256 c) internal pure returns(int256){ return int256(int8(uint8(t[c]))) * int256(BPS/2); }\n// in afterSwap, after STACK stale/tradeBoost block, before tail compression:\nuint256 c = _cell(spot, fastHat, sigmaHat, firstInStep, silent>8?8:silent);\nbidFee = _addSat(bidFee, _d(DB,c)); askFee = _addSat(askFee, _d(DA,c));\n// then STACK tail compression, first-step 30bps and 36bps caps unchanged (fallback guards)",
"rule_family_search": {
"primary": "RES-TABLE: 42-84 int8 cells, CMA-ES (popsize 16-24) on cell values with common random numbers, 1000-seed minibatches rising to 4000; early stop when generation-mean gain se overlaps 0 for 5 generations.",
"alternatives": [
{
"name": "GATED-MIX",
"form": "fee = w(z)*fee_STACK + (1-w(z))*fee_ALT, fee_ALT = symmetric wide fee f_w (e.g. sigmaHat-proportional), w a 2-level logistic of |g|/sigmaHat and q",
"search": "4-6 params, structural: tests whether a second regime (quote-wide-during-uncertainty) beats additive correction",
"hypothesis": "arb Delta >0 in high-uncertainty states, retail Delta <0 there"
},
{
"name": "HORIZON-FEE",
"form": "protect-side add-on = c1*sigmaHat*sqrt(H) with H = expected steps until next callback from lambdaHat and q; attract side minus c2*|g|",
"search": "2-4 params plus exponent of H in {0.5,1}",
"hypothesis": "captures fee persistence through silent steps, which STACK ignores beyond silenceScale on pHat; arb Delta >0"
},
{
"name": "ASYM-ROUTER-TARGET",
"form": "choose attract-side fee to target a marginal-price relationship vs an inferred 30bps competitor: f_attract = max(0, 30bps - k*|g|) bounded",
"search": "2-3 params",
"hypothesis": "retail Delta >0 via router share (router.py equalizes marginal prices); arb Delta <=0"
},
{
"name": "TREE-DISTILL",
"form": "depth-3 decision tree over (g/s, first, q, lambdaHat, toxEma) fit to the best RES-TABLE/CMA policy outputs (distilling our searched policy, not STACK), 8 leaves",
"search": "compare to table at equal gas",
"hypothesis": "same gain with fewer params -> less overfit"
}
],
"selection": "pick structure by tune-block paired gain minus 2 se; final constants frozen before the single confirm run on fresh seeds >=5000000 (>=2000 seeds), never reused."
},
"falsification": [
"If best RES-TABLE paired gain on a held-out tune block (not used in search) is < +0.3 or within 2 se of 0, the state-dependent residual hypothesis is rejected for this feature set.",
"If shuffled-cell table gives >=80% of the gain, gain is a level shift (constant tuning) -> not new.",
"If Delta_arb is strongly negative while total positive only via retail on train but not held-out, overfit.",
"Any OOG on gas_stats -> candidate invalid.",
"Transfer: expect board gain ~0.65x local (brief); +5.0 local needed for 10th – this design makes NO performance claim; all signs above are hypotheses needing empirical tests."
],
"risks": [
"Search cost: each fitness eval = run_batch over 1000+ seeds of 10,000 steps; budget generations accordingly; untested here.",
"Overfitting to tuning seeds; mitigate with CRN minibatch resampling per generation and held-out block.",
"Residual may just re-tune STACK's stale-shift (the +1.2 cap); anti-imitation correlation check flags it.",
"Discrete cell boundaries create fee jumps an arb cannot exploit within a step (fees fixed until next callback per IAMMStrategy.sol), but jumps may move router share abruptly; consider linear interpolation between gap bins (+~300 gas).",
"Large negative residuals on attract side can push fees to 0 and invite arb; keep STACK's 30/36bps caps and clampFee."
],
"stack_ablation": "Run (a) STACK; (b) STACK+T=0 (must equal STACK bit-for-bit, verifies integration); (c) STACK+T_learned; (d) STACK with stale-shift block removed + T_learned (does T replace STACK's stale logic?); (e) T_shuffled. Report paired{n,mean_diff,se,worst}, Delta_retail, Delta_arb, gas max/oog for each.",
"empirical_tests_required": [
"every retail/arb sign hypothesis",
"whether g/sigmaHat buckets carry state-dependent value beyond STACK",
"choice of structure among alternatives",
"gas peak after integration",
"board transfer ratio"
],
"integration": {
"file": "single Strategy.sol = STACK code verbatim + _cell/_d helpers + 2 lines before tail compression; DB/DA as bytes constants",
"fallback": "T=0 is STACK; STACK caps retained after residual",
"next_item_brief": "Implement RES-TABLE: clone amm-challenge@6b6921f8 and run setup per goal brief; port STACK verbatim into Strategy.sol with the _cell/_d residual (42 cells, int8*0.5bps, bytes constants, antisymmetric option). Verify T=0 equals STACK edge on seeds 10000..10199 exactly and gas_stats oog=0. Write a Python harness that renders a candidate table into Solidity, compiles, and calls amm_sim_rs.run_batch(X, VanillaStrategy, configs, 8) with configs per MatchRunner._build_configs; CMA-ES popsize 16, 1000 CRN seeds per generation drawn from 10000..999999, 20-40 generations; then held-out 4000 tune seeds; run shuffle control and GATED-MIX and HORIZON-FEE at equal budget. Freeze the best and confirm once vs STACK on fresh seeds 5000000.. (>=2000). Return code, sha256, paired{n,mean_diff,se,worst}, gas{max_ok,oog}, Delta_retail, Delta_arb, and the learned table."
}
}