# r6-arb / ar1: size-to-gap arb/retail classifier (replica test) All the code, the replica, the Gaussian filter and the tables are by **Taylor (gtmcknight)**: github.com/gtmcknight/amm-sim at `4812a6382311624e1506aca131ca35c378f930d9`, folder `research/solved`. This item takes up README "Open" item 3 (arb or retail). Every run is on Taylor's pure-Python replica (`replica2.py`) under plain `python3 -I` (Python 3.12.3, 6 cores). Nothing here ran on the Rust engine or in Solidity. ## Gate P: PASS `python3 -I gates.py`, unmodified, matches the README's gate block to every printed digit: ``` seed perfect-info gaussian+plain gaussian+qm lattice+qm 10000 448.261189 443.455598 443.522608 443.663520 10001 576.779199 571.463409 571.623551 571.977293 10002 531.886497 525.168028 525.865859 526.411631 ``` My driver, which runs `real.RealPolicy(T, {'soft': 1})` with `pol_mid.json`, also reproduces the gaussian+plain column (443.455598 / 571.463409 / 525.168028). On the report seeds it scores **+2.828 se 0.041 vs HA**, matching the README's +2.83 for Gaussian + plain tables. The HA rows come from `calib_out_2048.json`. ## Arms - **BASE**: unchanged `real.py`, Gaussian filter, full plain tables `pol_mid.json`, `soft=1`. sha256 of `real.py`: `1305b5333731c046c3e6736cc2578eff4635c88132a39c22f5780d78aef2c81e`. - **TEST**: `real_s2g.py`, which is a copy of `real.py`. The only changes are the size-to-gap classifier and the retail size-law state it needs. It runs with `soft=1,s2g=2` and the frozen defaults `ka=1, wa=1, n0=200, adapt=1`. sha256: `9b9ce0d5c385e3a93c98a505181f5c88afc6e9c674298c2c34967b74d88fa0a2`. - Harness (in scratch, not delivered): a subclass of each arm's `RealPolicy` that records, for the first own trade of each step, the replica's true flag and the call made. The call is read back as `cnt == 0` after the callback. The harness does not change the policy. Paired statistics are per-seed edge, TEST minus BASE, with se = sample sd / sqrt(n). ## What the idea computes **The basic observation.** Any trade's Y size follows exactly from the pre-trade spot `sp`, the post-trade spot `s` and the fee in force, because reserves stay on the curve x*y = K. That gives: - `q = sqrt(K) |sqrt(s) - sqrt(sp)|`, divided by (1 - fa) on the Y-in side. An arbitrage stops where the pool reaches the true price net of the fee: `p = s(1 - fb)` on a pool buy and `p = s/(1 - fa)` on a pool sell. So the size an arbitrageur trades is the size that moves `sp` to the band edge at `p`. The size implied by the filter's price estimate is the same formula evaluated at the estimate. **Two forms were tuned. Only seeds 60000..61023 were used for tuning.** 1. **Literal form (`s2g=1`): closeness to one implied size.** - Take the filter's predicted belief N(m, v), after the silent-step truncations and this step's sigma^2. - Cut it to the side where an arb in the observed direction is possible: below `lo` on a pool buy, above `hi` on a pool sell. Call its mass `Pt` and its mean and sd `mu`, `sd_t`. - The implied arb size is `q* = q(mu)`. - The arb score is a Gaussian kernel in ln size: `wa * Pt * N(ln q; ln q*, tau)`, where `tau = sqrt((ka * |d ln q*/d ln p| * sd_t)^2 + t0^2)`. - The retail score is the run's lognormal law of own-fill Y sizes: `Z * RHO * N(ln q; rmu, rsd)`. - `pi` = arb / (arb + retail), and it replaces `real.py`'s `pi`. Everything downstream is unchanged: the soft mixture update, `pi > 0.5` as the arb call, the rival clip and the tables. 2. **Density form (`s2g=2`, shipped): closeness measured by the filter's own predictive density.** - The arb score is the density of N(m, ka^2 v) at the price `lp` that the observed size implies, mapped to ln-size space through the Jacobian: `wa * phi((lp - m)/(ka sd)) / (ka sd) / |d ln q/d ln p|`, where `|d ln q/d ln p| = 0.5 sqrt(s)/|sqrt(s) - sqrt(sp)|`. - The retail score is the same run-adaptive lognormal law in ln q as in the literal form. **The run's retail size law** (`rmu`, `rsd`): - It is a running mean and sd of ln(own Y fill) over the second and later own trades of a step. Those trades are certainly retail, because the arb only ever trades first in a step. - It starts from Taylor's fitted spot-move law mapped to Y: median 0.5e4 * 11.12e-4 = 5.56 Y, log-sd 1.176, weighted as `n0` pseudo-observations. The sd has a floor of 0.3. ## Choose set (seeds 60000..61023, n = 1024), TEST minus BASE BASE mean edge 529.0617. BASE misclassification 16.64%. | form | ka | wa | other | mean_diff | se | wins | misclass | |---|---|---|---|---|---|---|---| | literal | 1 | 1 | t0=.05 n0=20 | -1.9865 | 0.0247 | 7 | 21.60% | | literal | 1 | 3 | | -4.7225 | 0.0479 | 2 | 25.93% | | literal | 2 | 1 | | -28.4147 | 0.6141 | 0 | 20.87% | | literal | 2 | 3 | | -39.4881 | 0.6927 | 0 | 25.10% | | literal | 4 | 1 | | -41.2865 | 0.7882 | 0 | 26.12% | | literal | 4 | 3 | | -50.4964 | 0.8589 | 0 | 22.45% | | literal | 0.5 | 1 | | -2.0883 | 0.0286 | 8 | 26.08% | | literal | 0.75 | 1 | | -1.0968 | 0.0199 | 47 | 22.77% | | literal | 0.5 | 0.6 | | -3.2512 | 0.0335 | 3 | 28.27% | | literal | 0.75 | 0.6 | | -1.5759 | 0.0238 | 23 | 24.79% | | literal | 0.5 | 1.5 | | -1.7588 | 0.0278 | 22 | 25.80% | | literal | 1 | 0.6 | | -2.0041 | 0.0244 | 6 | 24.17% | | literal | 0.75 | 1 | t0=0.01 | -1.0949 | 0.0200 | 49 | 22.80% | | literal | 0.75 | 1 | t0=0.15 | -1.1130 | 0.0198 | 44 | 22.56% | | literal | 0.75 | 1 | adapt=0 | -1.1182 | 0.0200 | 44 | 22.83% | | density | 1 | 1 | n0=20 | +0.0133 | 0.0095 | 526 | 16.59% | | density | 1 | 1 | n0=20 adapt=0 | -0.0019 | 0.0070 | 494 | 16.62% | | density | 1 | 0.7 | n0=20 | -0.2163 | 0.0172 | 346 | 17.87% | | density | 1 | 1.4 | n0=20 | -0.1379 | 0.0190 | 421 | 17.15% | | density | 0.8 | 1 | n0=20 | -1.1333 | 0.0280 | 89 | 18.52% | | density | 1.25 | 1 | n0=20 | -1.4228 | 0.0245 | 32 | 19.08% | | density | 1 | 1.15 | n0=200 | -0.0100 | 0.0150 | 494 | 16.63% | | density | 1 | 0.87 | n0=200 | -0.0208 | 0.0144 | 473 | 16.87% | | density | 1.08 | 1 | n0=200 | -0.2680 | 0.0165 | 306 | 17.06% | | density | 0.93 | 1 | n0=200 | +0.0183 | 0.0161 | 513 | 16.62% | | density | 1 | 1 | n0=1000 | +0.0130 | 0.0086 | 538 | 16.59% | | **density (shipped)** | **1** | **1** | **n0=200** | **+0.0171** | **0.0092** | **516** | **16.59%** | For the shipped setting, worst5 is (60218, -1.163), (60523, -0.990), (60084, -0.959), (60335, -0.912), (60407, -0.911). I chose ka=1, wa=1, n0=200: it is the highest mean with se under 0.01. ka=0.93 is a tie within noise, with a larger se. The frozen file was re-run on the choose set and is bit-identical to the tuning row (paired diff 0 on all 1024 seeds). ## Report set (seeds 10000..12047, n = 2048, each arm scored once) | comparison | n | mean_diff | se | wins | worst5 (seed, diff) | |---|---|---|---|---|---| | **TEST - BASE** | 2048 | **+0.0128** | **0.0068** | 1061 | (11937, -1.583), (10971, -1.351), (11510, -1.232), (11463, -1.154), (10990, -1.023) | | BASE with true flag (`oraclearb=1`, plain tables) - BASE | 2048 | +3.7226 | 0.0237 | 2044 | (11701, -4.076), (10359, -0.523), (11488, -0.500), (11519, -0.147), (10578, +0.446) | Against HA on the same seeds (HA rows from `calib_out_2048.json`): | arm | vs HA | se | |---|---|---| | BASE | +2.828 | 0.041 | | TEST | +2.841 | 0.041 | | BASE with true flag | +6.551 | 0.046 | The gap that the true flag closes for Gaussian + plain is **3.72**. This idea closes **0.013 of it (0.3%)**. The README's "worth 2.7" figure is for the lattice + qm version; with plain tables the flag is worth 3.72 on this replica. ### Confusion matrix, report seeds, first own trade of each step The true flag is from the replica. | | called arb | called retail | misclass | |---|---|---|---| | BASE, true arb | 6,301,005 | 1,309,754 | | | BASE, true retail | 1,048,181 | 5,566,964 | 16.57% | | TEST, true arb | 6,319,877 | 1,289,467 | | | TEST, true retail | 1,061,379 | 5,553,393 | 16.53% | TEST makes 32,070 more arb calls than BASE (0.23% of the 14,225,904 classified first trades). It reduces misclassifications by 7,089 (from 2,357,935 to 2,350,846). ### Seconds per seed Seed timing is elapsed time per seed inside one worker, for 10,000 steps, with 6 workers in parallel on 6 cores: | arm | report set | choose set | |---|---|---| | BASE | 0.083 s | 0.086 s | | TEST | 0.126 s | 0.106 s | | true-flag diagnostic | 0.115 s | | Total wall time for 2048 seeds: 29 s (BASE), 44 s (TEST). ## Verdict: not worth porting The bar was more than +0.3 with se under 0.1. The result is +0.013 with se 0.007 on the report set, about 2 se and far below the bar. Misclassification is unchanged at 16.5%. **Why:** 1. **The size an arb trades carries no information beyond the post-trade spot.** Given `sp` and the fee in force, the Y size is a deterministic monotone function of the post-trade spot `s`. So "the size the filter's estimate implies" is the same as "the price the trade implies, `lp`". `real.py` already scores `lp` against the filter's predictive density N(m, v), and that is the likelihood-optimal form of size-to-gap closeness. Written in size space with a consistent Jacobian, the density form is an exact reparametrisation of the base arb term (`ka=1, wa=1, adapt=0` gives -0.002 se 0.007). 2. **The literal form loses.** It uses a kernel around one implied size: the tail mean of the belief beyond the band edge. That throws away the shape of the predictive density, which is a cut Gaussian piled up against the band edge, not a symmetric kernel around its mean. It costs 1.1 at best and up to 50 when the kernel is wide (misclassification 21 to 28%). With wide kernels the soft update keeps mixing wrong pins into the belief, and the filter loses the price. 3. **The retail half has little room to help.** The retail-size half ("lognormal around the run's mean") is the only new information, and the replica leaves it little room. The run's order-size mean is drawn from U(19, 21) with a fixed log-sd of 1.2. Own fills are the router's split of that order, so a per-run size law moves the retail likelihood by a few percent at most. Learning it from the certainly-retail second trades gives +0.013 to +0.017. 4. **The overlap is in the data, not in the size feature.** The remaining 16% error comes from arb and retail first trades whose implied prices are equally plausible under the belief. No reparametrisation of the same observation can separate them. This agrees with Taylor's note that a table classifier on the filter's own features gets 18.8% against the lattice's 16.2%. A gain here needs information the size does not carry, for example the rival-quote structure or the order-arrival pattern, not a transform of `s`. ## The complete TEST file (`real_s2g.py`, sha256 `9b9ce0d5c385e3a93c98a505181f5c88afc6e9c674298c2c34967b74d88fa0a2`) To run: `python3 -I real_s2g.py pol_mid.json plain:soft=1,s2g=2` from `research/solved`. The scores above came from my scratch harness, which calls the same `RealPolicy` class. ```python """Solved policy driven ONLY by what afterSwap sees (side, amounts, timestamp, reserves). Filter: Gaussian belief over ln p (mean, variance). - new step: variance grows by sigma^2 a step; steps with no trade of ours truncate the belief to our no-arb interval - first trade of a step: arb or retail by likelihood ratio. Arb => p is exact (post-trade spot through the fee in force). Retail => belief truncated to the no-arb interval. Rival: absolute spot dead-reckoned (router identity on own retail trades, band clip against the price estimate). Policy: table lookup at the estimates. Oracle arguments are used for diagnostics only. usage: python3 -I real.py [variant ...] variants: ce | qm: ... """ import sys, os, math, json, time from multiprocessing import Pool HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import replica2 as R from evalpol import DPPolicy, load, stats, CALIB G2 = 1.0 - R.NF SIG2 = 0.000945 ** 2 RHO = 0.26 # P(a no-arb step's first own trade is retail on a given side) LD_M, LD_S = math.log(11.12e-4), 1.176 # own-fill spot move of a retail first trade: lognormal fit SQ2PI = 2.5066282746310002 H0 = 0.55 LD_FULL = math.log(20.0) - 0.72 + math.log(2.0 / 10000.0) # ln median of a full order's displacement def phi(x): return math.exp(-0.5 * x * x) / SQ2PI def Phi(x): return 0.5 * (1.0 + math.erf(x / 1.4142135623730951)) def trunc(m, v, lo, hi): """moments of N(m, v) restricted to (lo, hi); returns (mass, mean, var)""" if v <= 0.0: return (1.0 if lo <= m <= hi else 0.0), m, 0.0 sd = math.sqrt(v); a = (lo - m) / sd; b = (hi - m) / sd Z = Phi(b) - Phi(a) if Z < 1e-12: # belief is outside the interval: snap to the nearer edge return Z, (lo if m < lo else hi), v * 0.25 pa, pb = phi(a), phi(b) r = (pa - pb) / Z mean = m + sd * r var = v * (1.0 + (a * pa - b * pb) / Z - r * r) if var < 0.0: var = 0.0 return Z, mean, var class RealPolicy(DPPolicy): wants_j = True def __init__(self, T, opt=None): DPPolicy.__init__(self, T) self.o = opt or {} self.Q = load(self.o['qm']) if 'qm' in self.o else None self.synced = False self.m = math.log(100.0); self.v = 0.0 self.tlast = 0; self.cnt = 0 self.s2h = 100.0 self.fb = self.fa = 0.003 self.sp = 100.0 # own spot after our last callback # size-to-gap: running retail law of ln(own fill in Y), prior = the fitted spot-move law mapped to Y self.rn = self.o.get('n0', 200.0); self.rmu = math.log(0.5e4 * 11.12e-4); self.rsd = LD_S self.rss = self.rn * self.rsd ** 2 # diagnostics self.nmis = 0; self.ncls = 0; self.se = 0.0; self.nse = 0; self.se2 = 0.0 def init(self, *a): fb, fa = self._fees(0.0, 0.0, 0) self.fb, self.fa = R.clampf(fb), R.clampf(fa) return fb, fa def lookup(self, w, w2, phs, sd): Q = self.Q if Q is None: return self._fees(w, w2, phs) sd = sd * self.o.get('sdscale', 1.0) S = Q['S']; k = 0 while k < len(S) - 2 and sd >= S[k + 1]: k += 1 tt = (sd - S[k]) / (S[k + 1] - S[k]) if tt < 0.0: tt = 0.0 if tt > 1.0: tt = 1.0 tag = '0c' if phs == 0 else (str(phs) + ('s' if self.synced else 'c')) A, B = Q['A' + tag], Q['B' + tag] pw = w / self.h + self.W0 if pw < 0: pw = 0.0 if pw > self.NW - 1: pw = float(self.NW - 1) l2 = w2 / self.h if phs == 0: if l2 < -self.FB: l2 = -self.FB if l2 > self.FB: l2 = self.FB p2 = l2 + self.FB; n2 = 2 * self.FB + 1 else: p2 = self._pos2(l2); n2 = self.N2 i = int(pw); j = int(p2) if i > self.NW - 2: i = self.NW - 2 if j > n2 - 2: j = n2 - 2 f = pw - i; g = p2 - j def bl(Tb): return (Tb[i][j] * (1 - f) + Tb[i + 1][j] * f) * (1 - g) + (Tb[i][j + 1] * (1 - f) + Tb[i + 1][j + 1] * f) * g a = bl(A[k]) * (1 - tt) + bl(A[k + 1]) * tt b = bl(B[k]) * (1 - tt) + bl(B[k + 1]) * tt fa = 1.0 - math.exp(-(a - w) * 1e-4) if a > w else 0.0 fb = 1.0 - math.exp(-(b + w) * 1e-4) if b > -w else 0.0 return fb, fa def on_trade(self, is_buy, ax, ay, t, x, y, arb, p, s2, j): s = y / x dt = t - self.tlast is_arb = False if dt >= 1: self.cnt = 0 lo = math.log(self.sp * (1.0 - self.fb)); hi = math.log(self.sp / (1.0 - self.fa)) # no-arb interval of the quotes in force m, v = self.m, self.v for _ in range(min(dt - 1, 12)): # fully silent steps v += SIG2 _, m, v = trunc(m, v, lo, hi) v += SIG2 # this step's move # arb hypothesis: p exactly at the post-trade band edge lp_arb = math.log(s * (1.0 - self.fb)) if is_buy else math.log(s / (1.0 - self.fa)) sd = math.sqrt(v) o = self.o l_arb = phi((lp_arb - m) / sd) / sd eps = o.get('tail', 0.0) if eps > 0.0: l_arb = (1.0 - eps) * l_arb + eps * phi((lp_arb - m) / 40e-4) / 40e-4 Z, mt, vt = trunc(m, v, lo, hi) d = abs(math.log(s / self.sp)) if o.get('fill', 0): # retail fill density from the order-size law and how our quote compared with the rival's (estimates) pe0 = math.exp(mt) r2 = self.s2h if r2 < pe0 * G2: r2 = pe0 * G2 elif r2 > pe0 / G2: r2 = pe0 / G2 if is_buy: # trader sold: compare bids (log) delta = math.log(r2 * G2) - math.log(self.sp * (1.0 - self.fb)) # >0: rival bid better else: delta = math.log(self.sp / (1.0 - self.fa)) - math.log(r2 / G2) # >0: rival ask better if d > -delta: D = 2.0 * d + delta; jac = 2.0 else: D = d; jac = 1.0 D = max(D, 1e-9) l_ret = Z * 0.5 * H0 * jac * phi((math.log(D) - LD_FULL) / 1.2) / (D * 1.2) else: l_ret = Z * RHO * phi((math.log(max(d, 1e-9)) - LD_M) / LD_S) / (max(d, 1e-9) * LD_S) if o.get('s2g', 0) == 2: # size-to-gap, density form: closeness of the observed size to the size implied by the # filter's untruncated estimate N(m, ka^2 v), mapped to ln-size; retail = run's lognormal size law q = max(ay, 1e-9) Jq = 0.5 * math.sqrt(s) / max(abs(math.sqrt(s) - math.sqrt(self.sp)), 1e-12) # |d ln q / d ln p| sa = o.get('ka', 1.0) * sd l_arb = o.get('wa', 1.0) * phi((lp_arb - m) / sa) / sa / Jq zr = (math.log(q) - self.rmu) / self.rsd l_ret = Z * RHO * phi(zr) / self.rsd elif o.get('s2g', 0): # size-to-gap: compare the trade's Y size with the size an arb would have traded # if the price sat where the filter expects it beyond the band edge on this side rk = math.sqrt(R.K); q = max(ay, 1e-9) if is_buy: # pool bought X: arb case needs ln p < lo Pt, mu, vv = trunc(m, v, lo - 1.0, lo) ss = math.exp(mu) / (1.0 - self.fb) qs = rk * (math.sqrt(self.sp) - math.sqrt(ss)) else: # pool sold X: arb case needs ln p > hi Pt, mu, vv = trunc(m, v, hi, hi + 1.0) ss = math.exp(mu) * (1.0 - self.fa) qs = rk * (math.sqrt(ss) - math.sqrt(self.sp)) / (1.0 - self.fa) qs = max(qs, 1e-9) J = 0.5 * rk * math.sqrt(ss) / qs # |d ln q* / d ln p| tau = math.sqrt((o.get('ka', 1.0) * J * math.sqrt(vv)) ** 2 + o.get('t0', 0.05) ** 2) za = (math.log(q) - math.log(qs)) / tau l_arb = o.get('wa', 1.0) * Pt * phi(za) / tau zr = (math.log(q) - self.rmu) / self.rsd l_ret = Z * RHO * phi(zr) / self.rsd pi = l_arb / (l_arb + l_ret) if (l_arb + l_ret) > 0 else 0.0 if o.get('oraclearb', 0): pi = 1.0 if arb else 0.0 is_arb = pi > 0.5 self.ncls += 1 if is_arb != arb: self.nmis += 1 if o.get('soft', 0) and pi < o.get('hard', 0.995) and pi > 1.0 - o.get('hard', 0.995): m2 = pi * lp_arb + (1.0 - pi) * mt v2 = pi * (lp_arb - m2) ** 2 + (1.0 - pi) * (vt + (mt - m2) ** 2) self.m, self.v = m2, v2 elif is_arb: self.m, self.v = lp_arb, 0.0 else: self.m, self.v = mt, vt # rival was arbed at the start of this step: band clip against the estimate pe = math.exp(self.m) if self.s2h < pe * G2: self.s2h = pe * G2 elif self.s2h > pe / G2: self.s2h = pe / G2 self.synced = False elif self.o.get('s2g', 0) and self.o.get('adapt', 1): # a second own trade in a step is surely retail: learn this run's retail size law (ln Y) lq = math.log(max(ay, 1e-9)); self.rn += 1.0 dlt = lq - self.rmu; self.rmu += dlt / self.rn self.rss += dlt * (lq - self.rmu) self.rsd = max(0.3, math.sqrt(self.rss / self.rn)) pe = math.exp(self.m) if not is_arb: if is_buy: cand = s * (1.0 - self.fb) / G2 if cand < self.s2h: self.s2h = cand; self.synced = True else: cand = s * G2 / (1.0 - self.fa) if cand > self.s2h: self.s2h = cand; self.synced = True self.cnt += 1 phs = 1 if self.cnt <= 1 else 2 else: phs = 0 err = (self.m - math.log(p)) * 1e4 self.se += err * err; self.nse += 1 if not is_arb: self.se2 += err * err self.tlast = t; self.sp = s w = (math.log(s) - self.m) * 1e4; w2 = (math.log(self.s2h) - self.m) * 1e4 fb, fa = self.lookup(w, w2, phs, math.sqrt(self.v) * 1e4) self.fb, self.fa = R.clampf(fb), R.clampf(fa) return fb, fa def ev(a): path, opt, seeds = a T = load(path); out = [] for s in seeds: pol = RealPolicy(T, opt) r = R.run(s, pol) out.append((s, r['edge'], r['retail'], r['arb'], r['rv'], r['av'], pol.nmis, pol.ncls, pol.se, pol.nse)) return out def report(pool, path, opt, seeds, ref, label): t0 = time.time() res = pool.map(ev, [(path, opt, seeds[i:i + 8]) for i in range(0, len(seeds), 8)], chunksize=1) res = {r[0]: r for b in res for r in b} n = len(seeds); mean = lambda i: sum(res[s][i] for s in seeds) / n mh, seh = stats([res[s][1] - ref['HA'][s]['edge'] for s in seeds]) mis = sum(res[s][6] for s in seeds) / max(1, sum(res[s][7] for s in seeds)) rms = math.sqrt(sum(res[s][8] for s in seeds) / max(1, sum(res[s][9] for s in seeds))) print(f'{label:24s} edge {mean(1):8.3f} retail {mean(2):8.3f} arb {mean(3):7.3f} rv {mean(4):6.0f} av {mean(5):6.0f} vs HA {mh:+.3f} se {seh:.3f} misclass {mis:.2%} p rms {rms:.1f} bps ({time.time()-t0:.0f}s)', flush=True) return res if __name__ == '__main__': path = sys.argv[1] n = int(os.environ.get('NSEED', '2048')) seeds = list(range(10000, 10000 + n)) rows = json.load(open(CALIB)); ref = {} for r in rows: ref.setdefault(r['kind'], {})[r['seed']] = r with Pool(14) as pool: for spec in (sys.argv[2:] or ['ce']): name, _, kv = spec.partition(':') opt = {} for item in kv.split(','): if item: k, _, val = item.partition('=') opt[k] = os.path.join(HERE, val) if k == 'qm' else float(val) report(pool, path, opt, seeds, ref, spec) ```