{
 "item": "f-arrival-rate-online",
 "approach": "Blind build from the simulator only. S0: fair value tracked from hidden-competitor ladder sweeps (after an up-sweep p sits just under the new best ask, after a down-sweep just over the new best bid); quote only strictly inside the competitor spread, >= min_edge_ticks from fair; per-side size set where marginal retail edge equals marginal arb cost: P(N > q*price) = arb_cost/(rate/2 * half_spread), N lognormal(sigma 1.2), using band-centre priors rate 0.253, mean notional 4.488. S1 = S0 + online estimate of (retail rate, mean notional) from own fills: because quotes are strictly inside, every retail order on that side hits them first; each side-step is no-fill (no arrival), partial fill (one arrival, notional ~ fill*unit price) or full fill (retail above cap or arb sweep, dropped). Conditioning on 'not full' makes the likelihood independent of the unknown arb rate: P(none)=e^-r/D, p(partial,y)=(1-e^-r)f(y)/D, D=e^-r+(1-e^-r)F(c). Joint grid MLE over 24 log-notional x 14 per-side-rate values with a Gaussian prior; the estimate replaces the priors in the size rule. A first version that treated full fills as censored retail grossly overestimated size (arb fills look like big orders) and lost; it was replaced before formal tuning.",
 "tried": [
  {
   "variant": "S0 a: min_edge 2, arb_cost 0.0004",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 0.26,
   "se": 0.66
  },
  {
   "variant": "S0 b: min_edge 2, arb_cost 0.0002",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": -5.19,
   "se": 0.93
  },
  {
   "variant": "S0 c: min_edge 2, arb_cost 0.0008 (frozen)",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 4.22,
   "se": 0.55
  },
  {
   "variant": "S0 d: min_edge 2.5, arb_cost 0.0008",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 4.23,
   "se": 0.55,
   "note": "same quotes as c almost always (fair sits on half-ticks); kept c"
  },
  {
   "variant": "S1 a: arb_cost 0.0008, prior weights 4/4",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 4.41,
   "se": 0.54
  },
  {
   "variant": "S1 b: arb_cost 0.0008, prior weights 1/1",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 4.3,
   "se": 0.53
  },
  {
   "variant": "S1 c: arb_cost 0.0012, prior weights 4/4 (frozen)",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 5.77,
   "se": 0.55
  },
  {
   "variant": "S1 d: arb_cost 0.0016, prior weights 4/4",
   "seeds": "115000..115499",
   "n": 500,
   "mean_total_edge": 5.75,
   "se": 0.51
  },
  {
   "variant": "development only, before formal tuning: S0 with min_edge 1/2/3 at arb_cost 0.0004 on 115000..115019 (n=20): -0.34, 2.55, 0.91; an earlier S0 quoting around the plain competitor midpoint scored -63.2 on the same 20 seeds"
  }
 ],
 "next": "Most of S1's holdout gain (+1.36) comes from the different arb_cost it was tuned to, not the estimator: S1 at S0's arb_cost 0.0008 on the same 1000 holdout seeds gives +0.16 +/- 0.09 (retail -0.16, arb +0.32), not significant. Retail rate/size knowledge is worth little to sizing in this baseline, which fits wave 3's +1 for retail parameters. Next: tune S0 on arb_cost 0.0012 for a clean comparison, or use the rate estimate to decide when to quote (skip low-rate regimes), not how much. The baseline itself (about 5 edge against B's 99.7) is far below B, so a graft onto B is what matters.",
 "code": "from __future__ import annotations\n\nimport math\nfrom statistics import NormalDist\n\nfrom orderbook_pm_challenge.strategy import BaseStrategy\nfrom orderbook_pm_challenge.types import CancelAll, PlaceOrder, Side, StepState\n\n_ND = NormalDist()\nSIZE_SIGMA = 1.2  # retail log-notional sigma (fixed in the simulator config)\n\n\nclass Strategy(BaseStrategy):\n    \"\"\"S1 (S0 + online retail rate/size estimate): quote one tick inside the hidden competitor ladder, around its midpoint.\n\n    Fair value is tracked from competitor ladder sweeps (the arbitrageur keeps\n    the true probability between the competitor's best bid and ask). Quotes go\n    only strictly inside the competitor spread, at least min_edge_ticks from fair. Each side's size is\n    the notional threshold where the marginal retail fill edge equals the\n    marginal arbitrage cost: P(N > q * price) = arb_cost / (rate_per_side * half_spread).\n    \"\"\"\n\n    min_edge_ticks = 2.0   # do not quote closer than this to the midpoint\n    arb_cost = 0.0012      # expected arbitrage loss per share per step\n    max_shares = 60.0\n    min_shares = 0.5\n    prior_rate = 0.253     # retail arrivals per step (centre of the variance band)\n    prior_notional = 4.488 # retail mean notional (centre of the variance band)\n\n    # --- online retail arrival-rate and order-size estimate -------------------\n    # Every quote rests strictly inside the competitor spread, so each retail\n    # order on that side reaches it first. Per side-step with cap c = size*price\n    # (notional): no fill -> no arrival; partial fill -> one arrival with\n    # notional ~ fill * unit price. A full fill is retail above c or an\n    # arbitrage sweep, so it is dropped; conditioning on \"not full\" keeps the\n    # likelihood free of the unknown arb rate:\n    #   P(none) = e^-r / D,  p(partial, y) = (1 - e^-r) f(y) / D,\n    #   D = e^-r + (1 - e^-r) F(c),  r = per-side rate, f/F lognormal(mu, 1.2).\n    # Joint grid MLE over (r, mu) plus a Gaussian prior around the band centres.\n    size_prior_weight = 4.0\n    rate_prior_weight = 4.0\n\n    def _init_estimator(self) -> None:\n        s2 = 0.5 * SIZE_SIGMA * SIZE_SIGMA\n        self.mgrid = [math.log(1.5) + i * 0.08 for i in range(24)]   # log mean notional\n        self.rgrid = [0.04 + 0.02 * j for j in range(14)]            # per-side rate\n        lm0, r0 = math.log(self.prior_notional), 0.5 * self.prior_rate\n        self.loglik = [[-0.5 * self.size_prior_weight * ((lm - lm0) / 0.35) ** 2\n                        - 0.5 * self.rate_prior_weight * ((r - r0) / 0.05) ** 2\n                        for r in self.rgrid] for lm in self.mgrid]\n        self.mus = [lm - s2 for lm in self.mgrid]\n        self.er = [math.exp(-r) for r in self.rgrid]\n        self.estimate = (self.prior_rate, self.prior_notional)\n\n    def observe(self, state: StepState) -> None:\n        if not hasattr(self, \"mgrid\"):\n            self._init_estimator()\n        placed = getattr(self, \"placed\", None)\n        if not placed:\n            return\n        fair = getattr(self, \"est\", 50.0) / 100.0\n        changed = False\n        for side, (tick, q) in placed.items():\n            filled = state.buy_filled_quantity if side is Side.BUY else state.sell_filled_quantity\n            if filled >= q - 0.011:\n                continue\n            unit = max(fair, 0.05) if side is Side.BUY else tick / 100.0\n            y = math.log(max(filled * unit, 0.005)) if filled > 1e-9 else None\n            self._add_event(math.log(q * unit), y)\n            changed = True\n        if changed:\n            i, j = max(((i, j) for i in range(len(self.mgrid)) for j in range(len(self.rgrid))),\n                       key=lambda ij: self.loglik[ij[0]][ij[1]])\n            self.estimate = (2.0 * self.rgrid[j], math.exp(self.mgrid[i]))\n\n    def _add_event(self, logc: float, y: float | None) -> None:\n        s = SIZE_SIGMA\n        for i, mu in enumerate(self.mus):\n            big_f = _ND.cdf((logc - mu) / s)\n            row = self.loglik[i]\n            dens = -0.5 * ((y - mu) / s) ** 2 if y is not None else 0.0\n            for j, e in enumerate(self.er):\n                d = math.log(e + (1.0 - e) * big_f)\n                row[j] += (math.log(1.0 - e) + dens - d) if y is not None else (-self.rgrid[j] - d)\n\n    def rate_and_notional(self, state: StepState) -> tuple[float, float]:\n        return self.estimate\n\n    def side_size(self, rate: float, notional: float, half_spread: float, price: float) -> float:\n        if half_spread <= 0.0:\n            return 0.0\n        tail = self.arb_cost / (0.5 * rate * half_spread)\n        if tail >= 1.0:\n            return self.min_shares\n        tail = max(tail, 1e-6)\n        mu_ln = math.log(notional) - 0.5 * SIZE_SIGMA * SIZE_SIGMA\n        threshold = math.exp(mu_ln + SIZE_SIGMA * _ND.inv_cdf(1.0 - tail))\n        return max(self.min_shares, min(self.max_shares, threshold / price))\n\n    def on_step(self, state: StepState):\n        self.observe(state)\n        cb = state.competitor_best_bid_ticks\n        ca = state.competitor_best_ask_ticks\n        if cb is None and ca is None:\n            return [CancelAll()]\n        if cb is None:\n            cb = max(0, ca - 2)\n        if ca is None:\n            ca = min(100, cb + 2)\n        # Fair value: after an up-sweep the true probability sits just under the\n        # new best ask; after a down-sweep just over the new best bid.\n        prev = getattr(self, \"last_book\", None)\n        est = getattr(self, \"est\", 0.5 * (cb + ca))\n        if prev is not None:\n            if ca > prev[1]:\n                est = ca - 0.5\n            elif cb < prev[0]:\n                est = cb + 0.5\n        est = min(max(est, cb), ca)\n        self.est = est\n        self.last_book = (cb, ca)\n        mid = est\n        rate, notional = self.rate_and_notional(state)\n\n        bid = min(cb + 1, math.floor(mid - self.min_edge_ticks))\n        ask = max(ca - 1, math.ceil(mid + self.min_edge_ticks))\n        actions = [CancelAll()]\n        self.placed = {}\n        cash = state.cash\n        if 1 <= bid <= 99 and bid > cb:\n            p = bid / 100.0\n            q = self.side_size(rate, notional, (mid - bid) / 100.0, p)\n            q = math.floor(min(q, 0.9 * cash / p) * 100) / 100\n            if q > 0:\n                actions.append(PlaceOrder(side=Side.BUY, price_ticks=bid, quantity=q))\n                self.placed[Side.BUY] = (bid, q)\n                cash -= p * q\n        if 1 <= ask <= 99 and ask < ca:\n            p = ask / 100.0\n            q = self.side_size(rate, notional, (ask - mid) / 100.0, p)\n            uncovered = max(0.0, q - state.yes_inventory)\n            if uncovered > 0 and (1 - p) * uncovered > 0.9 * cash:\n                q = max(0.0, q - uncovered + 0.9 * cash / (1 - p))\n            q = math.floor(q * 100) / 100\n            if q > 0:\n                actions.append(PlaceOrder(side=Side.SELL, price_ticks=ask, quantity=q))\n                self.placed[Side.SELL] = (ask, q)\n        return actions\n",
 "code_sha256": "1944aff8e9f20f37db2f2870eca053a8511db8596c45c022ad7035ca4ce6124b",
 "code_s0": "from __future__ import annotations\n\nimport math\nfrom statistics import NormalDist\n\nfrom orderbook_pm_challenge.strategy import BaseStrategy\nfrom orderbook_pm_challenge.types import CancelAll, PlaceOrder, Side, StepState\n\n_ND = NormalDist()\nSIZE_SIGMA = 1.2  # retail log-notional sigma (fixed in the simulator config)\n\n\nclass Strategy(BaseStrategy):\n    \"\"\"S0: quote one tick inside the hidden competitor ladder, around its midpoint.\n\n    Fair value is tracked from competitor ladder sweeps (the arbitrageur keeps\n    the true probability between the competitor's best bid and ask). Quotes go\n    only strictly inside the competitor spread, at least min_edge_ticks from fair. Each side's size is\n    the notional threshold where the marginal retail fill edge equals the\n    marginal arbitrage cost: P(N > q * price) = arb_cost / (rate_per_side * half_spread).\n    \"\"\"\n\n    min_edge_ticks = 2.0   # do not quote closer than this to the midpoint\n    arb_cost = 0.0008      # expected arbitrage loss per share per step\n    max_shares = 60.0\n    min_shares = 0.5\n    prior_rate = 0.253     # retail arrivals per step (centre of the variance band)\n    prior_notional = 4.488 # retail mean notional (centre of the variance band)\n\n    def rate_and_notional(self, state: StepState) -> tuple[float, float]:\n        return self.prior_rate, self.prior_notional\n\n    def observe(self, state: StepState) -> None:\n        pass\n\n    def side_size(self, rate: float, notional: float, half_spread: float, price: float) -> float:\n        if half_spread <= 0.0:\n            return 0.0\n        tail = self.arb_cost / (0.5 * rate * half_spread)\n        if tail >= 1.0:\n            return self.min_shares\n        tail = max(tail, 1e-6)\n        mu_ln = math.log(notional) - 0.5 * SIZE_SIGMA * SIZE_SIGMA\n        threshold = math.exp(mu_ln + SIZE_SIGMA * _ND.inv_cdf(1.0 - tail))\n        return max(self.min_shares, min(self.max_shares, threshold / price))\n\n    def on_step(self, state: StepState):\n        self.observe(state)\n        cb = state.competitor_best_bid_ticks\n        ca = state.competitor_best_ask_ticks\n        if cb is None and ca is None:\n            return [CancelAll()]\n        if cb is None:\n            cb = max(0, ca - 2)\n        if ca is None:\n            ca = min(100, cb + 2)\n        # Fair value: after an up-sweep the true probability sits just under the\n        # new best ask; after a down-sweep just over the new best bid.\n        prev = getattr(self, \"last_book\", None)\n        est = getattr(self, \"est\", 0.5 * (cb + ca))\n        if prev is not None:\n            if ca > prev[1]:\n                est = ca - 0.5\n            elif cb < prev[0]:\n                est = cb + 0.5\n        est = min(max(est, cb), ca)\n        self.est = est\n        self.last_book = (cb, ca)\n        mid = est\n        rate, notional = self.rate_and_notional(state)\n\n        bid = min(cb + 1, math.floor(mid - self.min_edge_ticks))\n        ask = max(ca - 1, math.ceil(mid + self.min_edge_ticks))\n        actions = [CancelAll()]\n        self.placed = {}\n        cash = state.cash\n        if 1 <= bid <= 99 and bid > cb:\n            p = bid / 100.0\n            q = self.side_size(rate, notional, (mid - bid) / 100.0, p)\n            q = math.floor(min(q, 0.9 * cash / p) * 100) / 100\n            if q > 0:\n                actions.append(PlaceOrder(side=Side.BUY, price_ticks=bid, quantity=q))\n                self.placed[Side.BUY] = (bid, q)\n                cash -= p * q\n        if 1 <= ask <= 99 and ask < ca:\n            p = ask / 100.0\n            q = self.side_size(rate, notional, (ask - mid) / 100.0, p)\n            uncovered = max(0.0, q - state.yes_inventory)\n            if uncovered > 0 and (1 - p) * uncovered > 0.9 * cash:\n                q = max(0.0, q - uncovered + 0.9 * cash / (1 - p))\n            q = math.floor(q * 100) / 100\n            if q > 0:\n                actions.append(PlaceOrder(side=Side.SELL, price_ticks=ask, quantity=q))\n                self.placed[Side.SELL] = (ask, q)\n        return actions\n",
 "tuning_score": {
  "seeds": "115000..115499",
  "n": 500,
  "s1_mean": 5.77,
  "s0_mean": 4.22
 },
 "paired": {
  "seeds": "200000..200999",
  "n": 1000,
  "complete": true,
  "mechanism_diff": 1.3642,
  "mechanism_se": 0.1472,
  "mechanism_retail_diff": -1.2982,
  "mechanism_retail_se": 0.1538,
  "mechanism_arb_diff": 2.6624,
  "mechanism_arb_se": 0.1921,
  "reference_diff": -93.6334,
  "reference_se": 5.5555,
  "s1_mean": 6.0624,
  "s0_mean": 4.6983,
  "b_mean": 99.6959,
  "failures": [],
  "note": "mechanism_diff compares the frozen S1 (arb_cost 0.0012) with the frozen S0 (arb_cost 0.0008), so it mixes the estimator with a tuning difference. Disclosed diagnostic on the same seeds: S1 at arb_cost 0.0008 minus S0 = 0.1596 (se 0.0910), retail -0.1615 (se 0.0899), arb 0.3211 (se 0.0968). b_mean is B's fixture mean over seeds 200000..200999 (sha256 checked)."
 },
 "graft": "Keep the last placed size and price per side. Each step, skip a side whose fill >= its size (could be arb); otherwise add a no-fill or partial-fill event (notional = fill*price) to a grid log-likelihood over (per-side rate r, log mean notional), using P(none)=e^-r/D and p(y)=(1-e^-r)f(y)/D with D=e^-r+(1-e^-r)F(size*price). Only valid when the quote is best on its side. Feed the argmax into the size rule.",
 "seconds_per_simulation": {
  "s0": 0.505,
  "s1": 0.608
 }
}