feat(Notebooks): Add backtest-slippage use-case notebook - #36
Conversation
Percent-format .py notebook implementing the backtest-slippage outline spec from #34. Runs end-to-end on the DEMO-KEY May 2025 preview slice; .ipynb and HTML are generated by CI. Registers the notebook in the README and preview-notebooks.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
szemyd
left a comment
There was a problem hiding this comment.
Quant review of notebooks/backtest-slippage.py
Verdict: REQUEST CHANGES — posted as a comment review because GitHub blocks formal change-requests from the PR author's own identity. Treat the blocking items below as merge-blockers; this review is written so a follow-up agent can implement it directly.
Reviewed the .py source against the executed .ipynb outputs on this branch. The scaffolding is genuinely good — schema audit that fails loudly, documented key resolution, correct signal/return timing (no look-ahead), honest "one month is an illustration" disclaimers, and consistency with the house run_position_backtest. But the notebook's two central quantitative claims don't survive scrutiny yet. Inline comments carry the details and concrete fixes; summary below.
Blocking
- Fees are absent from the "realistic" cost model (inline at the scenario cell). Modelled dynamic cost @ $50k averages 0.21 bps; Binance USDT-M taker fees are ~1.7–5 bps. As written, the notebook teaches that a flat 5 bps assumption is "too harsh" when it is in fact a fair proxy for all-in taker cost, and that a properly modelled cost is ~negligible. Add a fee term or reframe the model explicitly as the microstructure-only component.
- The calibration story is circular (inline at the calibration cell). The "slippage-implied coefficient" (3.80 bps/$1M) is
(mean slippage − mean half-spread) / BACKTEST_NOTIONAL— inversely proportional to an assumed $50k that is never justified against the definition ofslippage_bps_mean. Additionally.clip(lower=0)biases the implied coefficient upward, and the executed MAE (0.296 bps) exceeds the mean measured level (0.195 bps), i.e. the model has no pointwise skill in-sample — only the means agree. State the metric definition, match the notional to it, drop or justify the clip, and report corr/R² alongside MAE. impact_per_notionalunits are guessed, not verified (inline). Everything downstream — including the siblingexecution-liquidity-timingnotebook that imports this convention — inherits the ×1e4 assumption. Verify against the catalogue docs and cite.
Important
- Position/notional convention unstated; sign flips under-charged ~2× on the impact term under the model's own linearity (inline). One sentence + a printed turnover stat fixes it.
Minor (no inline comment)
- Section numbering jumps
## 2 → ## 3 → ## 6(template remnant). Renumber. - The equity chart is unreadable around the interesting comparison because the −70% flat-cost curve compresses the zero-vs-dynamic gap (~4pp) to nothing; log scale, separate panel, or a saner flat benchmark all work.
- Chart 2's violin mixes
slippage_bps_std(a dispersion statistic) in with level statistics (mean/p95/vwap) on one "bps" axis — visually implies they're comparable quantities. amihud_like/kyle_like_lambdaare fetched and audited but never used; either use them (they'd make a nice alternative impact proxy comparison) or drop them from the fetch.- Annualized Sharpe of −36.5 from one month is technically what the arithmetic yields, but printing it invites ridicule; consider omitting Sharpe for the strawman scenario or footnoting it.
Suggested acceptance criteria for the revision
- Cost scenarios include (or explicitly disclaim) exchange fees, and the Takeaways no longer assert a flat bps assumption is "too harsh" for taker flow.
- The calibration section states the catalogue definition of
slippage_bps_mean, prices the model at a notional consistent with that definition, and reports a pointwise fit statistic next to the MAE. - The
impact_per_notionalunit is cited from documentation, not inferred. - Re-executed
.ipynb/HTML committed by CI so outputs match the revised source.
Generated by Claude Code
- Scope the cost model as microstructure-only (half-spread + impact) with a prominent fee disclaimer; a taker fee (~5 bps VIP0) is cited, not modelled. Drop the "flat is too harsh" framing; use a comparable ~1 bps flat benchmark and a readable equity chart. - Cite the catalogue definition of impact_per_notional (|return| / |net notional flow| -> fraction/USD; x1e4 -> bps/USD); drop the redundant abs() (non-negative by construction) and guard divide-by-near-zero infinities. - Rewrite the calibration honestly: measured slippage_bps_mean is per-trade, beyond-the-touch cost (excludes half-spread), so compare the model's IMPACT term priced at the actual average executed trade notional (volume_notional / taker trade count) against measured slippage; report corr and MAE (not just agreeing means), drop the upward-biasing clip, and print the notional at which the model mean would match measured slippage to expose the earlier $50k circularity. - State the notional convention (unit position = BACKTEST_NOTIONAL; flips priced as two one-way clips) and print turnover in units and USD. - Split slippage_bps_std (dispersion) out of the level-statistics violin. - Show impact_coef in bps/$1M so the coverage table is informative. - Renumber sections; footnote the one-month annualised Sharpe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
- Replace the circular schedule (hours picked and scored on the same in-window means) with a calibrate/evaluate split: rank hours by MEDIAN cost on the first CALIBRATION_DAYS, then evaluate the fixed best/uniform/worst hour-sets on the held-out remainder. Report hour-ranking persistence (calibrate->evaluate) and agreement with measured slippage_bps_mean by hour, and show per-hour IQR bars, so a surviving gap is real evidence and weak values are stated as the one-month verdict. Rank on the median because the cost distribution is heavy-tailed. - Fold the SIGNED flow_toxicity_score to |toxicity| (catalogue: signed notional flow x vol) in the liquidity score, the Chart 3 quintiles, and the Chart 4 hourly means; label axes accordingly. - Cite the catalogue impact_per_notional units (mirrors #36) and note the cost is ~98% impact term at $50k; guard the zero-flow infinity. - Label depth in BTC, add dollar depth and ORDER_NOTIONAL as a % of level-20 depth. - Fetch measured slippage for the validation; drop the unused flow/impact fields. - Note score_spread/score_impact overlap cost by construction; add a fees-cancel note; robust quintile binning; renumber sections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
|
Thanks for the thorough review — all items addressed in the latest commit (CI re-executed against real May 2025 data and is green). Point by point: Blocking
Important / minor: notional convention stated (unit = Generated by Claude Code |
szemyd
left a comment
There was a problem hiding this comment.
Quant re-review (round 2) of notebooks/backtest-slippage.py @ 9262367
Verdict: no remaining merge-blockers in the notebook. One required follow-up outside the code (stale PR description, below) and two inline nits. Posted as a comment review (GitHub blocks formal states from the PR author's identity).
All four round-1 blocking/important items are fixed, and I verified each against the re-executed .ipynb:
- Fees — resolved. Scope note up top (title now says microstructure cost),
REFERENCE_TAKER_FEE_BPS = 5.0documented and referenced in the scenario markdown and Takeaways ("Fees dominate for takers"); the misleading "flat is too harsh" claim is gone; flat benchmark lowered to 1 bps so the chart is readable (zero +2.47% / flat-1bps −19.9% / dynamic −1.46%). - Calibration circularity — resolved, and honestly. The comparison is now impact-only at the bar's actual average trade size (median $5.2k, computed from
volume_notional / taker trade count) vs measured per-trade slippage, with pointwise stats printed in the chart title and table: corr −0.01, MAE 0.195 bps, mean model 0.020 vs measured 0.195 bps. The old $50k "agreement" is exposed explicitly via "Notional where model mean = measured ($45,385)". The clip(lower=0) is gone. This is exactly the right shape: a negative validation result reported straight. - Units — addressed via quoted catalogue definitions for
impact_per_notionalandslippage_bps_mean, which now justify the ×1e4 conversion and the impact-only comparison. Caveat: the metrics docs are auth-walled from here, so I could not independently confirm the quotes — see inline note asking for an exact doc link (the cites are now load-bearing). - Notional convention — documented (1 unit =
BACKTEST_NOTIONAL= 100% of equity; flips priced as two one-way clips, stated as deliberately under-charging vs a single $100k clip), plus printed turnover diagnostics (1,234 trades ≈ $123M on $50k equity) and the Sharpe-comparability footnote.
Minor round-1 items also done: section numbering, violin split (levels vs dispersion), unused fetches dropped, impact-coef display in bps/$1M.
Remaining items
- [Required] Update the PR description. It still claims "the modelled impact coefficient (4.18 bps/$1M) calibrates closely against measured slippage (implied 3.80 bps/$1M)" — the revised notebook explicitly repudiates that framing (coincidence of the $50k size assumption; corr ≈ 0). Whoever merges will otherwise ship a summary the notebook itself contradicts. Suggested replacement: "checks the impact model against measured per-trade slippage at the actual average trade size; the honest result is weak per-bar tracking (corr ≈ 0) and a ~10× level gap, discussed in Section 6."
- [Nit, inline] Non-negativity assertion on the impact coefficient (self-enforcing units tripwire).
- [Nit, inline] One sentence stating the 10× level miss as plainly as the correlation.
Generated by Claude Code
- Add a non-negativity assertion on impact_coef_bps_per_usd as a self-enforcing units tripwire (a negative would silently credit turnover). - State the ~order-of-magnitude level miss plainly (print the measured/model ratio; spell it out in the interpretation alongside the ~zero correlation). - Link the Aperiodic trade-metrics catalogue for the impact_per_notional and slippage_bps_mean definitions the Section 6 comparison rests on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
|
Round-2 items done:
Generated by Claude Code |
|
please change the y axis starting position in 4. Chart 1 — Spread distribution by hour |
… spread (#36) Chart 1 (spread distribution by hour) anchored the y-axis at 0, leaving the hourly boxplots compressed near the top of the panel. Start the axis at the minimum observed spread instead so the intraday distribution fills the panel, per PR review feedback. CI regenerates the .ipynb and HTML from this .py. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVxnJZYNRifP6pgJ5D13bz
|
Done in ax.set_ylim(df["spread_bps"].min(), df["spread_bps"].quantile(0.99))so the hourly boxplots fill the panel rather than sitting compressed near the top. CI will re-execute the notebook and regenerate the Generated by Claude Code |
Summary
Adds
notebooks/backtest-slippage.py, implementing the backtest-slippage outline from #34 (use case: Improve your backtest).Builds a microstructure transaction-cost model — half-spread plus market impact — from Aperiodic's
l1_price,impactandslippagemetrics, and shows how different cost assumptions reshape a simple momentum backtest.DEMO-KEY.slippage_bps_meanat the actual average trade size (volume_notional / taker trade count). The result is reported straight: the model under-predicts measured slippage by roughly an order of magnitude at the average trade size, with ~zero per-bar correlation —impact_per_notional(net-flow-based) is an order-of-magnitude proxy, not a per-trade predictor. The notebook prints the notional at which the model mean would equal measured slippage to make the size dependence explicit.run_position_backtest..ipynband HTML are generated by CI. Adds one README row and onepreview-notebooks.jsonslug (also touched by the sibling notebook PRs, so whichever merges last needs a trivial rebase).Part of splitting #35 into one PR per notebook. Implements #34.
🤖 Generated with Claude Code