From 9437ab1195fe90509131943ce2a4547482355e4f Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Thu, 3 Sep 2026 02:06:40 -0400 Subject: [PATCH] feat(research): the equities DCA benchmark, read per ticker (#371) Alpaca Phase C's second half, and the last of #371. It follows the cost-fidelity document in that order deliberately -- the PRD makes that a precondition, and a DCA sleeve priced wrong is exactly the claim it was protecting against. THE BENCHMARK IS THE MEDIAN TICKER AT +70.62%, NOT THE POOLED SLEEVE AT +145.10%. The run's pre-declared refutation condition -- "a sleeve whose terminal value is dominated by one ticker, making 'beat DCA' a statement about NVDA rather than about equities" -- fired. NVDA returned +449.35% and is 44.8% of the terminal sleeve. Without it the remaining four returned +69.03%, within 1.6pp of the median, which is why the median is the honest summary. Quoting the pooled figure would set a bar one exceptional name built and then credit or blame a strategy for clearing it. THE COST ARMS BARELY DIFFER, AND THAT IS THE USEFUL PART. keel's 7.5x equities mispricing is worth 0.18 percentage points over five years here; even the fully counterfactual `crypto_regime` arm (Coinbase's 1.2%/leg on equity bars) costs 3.08pp. A DCA sleeve pays its spread 250 times, one way, on a position it never closes. This is the crypto finding reached from the other side and it is easy to get backwards: cost is levied on the SEARCH, not on the edge. Cheap execution does not make a strategy good; expensive execution makes a mediocre one impossible. `sim/portfolio_sim` is deliberately NOT used, and the record says why: it iterates ONE_HOUR bars (`_window_bars` is `history_days * 24`, mirroring the live agent's hourly account pass) and the equities profile is ONE_DAY only by configuration. Handing it daily bars labelled as hourly would produce numbers that look right and mean nothing. The accumulation loop is in the driver and calls the SHIPPED `Dca` rule, filling at the next bar's open plus one-way slippage -- keel's own market-order convention since #258, with the deciding bar always strictly before the filling bar. Tests (written first, red before green): tests/test_dca_benchmark_is_read_per_ticker.py -- 6 tests. They pin the PROSE, because that is where the damage would be done: the headline must name the median rather than the pooled figure, the concentration must be quantified BEFORE the first heading, the ex-NVDA row must survive, all three declared cost arms must still be reported, and the record must refuse to read as a recommendation. `test_one_ticker_really_does_dominate_the_sleeve` guards the premise -- if a re-run ever balances the sleeve, these fail loudly rather than enforce a caveat that no longer applies. Mutation-verified, 5 mutants, all killed. One initially survived: gutting the concentration sentence while the table's "44.8%" column kept the pin green. The pin now requires the figure in the opening, since a reader who takes the headline and stops is exactly who the caveat is for. Closes #371 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL --- .../2026-09-03-equities-dca-benchmark.jsonl | 15 ++ .../2026-09-03-equities-dca-benchmark.md | 96 +++++++++ .../2026-09-03-equities-dca-benchmark.py | 200 ++++++++++++++++++ .../test_dca_benchmark_is_read_per_ticker.py | 83 ++++++++ 4 files changed, 394 insertions(+) create mode 100644 docs/experiments/2026-09-03-equities-dca-benchmark.jsonl create mode 100644 docs/experiments/2026-09-03-equities-dca-benchmark.md create mode 100644 docs/experiments/2026-09-03-equities-dca-benchmark.py create mode 100644 tests/test_dca_benchmark_is_read_per_ticker.py diff --git a/docs/experiments/2026-09-03-equities-dca-benchmark.jsonl b/docs/experiments/2026-09-03-equities-dca-benchmark.jsonl new file mode 100644 index 0000000..6a67690 --- /dev/null +++ b/docs/experiments/2026-09-03-equities-dca-benchmark.jsonl @@ -0,0 +1,15 @@ +{"buys": 250, "qty": "35.70395860", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "17788.07", "gain_pct": "42.30", "product_id": "MSFT-USD", "arm": "measured", "bars": 1253, "fee_pct": "0", "slippage_bp": "0.97"} +{"buys": 250, "qty": "35.67820608", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "17775.24", "gain_pct": "42.20", "product_id": "MSFT-USD", "arm": "keel_today", "bars": 1253, "fee_pct": "0", "slippage_bp": "8.19"} +{"buys": 250, "qty": "35.67820608", "deployed": "12650.00", "fees_paid": "150.00", "market_value": "17775.24", "gain_pct": "40.52", "product_id": "MSFT-USD", "arm": "crypto_regime", "bars": 1253, "fee_pct": "0.012", "slippage_bp": "8.19"} +{"buys": 250, "qty": "65.65725200", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "21327.77", "gain_pct": "70.62", "product_id": "AAPL-USD", "arm": "measured", "bars": 1253, "fee_pct": "0", "slippage_bp": "1.42"} +{"buys": 250, "qty": "65.61663601", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "21314.58", "gain_pct": "70.52", "product_id": "AAPL-USD", "arm": "keel_today", "bars": 1253, "fee_pct": "0", "slippage_bp": "7.61"} +{"buys": 250, "qty": "65.61663601", "deployed": "12650.00", "fees_paid": "150.00", "market_value": "21314.58", "gain_pct": "68.49", "product_id": "AAPL-USD", "arm": "crypto_regime", "bars": 1253, "fee_pct": "0.012", "slippage_bp": "7.61"} +{"buys": 250, "qty": "82.91063513", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "27862.12", "gain_pct": "122.90", "product_id": "GOOGL-USD", "arm": "measured", "bars": 1253, "fee_pct": "0", "slippage_bp": "0.64"} +{"buys": 250, "qty": "82.83952785", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "27838.22", "gain_pct": "122.71", "product_id": "GOOGL-USD", "arm": "keel_today", "bars": 1253, "fee_pct": "0", "slippage_bp": "9.23"} +{"buys": 250, "qty": "82.83952785", "deployed": "12650.00", "fees_paid": "150.00", "market_value": "27838.22", "gain_pct": "120.07", "product_id": "GOOGL-USD", "arm": "crypto_regime", "bars": 1253, "fee_pct": "0.012", "slippage_bp": "9.23"} +{"buys": 250, "qty": "305.61595578", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "68668.85", "gain_pct": "449.35", "product_id": "NVDA-USD", "arm": "measured", "bars": 1253, "fee_pct": "0", "slippage_bp": "2.25"} +{"buys": 250, "qty": "305.48764946", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "68640.02", "gain_pct": "449.12", "product_id": "NVDA-USD", "arm": "keel_today", "bars": 1253, "fee_pct": "0", "slippage_bp": "6.45"} +{"buys": 250, "qty": "305.48764946", "deployed": "12650.00", "fees_paid": "150.00", "market_value": "68640.02", "gain_pct": "442.61", "product_id": "NVDA-USD", "arm": "crypto_regime", "bars": 1253, "fee_pct": "0.012", "slippage_bp": "6.45"} +{"buys": 250, "qty": "18.78296641", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "17537.84", "gain_pct": "40.30", "product_id": "COST-USD", "arm": "measured", "bars": 1253, "fee_pct": "0", "slippage_bp": "0.68"} +{"buys": 250, "qty": "18.75277028", "deployed": "12500.00", "fees_paid": "0.00", "market_value": "17509.65", "gain_pct": "40.08", "product_id": "COST-USD", "arm": "keel_today", "bars": 1253, "fee_pct": "0", "slippage_bp": "16.78"} +{"buys": 250, "qty": "18.75277028", "deployed": "12650.00", "fees_paid": "150.00", "market_value": "17509.65", "gain_pct": "38.42", "product_id": "COST-USD", "arm": "crypto_regime", "bars": 1253, "fee_pct": "0.012", "slippage_bp": "16.78"} diff --git a/docs/experiments/2026-09-03-equities-dca-benchmark.md b/docs/experiments/2026-09-03-equities-dca-benchmark.md new file mode 100644 index 0000000..821604f --- /dev/null +++ b/docs/experiments/2026-09-03-equities-dca-benchmark.md @@ -0,0 +1,96 @@ +# 2026-09-03: The equities DCA benchmark (#371, Alpaca Phase C) + +**The benchmark to beat is the MEDIAN TICKER at +70.62%, not the pooled sleeve at +145.10%.** +The pre-declared refutation condition fired: NVDA returned +449.35% and is 44.8% of the +terminal sleeve, so the pooled figure is very largely one name. Excluding it, the remaining +four returned +69.03% — within 1.6 percentage points of the median, which is why the median is +the honest summary. + +Any future equity strategy claim is read against that number over these bars. A rule that +returns less than simply buying $50 of each name every Thursday has not earned the complexity +it costs. + +**This is a benchmark, not a recommendation and not advice.** It says what accumulation did on +five liquid US large caps over one five-year window that happened to contain a historic +mega-cap run. It is not evidence that accumulation works, and the window is short enough and +the universe narrow enough that the number is a yardstick, nothing more. + +## What was measured, on what + +- Data: `keel-equities.db`, 5 tickers × 1,253 ONE_DAY bars (Alpaca IEX feed). +- Rule: the **shipped** `keel.strategy.rules.dca.Dca` at constructor defaults — + `cadence_days=7, budget_usd=50, dip_bonus_pct=0, lookback_days=90`. Not a reimplementation, + and not tuned: a benchmark that has been swept is not a benchmark. +- Fills: **next bar's open plus one-way slippage**, keel's market-order convention since #258 + and what `execution/executor.py` actually places. The deciding bar is always strictly before + the filling bar, so there is no lookahead. +- 250 buys per ticker (weekly cadence lands on Thursdays — `Dca.detect` tests epoch-day + divisibility, not bar count), $12,500 deployed per ticker. +- Driver: [`2026-09-03-equities-dca-benchmark.py`](2026-09-03-equities-dca-benchmark.py). + +**Why not `sim/portfolio_sim`, which the crypto DCA measurement used.** That harness iterates +ONE_HOUR bars — `_window_bars` is `history_days * 24`, mirroring the live agent's hourly account +pass — and the equities profile is ONE_DAY only, by configuration, because Alpaca mints hourly +bars only inside a session and the daily turtle rules never read them. Handing it daily bars +labelled as hourly would have produced numbers that looked right and meant nothing. The +accumulation loop is therefore in the driver, and is the smallest thing that can be faithful. + +## Result — the `measured` arm + +| ticker | buys | deployed | terminal value | gain | share of sleeve | +| --- | --- | --- | --- | --- | --- | +| NVDA | 250 | $12,500.00 | $68,668.85 | **+449.35%** | 44.8% | +| GOOGL | 250 | $12,500.00 | $27,862.12 | +122.90% | 18.2% | +| AAPL | 250 | $12,500.00 | $21,327.77 | +70.62% | 13.9% | +| MSFT | 250 | $12,500.00 | $17,788.07 | +42.30% | 11.6% | +| COST | 250 | $12,500.00 | $17,537.84 | +40.30% | 11.4% | +| **median ticker** | | | | **+70.62%** | | +| pooled | 1,250 | $62,500.00 | $153,184.65 | +145.10% | | +| pooled, excluding NVDA | 1,000 | $50,000.00 | $84,515.80 | +69.03% | | + +The spread between +40% and +449% across five liquid mega-caps over identical bars is itself +worth registering. Whatever a strategy on this universe is measuring, ticker selection is a +larger term than anything a rule is likely to add. + +## The three cost arms — and why they barely differ + +| arm | commission | slippage | pooled gain | vs measured | +| --- | --- | --- | --- | --- | +| `measured` | 0% (Alpaca's real rate) | measured half-spread, 0.64–2.25bp | +145.10% | — | +| `keel_today` | 0% | `slippage_for_quote_volume`, 6.45–16.78bp | +144.92% | −0.18pp | +| `crypto_regime` | 1.2%/leg | as above | +142.02% | −3.08pp | + +**keel's 7.5× equities cost error is worth 0.18 percentage points over five years here.** That +is the pre-declared expectation, confirmed, and it is the most useful thing in this document. +A DCA sleeve pays its spread 250 times, one way, and never exits. The cost regime that +annihilated every crypto strategy result barely scratches it — even the fully counterfactual +`crypto_regime` arm, charging Coinbase's 1.2% per leg on equity bars, costs only 3.08pp. + +This is the same finding the crypto work reached from the other side, and it is worth stating +plainly because it is easy to get backwards: **cost is levied on the SEARCH, not on the edge.** +An active rule pays the toll on every one of its round trips, and there is no edge in the +measured library large enough to survive ~241 of them at 2.5% each. Accumulation pays it 250 +times too — but one way, on a position it never closes, so the toll is a rounding error against +a five-year hold. Cheap execution does not make a strategy good; expensive execution makes a +mediocre one impossible. + +## What this does and does not license + +It licenses the comparison: future equity results are read against **+70.62% median ticker** +over these bars, at the `measured` cost regime. + +It licenses nothing about DCA. The sleeve's return is what these five names did in this window, +levered by nothing and skilled at nothing. A different five names, or the same five over +2000–2005, would produce a different number, and the concentration above shows how little it +takes to move it. + +It says nothing about whether any keel rule can beat it, because no keel rule has yet been +measured on equities at all. That measurement is the next step and is deliberately not in this +document — running it here, against a benchmark computed in the same file, is precisely how a +result gets chosen after the fact. + +One caveat inherited whole from [the cost-fidelity measurement](2026-09-02-equities-cost-fidelity.md): +the `keel_today` arm's slippage is computed from the cached IEX-only volume statistic, which is +structurally unable to report consolidated volume (#696). That arm is therefore "what keel +charges today", which is what it is labelled — not "what a correct volume-keyed model would +charge". diff --git a/docs/experiments/2026-09-03-equities-dca-benchmark.py b/docs/experiments/2026-09-03-equities-dca-benchmark.py new file mode 100644 index 0000000..319a560 --- /dev/null +++ b/docs/experiments/2026-09-03-equities-dca-benchmark.py @@ -0,0 +1,200 @@ +"""The DCA benchmark on equities -- issue #371 (Alpaca Phase C), second half. + +**What a benchmark is for.** The crypto side has one: every strategy claim keel has ever made +is read against what simply accumulating would have done over the same bars, because a rule +that underperforms scheduled buying has not earned the complexity it costs. Equities had no +such baseline, so any future equity result would have had nothing to be compared against and +would inevitably have been compared against zero. This run supplies it. + +It follows [the cost-fidelity measurement](2026-09-02-equities-cost-fidelity.md), in that +order, deliberately: the PRD (§6.3, O4) makes the cost document a precondition -- "no strategy +claim is believed before it" -- and a DCA sleeve priced wrong is exactly the kind of claim it +was protecting against. + +DECLARED BEFORE THE RUN: + +* **Metric: terminal sleeve mark-to-market against deployed cost basis.** NEVER a profit + factor. The DCA sleeve never closes (`sim/portfolio_sim` accumulates into `dca_positions` + and never exits them), so there are no round trips to factor, and DCA is exempt from the + promotion gate by design. This matches the crypto DCA measurement of 2026-08-17 exactly, so + the two are readable side by side. +* **Constructor defaults, no sweep**: `cadence_days=7, budget_usd=50, dip_bonus_pct=0, + lookback_days=90`. A benchmark that has been tuned is not a benchmark. +* **Three cost arms, same bars**, because the point of the preceding measurement was that the + price you assume decides what you conclude: + - `measured` -- the 2026-09-02 figure: 0% commission (Alpaca's real rate) and the + per-ticker measured half-spread as one-way slippage. + - `keel_today` -- what keel charges this profile right now: 0% commission and + `slippage_for_quote_volume` on the cached (IEX-only) volume, ~7.5x the measured cost. + - `crypto_regime` -- the SAME equity bars priced at Coinbase's cost structure (1.2%/leg, + per-product slippage). Counterfactual by construction and labelled as such: it isolates + how much of the crypto null is the venue rather than the series. +* **The sell-side regulatory pass-throughs do not appear, and that is not an omission.** SEC + Section 31 and FINRA TAF are levied on SELLS; a DCA sleeve only ever buys. Its entire cost + is spread. +* **Pre-declared expectation**: the three arms differ by very little in the `measured` vs + `keel_today` comparison (a low-turnover sleeve pays the spread ~260 times over five years, + not thousands), and by a lot under `crypto_regime`. If the first pair DOES differ materially, + the cost model's equities error is not confined to strategy evaluation and reaches the + passive baseline too. +* **What would refute the benchmark's usefulness**: a sleeve whose terminal value is dominated + by one ticker, making "beat DCA" a statement about NVDA rather than about equities. Reported + per ticker for exactly that reason. + +**This is a benchmark, not a recommendation.** It says what accumulation did on five liquid +US large caps over one particular five-year window that contained a historic mega-cap run. It +is not evidence that accumulation works, is not advice to accumulate, and its window is short +enough and its universe narrow enough that the number is a yardstick, not a finding. + +Re-run: + KEEL_EQUITIES_DB=~/keel/keel-equities.db \ + python docs/experiments/2026-09-03-equities-dca-benchmark.py +""" + +from __future__ import annotations + +import json +import os +import sqlite3 +import statistics +from decimal import Decimal +from pathlib import Path + +from keel_core.types import Candle, Granularity + +from keel.research.spread import corwin_schultz_spread +from keel.strategy.backtest import TAKER_FEE_PCT, slippage_for_quote_volume +from keel.strategy.rules.dca import Dca + +DB = os.environ.get("KEEL_EQUITIES_DB") or str(Path.home() / "keel" / "keel-equities.db") +OUT_DIR = Path(os.environ.get("KEEL_EXPERIMENT_OUT") or Path(__file__).resolve().parent / "_out") +OUT_DIR.mkdir(parents=True, exist_ok=True) +JSONL_PATH = OUT_DIR / "equities_dca_benchmark.jsonl" + +TICKERS = ["MSFT-USD", "AAPL-USD", "GOOGL-USD", "NVDA-USD", "COST-USD"] + +#: WHY NOT `sim/portfolio_sim`, WHICH THE CRYPTO DCA MEASUREMENT USED. That harness iterates +#: ONE_HOUR bars (`_window_bars` is `history_days * 24`, mirroring the live agent's hourly +#: account pass), and the equities profile is ONE_DAY only -- by configuration, because Alpaca +#: mints hourly bars only inside a session and the daily turtle rules never read them. Handing +#: it daily bars labelled as hourly would produce numbers that look right and mean nothing. +#: +#: So the loop is here, and it is deliberately the smallest thing that can be faithful: walk +#: the daily series, ask the SHIPPED `Dca` rule (not a reimplementation of it) whether this bar +#: is a cadence hit, and fill any Setup at the NEXT bar's open plus one-way slippage -- keel's +#: own market-order convention since #258, and what `execution/executor.py` really places. +#: There is no exit path to model: DCA never sells, which is also why the sell-side regulatory +#: fees never enter. +_FILL_CONVENTION = "next bar open + one-way slippage, market order (#258)" + + +def load(product_id: str) -> list[Candle]: + con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True) + try: + rows = con.execute( + "SELECT ts, o, h, l, c, v FROM candles WHERE product_id=? AND granularity='ONE_DAY'" + " ORDER BY ts", + (product_id,), + ).fetchall() + finally: + con.close() + return [ + Candle(ts=r[0], open=Decimal(r[1]), high=Decimal(r[2]), low=Decimal(r[3]), + close=Decimal(r[4]), volume=Decimal(r[5])) + for r in rows + ] + + +def arm_costs(product_id: str, candles: list[Candle]) -> dict[str, tuple[Decimal, Decimal]]: + """`(fee_pct, slippage_pct)` per arm, per ticker. Slippage is a ONE-WAY number both here and + in `slippage_for_quote_volume`, so the measured HALF-spread -- the cost of crossing from the + mid to the touch on one leg -- is the like-for-like quantity.""" + est = corwin_schultz_spread(candles) + assert est is not None, product_id + adv = Decimal(str(statistics.median(float(c.volume * c.close) for c in candles))) + modelled = slippage_for_quote_volume(adv) + return { + "measured": (Decimal("0"), est.half_spread_pct), + "keel_today": (Decimal("0"), modelled), + "crypto_regime": (TAKER_FEE_PCT, modelled), + } + + +def run_arm(product_id: str, candles: list[Candle], fee: Decimal, slip: Decimal) -> dict: + """Accumulate the shipped `Dca` rule over the series and mark the sleeve at the last close. + + No cash constraint is imposed. The benchmark measures what the SCHEDULE accumulates; an + account that ran dry would be measuring the contribution rate instead, which is a different + question and not one a benchmark should silently answer. + """ + rule = Dca(product_id=product_id) + qty = Decimal("0") + deployed = Decimal("0") + fees_paid = Decimal("0") + buys = 0 + # `i` is the DECIDING bar; the fill lands on `i + 1`, so the last bar can never be a + # decision -- there is no next open to fill against. No lookahead anywhere. + for i in range(len(candles) - 1): + setup = rule.detect({Granularity.ONE_DAY: candles[: i + 1]}) + if setup is None: + continue + size_usd = setup.context["size_usd"] + fill_price = candles[i + 1].open * (Decimal("1") + slip) + if fill_price <= 0: + continue + fee_usd = size_usd * fee + qty += size_usd / fill_price + deployed += size_usd + fee_usd + fees_paid += fee_usd + buys += 1 + + last_close = candles[-1].close + market_value = qty * last_close + gain = ((market_value / deployed - 1) * 100) if deployed > 0 else Decimal("0") + return { + "buys": buys, + "qty": str(qty.quantize(Decimal("0.00000001"))), + "deployed": str(deployed.quantize(Decimal("0.01"))), + "fees_paid": str(fees_paid.quantize(Decimal("0.01"))), + "market_value": str(market_value.quantize(Decimal("0.01"))), + "gain_pct": str(gain.quantize(Decimal("0.01"))), + } + + +def main() -> None: + rows: list[dict] = [] + for product_id in TICKERS: + candles = load(product_id) + costs = arm_costs(product_id, candles) + for arm, (fee, slip) in costs.items(): + row = run_arm(product_id, candles, fee, slip) + row |= { + "product_id": product_id, + "arm": arm, + "bars": len(candles), + "fee_pct": str(fee), + "slippage_bp": str((slip * 10000).quantize(Decimal("0.01"))), + } + rows.append(row) + + with JSONL_PATH.open("w") as fh: + for row in rows: + fh.write(json.dumps(row) + "\n") + + for arm in ("measured", "keel_today", "crypto_regime"): + sub = [r for r in rows if r["arm"] == arm] + print(f"\n=== {arm} ===") + print(f"{'ticker':<12}{'buys':>6}{'slip bp':>9}{'deployed':>13}{'value':>13}{'gain %':>10}") + for r in sub: + print(f"{r['product_id']:<12}{r['buys']:>6}{r['slippage_bp']:>9}{r['deployed']:>13}" + f"{r['market_value']:>13}{r['gain_pct']:>10}") + dep = sum(Decimal(r["deployed"]) for r in sub) + val = sum(Decimal(r["market_value"]) for r in sub) + pooled = ((val / dep - 1) * 100) if dep > 0 else Decimal("0") + print(f"{'POOLED':<12}{'':>6}{'':>9}{dep.quantize(Decimal('0.01')):>13}" + f"{val.quantize(Decimal('0.01')):>13}{pooled.quantize(Decimal('0.01')):>10}") + print(f"\nwrote {JSONL_PATH}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_dca_benchmark_is_read_per_ticker.py b/tests/test_dca_benchmark_is_read_per_ticker.py new file mode 100644 index 0000000..a90d976 --- /dev/null +++ b/tests/test_dca_benchmark_is_read_per_ticker.py @@ -0,0 +1,83 @@ +"""A benchmark carried by one name is not a benchmark -- issue #371. + +The equities DCA run pre-declared its own refutation condition: "a sleeve whose terminal value +is dominated by one ticker, making 'beat DCA' a statement about NVDA rather than about +equities". It fired. NVDA is 44.8% of the terminal sleeve and its +449% drags the pooled figure +to +145.10% while the median ticker returned +70.62% and the four-name sleeve without it +returned +69.03%. + +So the number a future equity claim is measured against is the MEDIAN TICKER, not the pooled +sleeve, and the document has to say so where a reader will see it. Quoting +145% as "what DCA +did" would set a bar that one exceptional name built and then attribute clearing it -- or +failing to -- to a strategy. + +These pins are on the prose because that is where the damage would be done. The numbers +themselves live in the `.jsonl` beside the record and are reproducible from the driver. +""" + +from __future__ import annotations + +import json +import re +from decimal import Decimal +from pathlib import Path + +_ROOT = Path(__file__).resolve().parent.parent +_RECORD = _ROOT / "docs/experiments/2026-09-03-equities-dca-benchmark.md" +_DATA = _ROOT / "docs/experiments/2026-09-03-equities-dca-benchmark.jsonl" + + +def _measured() -> list[dict]: + rows = [json.loads(line) for line in _DATA.read_text().splitlines() if line.strip()] + return [r for r in rows if r["arm"] == "measured"] + + +def test_the_record_and_its_data_both_exist() -> None: + assert _RECORD.is_file(), "the benchmark record is missing" + assert _measured(), "the benchmark's measured arm produced no rows" + + +def test_one_ticker_really_does_dominate_the_sleeve() -> None: + """The premise of every other pin here. If a re-run ever makes the sleeve balanced, these + tests should fail loudly rather than keep enforcing a caveat that no longer applies.""" + rows = _measured() + values = {r["product_id"]: Decimal(r["market_value"]) for r in rows} + total = sum(values.values()) + top = max(values.values()) + assert top / total > Decimal("0.4"), f"no ticker dominates any more: {values}" + + +def test_the_headline_benchmark_is_the_median_ticker_not_the_pooled_sleeve() -> None: + """The pooled figure may appear -- suppressing it would be its own dishonesty -- but the + sentence naming what a strategy must beat has to name the median.""" + text = _RECORD.read_text(encoding="utf-8") + headline = re.search(r"(?im)^.*benchmark to beat.*$", text) + assert headline, "the record never says what the benchmark to beat is" + assert "median" in headline.group(0).lower(), headline.group(0) + + +def test_the_concentration_caveat_is_stated_with_its_number() -> None: + """In the OPENING, not just in the table. A reader who takes the headline and stops must + still have been told that one name carries it -- the table row alone lets the prose be + gutted while this pin stays green, which is exactly what a mutation run showed.""" + text = _RECORD.read_text(encoding="utf-8") + opening = text[: text.index("## What was measured")] + assert "44.8" in opening, "the concentration is not quantified before the first heading" + assert re.search(r"(?i)without NVDA|excluding NVDA|ex-NVDA", text), ( + "the record does not report the sleeve with the dominant name removed" + ) + + +def test_every_cost_arm_is_named_with_its_result() -> None: + """Three arms were declared before the run; a record that quietly drops one is a record + that chose its comparison after seeing it.""" + text = _RECORD.read_text(encoding="utf-8") + for arm in ("measured", "keel_today", "crypto_regime"): + assert f"`{arm}`" in text, f"cost arm {arm} is not named in the record" + + +def test_the_record_refuses_to_read_as_a_recommendation() -> None: + """DCA outperforming here is a property of a five-year window containing a historic + mega-cap run, not evidence that accumulation works.""" + text = _RECORD.read_text(encoding="utf-8").lower() + assert "not a recommendation" in text or "not advice" in text