From d5e148577e93de176fc0243aaf86512503fe1c48 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 26 Sep 2026 20:38:10 -0400 Subject: [PATCH 1/4] feat(strategy): an optional long-term SMA trend filter on turtle_breakout (#830) trend_filter in {off, above, slope, both} gates entries on a simple moving average of trend_sma_period closes (default 200): the close above it, the SMA rising over trend_slope_lookback bars (default 5), or both. Default off: every existing rule behaves exactly as before. Too little history to compute the SMA declines the entry (trend_filter_history) rather than passing it, so a filtered arm never trades a bar its filter did not see. Co-Authored-By: Claude Opus 5.5 --- keel/strategy/rules/turtle_breakout.py | 62 +++++++++++++ tests/strategy/test_turtle_breakout.py | 116 +++++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/keel/strategy/rules/turtle_breakout.py b/keel/strategy/rules/turtle_breakout.py index 914040dc..53650979 100644 --- a/keel/strategy/rules/turtle_breakout.py +++ b/keel/strategy/rules/turtle_breakout.py @@ -154,8 +154,19 @@ class TurtleBreakout(Rule): "nominal take-profit distance in R; the real exit is the channel-low -- it " "exists to clear the engine's rr>=1 kill-zone gate and let winners run." ), + "trend_filter": ( + "long-term trend gate on entries (default off): 'above' = close over its SMA, " + "'slope' = SMA rising, 'both'. Fewer entries against a falling market (#830)." + ), + "trend_sma_period": "SMA length (bars) for trend_filter; 200 = the classic 200-day.", + "trend_slope_lookback": ( + "bars back the SMA slope is measured over when trend_filter is slope/both." + ), } + #: `trend_filter`'s closed vocabulary. "off" is the default and filters nothing. + TREND_FILTERS: tuple[str, ...] = ("off", "above", "slope", "both") + # The declared parameter space (issue #528): what a sweep may legitimately explore on # this rule, stated HERE so the trials count is derived from the rule rather than # remembered by the operator. Ranges are the ones the #476 Optuna study pinned (and the @@ -199,6 +210,9 @@ def __init__( volume_ma_period: int = 20, # lookback for the average-volume comparison (days) volume_mult: float = 1.2, # breakout volume must exceed volume_mult x the average target_rr: Decimal = Decimal("6"), # distant nominal take-profit; see detect() + trend_filter: str = "off", # long-term SMA gate (#830): off | above | slope | both + trend_sma_period: int = 200, # the SMA's length, in bars of `granularity` + trend_slope_lookback: int = 5, # slope = SMA[t] - SMA[t - lookback] name: str = "turtle_breakout", ) -> None: if entry_lookback <= 0: @@ -211,6 +225,14 @@ def __init__( raise ValueError("atr_period must be positive") if volume_ma_period <= 0: raise ValueError("volume_ma_period must be positive") + if trend_filter not in self.TREND_FILTERS: + raise ValueError( + f"trend_filter must be one of {self.TREND_FILTERS}, not {trend_filter!r}" + ) + if trend_sma_period <= 0: + raise ValueError("trend_sma_period must be positive") + if trend_slope_lookback <= 0: + raise ValueError("trend_slope_lookback must be positive") self.name = name self.product_id = product_id @@ -229,6 +251,9 @@ def __init__( "volume_ma_period": volume_ma_period, "volume_mult": volume_mult, "target_rr": target_rr, + "trend_filter": trend_filter, + "trend_sma_period": trend_sma_period, + "trend_slope_lookback": trend_slope_lookback, } # memoizes the S1-filter decision by the completed-history's last ts, so the account # sim's repeated intraday calls on the same forming day don't re-replay (only ever @@ -322,6 +347,9 @@ def detect(self, candles_by_tf: dict[Granularity, list[Candle]]) -> Setup | None "adx", adx=adx_now, adx_threshold=self.params["adx_threshold"], **breakout ) + if self.params["trend_filter"] != "off" and not self._passes_trend_filter(daily, breakout): + return None + if self.params["use_macd_confirm"]: closes = [float(c.close) for c in work] histogram = macd(closes)[2] @@ -388,6 +416,40 @@ def detect(self, candles_by_tf: dict[Granularity, list[Candle]]) -> Setup | None ts=current.ts, ) + def _passes_trend_filter(self, daily: list[Candle], breakout: dict) -> bool: + """The long-term trend gate (#830): `True` lets the entry through; `False` has already + recorded why on `last_rejection`. + + A simple moving average of `trend_sma_period` closes, ending at the decision bar. + `above` needs the close over it; `slope` needs it higher than it was + `trend_slope_lookback` bars ago; `both` needs both. It runs after the ADX gate, so it + only costs anything on a bar that has already broken out in a trend. + + Too little history to compute it DECLINES (`trend_filter_history`) rather than passing: + a filtered rule must never take an entry its filter could not see, or an arm measured + "with the filter" would include trades made without it. + """ + period = self.params["trend_sma_period"] + lookback = self.params["trend_slope_lookback"] + mode = self.params["trend_filter"] + needed = period + (lookback if mode in ("slope", "both") else 0) + if len(daily) < needed: + self._decline("trend_filter_history", bars=len(daily), bars_needed=needed, **breakout) + return False + closes = [float(c.close) for c in daily[-needed:]] + sma_now = sum(closes[-period:]) / period + numbers: dict = {"sma": sma_now, "trend_filter": mode} + passes = True + if mode in ("above", "both"): + passes = passes and closes[-1] > sma_now + if mode in ("slope", "both"): + sma_before = sum(closes[-period - lookback : -lookback]) / period + numbers["sma_before"] = sma_before + passes = passes and sma_now > sma_before + if not passes: + self._decline("trend_filter", **numbers, **breakout) + return passes + def exit_signal(self, held: Setup, candles_by_tf: dict[Granularity, list[Candle]]) -> bool: """The asymmetric Turtle channel exit: a close at/below the prior exit-lookback Donchian low. diff --git a/tests/strategy/test_turtle_breakout.py b/tests/strategy/test_turtle_breakout.py index 5c92db55..436937c3 100644 --- a/tests/strategy/test_turtle_breakout.py +++ b/tests/strategy/test_turtle_breakout.py @@ -806,3 +806,119 @@ def test_low_volume_breakout_skipped(self) -> None: series = _with_breakout_volume(_breakout_candles(), breakout_vol=110.0) assert _rule(min_volume_filter=True).detect({Granularity.ONE_DAY: series}) is None assert _rule(min_volume_filter=False).detect({Granularity.ONE_DAY: series}) is not None + + +# -- the 200-day SMA trend filter (#830) ----------------------------------------------------------- +# +# Small periods so the fixtures stay readable: a 10-bar SMA and a 5-bar slope lookback stand in +# for the 200 / 5 the experiment runs. `adx_threshold=0` takes the ADX gate out of the way -- the +# filter runs after it, and these tests are about the filter alone. Every fixture asserts its own +# premise (above/below the SMA, rising/falling slope, a real breakout) so it cannot pass for the +# wrong reason. + +_TF = {"trend_sma_period": 10, "trend_slope_lookback": 5, "adx_threshold": 0.0} + + +def _closes_to_candles(closes: list[float]) -> list[Candle]: + return [_candle(i, c, c + 0.5, c - 0.5, c) for i, c in enumerate(closes)] + + +def _rising() -> list[float]: + return [100.0 + i for i in range(30)] + [140.0] + + +def _above_but_falling() -> list[float]: + return [300.0 - 6 * i for i in range(30)] + [128.0, 130.0, 132.0, 134.0, 136.0, 175.0] + + +def _below_but_rising() -> list[float]: + return [100.0 + 10 * i for i in range(25)] + [275.0 + 2 * i for i in range(5)] + [290.0] + + +def _sma(closes: list[float], period: int, back: int = 0) -> float: + end = len(closes) - back + return sum(closes[end - period : end]) / period + + +def _premise(closes: list[float], above: bool, rising: bool) -> None: + assert closes[-1] > max(closes[-6:-1]), "fixture is not a 5-bar Donchian breakout" + assert (closes[-1] > _sma(closes, 10)) is above + assert (_sma(closes, 10) > _sma(closes, 10, back=5)) is rising + + +def _fires(trend_filter: str, closes: list[float]) -> bool: + rule = _rule(trend_filter=trend_filter, **_TF) + return rule.detect({Granularity.ONE_DAY: _closes_to_candles(closes)}) is not None + + +class TestTrendFilter: + def test_off_by_default_and_unchanged(self) -> None: + rule = TurtleBreakout(product_id="BTC-USD") + assert rule.params["trend_filter"] == "off" + assert rule.params["trend_sma_period"] == 200 + assert rule.params["trend_slope_lookback"] == 5 + # below a falling SMA, and it still fires: "off" filters nothing. + _premise(_above_but_falling(), above=True, rising=False) + _premise(_below_but_rising(), above=False, rising=True) + assert _fires("off", _above_but_falling()) + assert _fires("off", _below_but_rising()) + + def test_above_requires_the_close_above_the_sma(self) -> None: + _premise(_rising(), above=True, rising=True) + assert _fires("above", _rising()) + assert _fires("above", _above_but_falling()) + assert not _fires("above", _below_but_rising()) + + def test_slope_requires_a_rising_sma(self) -> None: + assert _fires("slope", _rising()) + assert not _fires("slope", _above_but_falling()) + assert _fires("slope", _below_but_rising()) + + def test_both_requires_both(self) -> None: + assert _fires("both", _rising()) + assert not _fires("both", _above_but_falling()) + assert not _fires("both", _below_but_rising()) + + def test_a_filtered_bar_names_the_gate_and_the_numbers(self) -> None: + closes = _below_but_rising() + rule = _rule(trend_filter="above", **_TF) + assert rule.detect({Granularity.ONE_DAY: _closes_to_candles(closes)}) is None + assert rule.last_rejection is not None + assert rule.last_rejection["gate"] == "trend_filter" + assert rule.last_rejection["sma"] == _sma(closes, 10) + + def test_too_little_history_for_the_sma_declines_rather_than_passing(self) -> None: + """A filter that cannot be evaluated must not wave the entry through: that would make + the filtered arm trade bars the filter never saw.""" + closes = _rising()[-14:] # enough for the 5-bar channel, not for SMA10 + 5 back + rule = _rule(trend_filter="slope", **_TF) + assert rule.detect({Granularity.ONE_DAY: _closes_to_candles(closes)}) is None + assert rule.last_rejection is not None + assert rule.last_rejection["gate"] == "trend_filter_history" + + def test_an_unknown_mode_is_refused(self) -> None: + import pytest + + with pytest.raises(ValueError): + _rule(trend_filter="sometimes") + with pytest.raises(ValueError): + _rule(trend_filter="above", trend_sma_period=0) + with pytest.raises(ValueError): + _rule(trend_filter="slope", trend_slope_lookback=0) + + def test_a_stored_row_round_trips(self) -> None: + rule = agent.build_rule_from_params( + "turtle_breakout", + { + "product_id": "BTC-USD", + "trend_filter": "both", + "trend_sma_period": 200, + "trend_slope_lookback": 5, + }, + ) + assert isinstance(rule, TurtleBreakout) + assert ( + rule.params["trend_filter"], + rule.params["trend_sma_period"], + rule.params["trend_slope_lookback"], + ) == ("both", 200, 5) From 696bc5b5037f410808a7b714a14b92676ad80216 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 26 Sep 2026 20:39:46 -0400 Subject: [PATCH 2/4] docs(experiments): pre-register the 200-day SMA filter test on the daily turtle (#830) The driver's docstring is the pre-registration: four arms (off, above, slope, both) at 200/5, the daily paper rule set, the 2026-09-26 window, a common eligibility window for every arm, the cluster-bootstrap difference interval, the N>=100 floor, the decision rule, and the expectation -- committed before any arm has been run. Co-Authored-By: Claude Opus 5.5 --- .../2026-09-27-turtle-sma200-filter.py | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 docs/experiments/2026-09-27-turtle-sma200-filter.py diff --git a/docs/experiments/2026-09-27-turtle-sma200-filter.py b/docs/experiments/2026-09-27-turtle-sma200-filter.py new file mode 100644 index 00000000..e69c08ab --- /dev/null +++ b/docs/experiments/2026-09-27-turtle-sma200-filter.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python +"""Does a 200-day SMA trend filter improve the daily `turtle_breakout`? (#830): the driver. + +PRE-REGISTERED BEFORE RUNNING. This docstring is the pre-registration, committed before any +arm was run. The companion record, `docs/experiments/2026-09-27-turtle-sma200-filter.md`, +reports what came out. + +## The question +The daily Turtle already gates on ADX(14) > 25, a choppy-regime filter and a higher-timeframe +bias. Its 5-year baseline on v0.18.0 (#830's issue body) is 220 pooled trades, 25.0% wins, ++0.166 R per trade: about 3 win-rate points above break-even, against a detectable edge of +about 13 points. So it is not distinguishable from zero. The hypothesis: entries against a +falling long-term trend are disproportionately false breakouts, and gating on the 200-day SMA +raises pooled expectancy in R. + +## Arms (declared now, not tuned afterwards) +All four use `TurtleBreakout(trend_filter=..., trend_sma_period=200, trend_slope_lookback=5)`, +with every other parameter as persisted: +- `off`: no filter. This is the baseline. +- `above`: the decision bar's close is above its 200-day SMA. +- `slope`: the 200-day SMA is higher than 5 daily bars earlier. +- `both`: `above` AND `slope`. + +## Data and engine (the same code path as `keel simulate`'s edge pass) +- **Rules:** every `turtle_breakout` row with status `paper` and daily granularity in the + `--db` cache. For the run this record cites, that is a copy of `~/keel/keel.db` taken + 2026-09-26: the daily paper account's 19 rows. The one `candidate` row (AAVE) is excluded, + since the paper account does not trade it. +- **Window:** 5 × 365 days ending `now_ts = 1790455996` (2026-09-26 16:53 UTC), the window of + the 2026-09-26 hourly record, loaded by `commands.simulate.load_sim_candles`. +- **Costs:** 1.20% taker per leg plus per-product slippage from + `commands.simulate.slippage_assumptions` (#259). +- **Fills and R:** `sim.report.edge_table`: next-bar-open fills, and R from the achieved fill + against the original stop (#820). + +## One comparison window for every arm +A filtered arm cannot enter until a product has 200 + 5 completed daily bars, since the rule +declines rather than trading blind (`trend_filter_history`). The unfiltered arm could, and +those early trades would make the comparison unequal. So **every arm, including `off`, is +evaluated only on entries at or after each product's eligibility time**: the timestamp of +its 206th daily bar in the window. The unrestricted `off` result is reported beside it for +reference only. One known imperfection is stated here rather than discovered later: dropping +an early `off` trade after the fact cannot re-open an entry that trade's open position had +blocked. The effect is at most one trade per product near the boundary. + +## Statistics, per arm +Pooled N; win rate; expectancy_r; avg win / avg loss in R; profit factor in R; +break-even win rate `1 / (1 + avg_win_r / |avg_loss_r|)` and the edge over it; +`throughput.n_eff(N)` and `throughput.detectable_edge(n_eff)`, stated as ADR 0006's sentence. +Per-product rows are diagnostics only. + +**Difference from the baseline:** expectancy_r(arm) − expectancy_r(`off`, restricted), with a +95% percentile interval from a **cluster bootstrap by entry UTC day**. Trades are resampled by +the day they entered, because breakouts herd (#427). Each arm and the baseline are resampled +independently: 10,000 draws, seed 830. Independent resampling ignores the overlap between the +two samples, which makes the interval wider (conservative), never narrower. + +## Decision rule (pre-registered) +- An arm with **N < 100** pooled trades is reported and **not evaluated** (ADR 0006's floor). +- An arm is **"promising, to walk-forward"** only if N ≥ 100 **and** the lower bound of its + 95% difference interval is **> 0**. +- Otherwise the result is **"no improvement distinguishable from the baseline"**. + +Nothing here promotes, demotes or changes a rule. A "promising" arm's next step is +`keel research walk-forward` as a separate, separately recorded run. + +## Multiple testing +Three filtered arms are three trials. Each arm, including the baseline, is appended to the +trials ledger (`--ledger`) as `kind="ablation"`, `provenance="a_priori"` (the 200-day SMA is +the textbook choice, not fitted), `decision="diagnostic_only"`, with its per-trade R series. +The record states the number of arms beside any difference it reports. + +## Expectation, recorded before running +Most likely: every filter removes a large share of entries (a guess of 40–60% retained); +expectancy_r moves by less than its interval width; and the honest headline is "no +improvement distinguishable from the baseline". The `above` arm could fall under the +100-trade floor. A lower bound above zero would be a surprise worth inspecting, not a finding +to celebrate: three arms is three chances. + +## Provenance and safety +READ-ONLY against the candle cache (`mode=ro`). The only writes are `--out` (JSONL), the +`--ledger` append, and stdout. +""" + +from __future__ import annotations + +import argparse +import json +import random +import sqlite3 +from decimal import Decimal +from typing import Any + +from keel import agent +from keel.commands.fetch import DAYS_PER_YEAR +from keel.commands.simulate import SIM_SLIPPAGE_PCT, load_sim_candles, slippage_assumptions +from keel.data.repository import Repository +from keel.research import throughput +from keel.research.ledger import append_trial +from keel.sim import report as report_mod +from keel.strategy.rules.base import Rule, Trade +from keel.strategy.stats import summarize +from keel.types import Granularity + +NOW_TS = 1790455996 +FEE = Decimal("0.012") +ARMS = ("off", "above", "slope", "both") +SMA_PERIOD = 200 +SLOPE_LOOKBACK = 5 +FLOOR = 100 +BOOTSTRAP_DRAWS = 10_000 +SEED = 830 +SESSION = "turtle-sma200-filter-2026-09-27" + + +def _repo(db_path: str) -> Repository: + conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) + conn.row_factory = sqlite3.Row + return Repository(conn) + + +def _rules(repo: Repository, arm: str) -> list[Rule]: + rules = [] + for row in repo.get_rules(): + params = dict(row["params"] or {}) + if row["kind"] != "turtle_breakout" or row["status"] != "paper": + continue + if params.get("granularity", "ONE_DAY") != "ONE_DAY": + continue + params.update( + trend_filter=arm, trend_sma_period=SMA_PERIOD, trend_slope_lookback=SLOPE_LOOKBACK + ) + rule = agent.build_rule_from_params("turtle_breakout", params) + rule.rule_id = row["id"] + rules.append(rule) + return rules + + +def _eligible_ts(candles: dict, asset: str) -> int | None: + daily = candles.get(asset, {}).get(Granularity.ONE_DAY, []) + index = SMA_PERIOD + SLOPE_LOOKBACK + return daily[index].ts if len(daily) > index else None + + +def _stats(trades: list[Trade]) -> dict[str, Any]: + s = summarize(sorted(trades, key=lambda t: t.exit_ts or 0)) + out: dict[str, Any] = { + "n": s.n_trades, + "win_rate": round(s.win_rate, 4), + "expectancy_r": None if s.expectancy_r is None else str(round(s.expectancy_r, 4)), + "avg_win_r": None if s.avg_win_r is None else str(round(s.avg_win_r, 4)), + "avg_loss_r": None if s.avg_loss_r is None else str(round(s.avg_loss_r, 4)), + "profit_factor_r": None if s.profit_factor_r is None else str(round(s.profit_factor_r, 4)), + } + if s.n_trades and s.avg_win_r and s.avg_loss_r: + b = s.avg_win_r / abs(s.avg_loss_r) + breakeven = Decimal(1) / (Decimal(1) + b) + effective = throughput.n_eff(Decimal(s.n_trades)) + out["breakeven_win_rate"] = str(round(breakeven, 4)) + out["edge_over_breakeven"] = str(round(Decimal(str(s.win_rate)) - breakeven, 4)) + out["n_eff"] = str(round(effective, 1)) + out["detectable_edge"] = str(round(throughput.detectable_edge(effective), 4)) + return out + + +def _mean_r(trades: list[Trade]) -> float: + rs = [float(t.r_multiple) for t in trades if t.r_multiple is not None] + return sum(rs) / len(rs) if rs else 0.0 + + +def _by_day(trades: list[Trade]) -> list[list[Trade]]: + days: dict[int, list[Trade]] = {} + for t in trades: + days.setdefault(t.entry_ts // 86400, []).append(t) + return list(days.values()) + + +def _bootstrap_difference(arm: list[Trade], base: list[Trade]) -> tuple[float, float]: + rng = random.Random(SEED) + a_days, b_days = _by_day(arm), _by_day(base) + diffs = [] + for _ in range(BOOTSTRAP_DRAWS): + a = [t for day in rng.choices(a_days, k=len(a_days)) for t in day] + b = [t for day in rng.choices(b_days, k=len(b_days)) for t in day] + diffs.append(_mean_r(a) - _mean_r(b)) + diffs.sort() + return diffs[int(0.025 * BOOTSTRAP_DRAWS)], diffs[int(0.975 * BOOTSTRAP_DRAWS) - 1] + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--db", required=True, help="candle cache holding the daily paper rules") + parser.add_argument("--out", required=True) + parser.add_argument("--ledger", help="trials ledger to append to (omit to skip)") + args = parser.parse_args() + + repo = _repo(args.db) + start_ts = NOW_TS - 5 * DAYS_PER_YEAR * 86400 + base_rules = _rules(repo, "off") + products = sorted({rule.product_id for rule in base_rules}) + candles, _prices = load_sim_candles(repo, products, start_ts, NOW_TS) + _rows, resolve = slippage_assumptions(candles, products, products, SIM_SLIPPAGE_PCT) + eligible = {p.split("-")[0]: _eligible_ts(candles, p.split("-")[0]) for p in products} + + trades: dict[str, dict[str, list[Trade]]] = {} + for arm in ARMS: + rules = _rules(repo, arm) + table = report_mod.edge_table( + rules, candles, fee_pct=FEE, slippage_pct=SIM_SLIPPAGE_PCT, slippage_by_product=resolve + ) + trades[arm] = { + key: [t for t in result.trades if t.exit_ts is not None] + for key, result in table.items() + if key != report_mod.POOLED_KEY + } + + def restricted(arm: str) -> dict[str, list[Trade]]: + out = {} + for key, ts in trades[arm].items(): + cutoff = eligible.get(key.split(":", 1)[1]) + out[key] = [t for t in ts if cutoff is not None and t.entry_ts >= cutoff] + return out + + pooled = {arm: [t for ts in restricted(arm).values() for t in ts] for arm in ARMS} + unrestricted_off = [t for ts in trades["off"].values() for t in ts] + + with open(args.out, "w", encoding="utf-8") as fh: + fh.write( + json.dumps( + { + "row": "meta", + "session": SESSION, + "now_ts": NOW_TS, + "start_ts": start_ts, + "fee_pct": str(FEE), + "arms": list(ARMS), + "sma_period": SMA_PERIOD, + "slope_lookback": SLOPE_LOOKBACK, + "eligible_ts": eligible, + "rules": len(base_rules), + } + ) + + "\n" + ) + fh.write( + json.dumps({"row": "reference", "arm": "off_unrestricted", **_stats(unrestricted_off)}) + + "\n" + ) + for arm in ARMS: + row: dict[str, Any] = {"row": "arm", "arm": arm, **_stats(pooled[arm])} + row["evaluated"] = row["n"] >= FLOOR + if arm != "off": + low, high = _bootstrap_difference(pooled[arm], pooled["off"]) + row["diff_expectancy_r_ci95_low"] = round(low, 4) + row["diff_expectancy_r_ci95_high"] = round(high, 4) + row["verdict"] = ( + "not evaluated (N < 100)" + if not row["evaluated"] + else "promising, to walk-forward" + if low > 0 + else "no improvement distinguishable from the baseline" + ) + fh.write(json.dumps(row) + "\n") + for key, ts in sorted(restricted(arm).items()): + fh.write( + json.dumps({"row": "product", "arm": arm, "key": key, **_stats(ts)}) + "\n" + ) + if args.ledger: + series = [t.r_multiple for t in pooled[arm] if t.r_multiple is not None] + append_trial( + args.ledger, + trial_id=f"{SESSION}-{arm}", + session=SESSION, + rule="turtle_breakout", + params={ + "trend_filter": arm, + "trend_sma_period": SMA_PERIOD, + "trend_slope_lookback": SLOPE_LOOKBACK, + "units": "per_trade_pnl is R, not dollars", + }, + provenance="a_priori", + kind="ablation", + decision="diagnostic_only", + per_trade_pnl=series, + series_missing=not series, + summary={k: v for k, v in row.items() if k not in ("row",)}, + ) + print(f"wrote {args.out}") + + +if __name__ == "__main__": + main() From 37cc6acceee70d9d160fb3ca2d6f5051f8b7bbbe Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 26 Sep 2026 20:48:10 -0400 Subject: [PATCH 3/4] fix(research): the trials ledger refuses summary values it cannot read back Two write-side gaps in _validate_summary, found recording #830. A non-numeric string (a verdict, an arm name) is decoded as a Decimal by read_trials and raises, making the append-only ledger unreadable from that row on. A float is hashed as a JSON number but read back as a Decimal, so verify_chain reports the row as tampered forever. Both are now refused at append time, with a test each. Co-Authored-By: Claude Opus 5.5 --- keel/research/ledger.py | 18 ++++++++++- tests/research/test_ledger.py | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/keel/research/ledger.py b/keel/research/ledger.py index 05f79cc9..df142dbf 100644 --- a/keel/research/ledger.py +++ b/keel/research/ledger.py @@ -148,7 +148,11 @@ def compute_row_hash(record: TrialRecord) -> str: #: So the check is here, at the write, where it is a refusal rather than a catastrophe. It is also #: why #726's gauntlet artifacts are FLAT keys -- `final_p05`, `final_p50` -- rather than a nested #: quantile ladder: the shape a reader can survive is the shape a writer may use. -_SUMMARY_SCALARS = (Decimal, int, float, str, bool) +#: +#: NOT `float` (#830). A float is hashed as a JSON number on the write and read back as a +#: `Decimal`, so the row's own hash never verifies again and `verify_chain` reports it as tampered +#: forever. Store a `Decimal`, or its numeric string: both round-trip exactly. +_SUMMARY_SCALARS = (Decimal, int, str, bool) def _validate_summary(summary: Mapping[str, Any]) -> None: @@ -159,6 +163,18 @@ def _validate_summary(summary: Mapping[str, Any]) -> None: "or None. A nested value would make this append-only ledger unreadable on the " "next read_trials, permanently -- store a flat key per figure instead" ) + # `_decode_summary` reads every string back as `Decimal(value)`, so a word here (a + # verdict, an arm name) raises on READ and bricks the chain the same way a nested value + # would. Words belong in `params`, which is stored and read back verbatim. + if isinstance(value, str): + try: + Decimal(value) + except ArithmeticError: + raise ValueError( + f"summary[{key!r}] is the non-numeric string {value!r}; every summary string " + "is read back as a Decimal, so this row would make the ledger unreadable -- " + "put words in params instead" + ) from None def _validate(record: TrialRecord) -> None: diff --git a/tests/research/test_ledger.py b/tests/research/test_ledger.py index bd8c802a..1554bce5 100644 --- a/tests/research/test_ledger.py +++ b/tests/research/test_ledger.py @@ -291,3 +291,59 @@ def test_the_scalar_summary_values_the_gauntlet_writes_all_round_trip(tmp_path) assert stored.summary["dominance_1st"] is True assert stored.summary["trial_sharpe_variance"] is None assert ledger.verify_chain(path) == [] + + +def test_a_non_numeric_string_summary_value_is_refused_at_append_time(tmp_path) -> None: + """The same catastrophe through a different door. `_decode_summary` turns every string into + `Decimal(value)`, so a word -- a verdict, an arm name -- raises `InvalidOperation` on READ, + and one such row bricks the append-only chain forever. A numeric string is fine: it is how + a `Decimal` is stored.""" + path = tmp_path / "ledger.jsonl" + for value in ("off", "no improvement distinguishable from the baseline", ""): + with pytest.raises(ValueError, match="summary"): + ledger.append_trial( + path, + trial_id="t1", + session="s", + rule="r", + provenance="a_priori", + kind="ablation", + decision="diagnostic_only", + series_missing=True, + summary={"verdict": value}, + ) + assert not path.exists(), "a refused append must not have written a row" + + ledger.append_trial( + path, + trial_id="t1", + session="s", + rule="r", + provenance="a_priori", + kind="ablation", + decision="diagnostic_only", + series_missing=True, + summary={"expectancy_r": "0.2697"}, + ) + (stored,) = ledger.read_trials(path) + assert stored.summary["expectancy_r"] == Decimal("0.2697") + + +def test_a_float_summary_value_is_refused_at_append_time(tmp_path) -> None: + """A float is hashed as a JSON number when written but read back as a `Decimal`, so the + row's own hash never verifies again: `verify_chain` reports it as tampered, forever. A + `Decimal` (or its numeric string) round-trips exactly, which is what a summary figure is.""" + path = tmp_path / "ledger.jsonl" + with pytest.raises(ValueError, match="summary"): + ledger.append_trial( + path, + trial_id="t1", + session="s", + rule="r", + provenance="a_priori", + kind="ablation", + decision="diagnostic_only", + series_missing=True, + summary={"win_rate": 0.2673}, + ) + assert not path.exists(), "a refused append must not have written a row" From 8604e800bdfecf31b171834ed79c5b50f3d55c95 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Sat, 26 Sep 2026 20:48:10 -0400 Subject: [PATCH 4/4] docs(experiments): the 200-day SMA filter on the daily turtle -- no arm distinguishable from the baseline (#830) Pre-registered (696bc5b, before the run). On a common window, off: 202 trades +0.270 R; above 181 +0.218 R; slope and both 121 at ~+0.41 R, 95% difference intervals ~[-0.52, +0.83]. No lower bound above zero, so under the pre-registered rule nothing goes to walk-forward. Four ablation rows appended to the trials ledger (diagnostic_only, a priori, R series). Co-Authored-By: Claude Opus 5.5 --- .../2026-09-27-turtle-sma200-filter.jsonl | 82 ++++++++++++++ .../2026-09-27-turtle-sma200-filter.md | 104 ++++++++++++++++++ .../2026-09-27-turtle-sma200-filter.py | 11 +- docs/experiments/README.md | 7 ++ docs/experiments/trials-ledger.jsonl | 4 + 5 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 docs/experiments/2026-09-27-turtle-sma200-filter.jsonl create mode 100644 docs/experiments/2026-09-27-turtle-sma200-filter.md diff --git a/docs/experiments/2026-09-27-turtle-sma200-filter.jsonl b/docs/experiments/2026-09-27-turtle-sma200-filter.jsonl new file mode 100644 index 00000000..fb0d896b --- /dev/null +++ b/docs/experiments/2026-09-27-turtle-sma200-filter.jsonl @@ -0,0 +1,82 @@ +{"row": "meta", "session": "turtle-sma200-filter-2026-09-27", "now_ts": 1790455996, "start_ts": 1632775996, "fee_pct": "0.012", "arms": ["off", "above", "slope", "both"], "sma_period": 200, "slope_lookback": 5, "eligible_ts": {"ADA": 1650499200, "ALGO": 1650499200, "AVAX": 1650672000, "BCH": 1650499200, "BTC": 1650499200, "CRV": 1650499200, "DOGE": 1650499200, "DOT": 1650499200, "ETH": 1650499200, "FET": 1650499200, "ICP": 1650499200, "LINK": 1650499200, "LTC": 1650499200, "NEAR": 1679702400, "PAXG": 1764374400, "SOL": 1650499200, "UNI": 1650499200, "XLM": 1650499200, "ZEC": 1650499200}, "rules": 19} +{"row": "reference", "arm": "off_unrestricted", "n": 220, "win_rate": "0.2500", "expectancy_r": "0.1658", "avg_win_r": "4.1352", "avg_loss_r": "-1.1573", "profit_factor_r": "1.1910", "breakeven_win_rate": "0.2187", "edge_over_breakeven": "0.0313", "n_eff": "85.4", "detectable_edge": "0.1345"} +{"row": "arm", "arm": "off", "n": 202, "win_rate": "0.2673", "expectancy_r": "0.2697", "avg_win_r": "4.1553", "avg_loss_r": "-1.1480", "profit_factor_r": "1.3206", "breakeven_win_rate": "0.2165", "edge_over_breakeven": "0.0509", "n_eff": "78.4", "detectable_edge": "0.1404", "evaluated": true} +{"row": "product", "arm": "off", "key": "turtle_breakout:ADA", "n": 8, "win_rate": "0.3750", "expectancy_r": "1.3571", "avg_win_r": "5.4262", "avg_loss_r": "-1.0843", "profit_factor_r": "3.0026", "breakeven_win_rate": "0.1665", "edge_over_breakeven": "0.2085", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "off", "key": "turtle_breakout:ALGO", "n": 11, "win_rate": "0.1818", "expectancy_r": "-0.0523", "avg_win_r": "5.2185", "avg_loss_r": "-1.2237", "profit_factor_r": "0.9477", "breakeven_win_rate": "0.1899", "edge_over_breakeven": "-0.0081", "n_eff": "4.3", "detectable_edge": "0.6017"} +{"row": "product", "arm": "off", "key": "turtle_breakout:AVAX", "n": 7, "win_rate": "0.4286", "expectancy_r": "0.8979", "avg_win_r": "3.7166", "avg_loss_r": "-1.2162", "profit_factor_r": "2.2920", "breakeven_win_rate": "0.2466", "edge_over_breakeven": "0.1820", "n_eff": "2.7", "detectable_edge": "0.7542"} +{"row": "product", "arm": "off", "key": "turtle_breakout:BCH", "n": 10, "win_rate": "0.3000", "expectancy_r": "-0.0138", "avg_win_r": "2.4539", "avg_loss_r": "-1.0714", "profit_factor_r": "0.9816", "breakeven_win_rate": "0.3039", "edge_over_breakeven": "-0.0039", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "off", "key": "turtle_breakout:BTC", "n": 11, "win_rate": "0.4545", "expectancy_r": "0.6532", "avg_win_r": "2.8935", "avg_loss_r": "-1.2136", "profit_factor_r": "1.9868", "breakeven_win_rate": "0.2955", "edge_over_breakeven": "0.1591", "n_eff": "4.3", "detectable_edge": "0.6017"} +{"row": "product", "arm": "off", "key": "turtle_breakout:CRV", "n": 14, "win_rate": "0.1429", "expectancy_r": "-0.5425", "avg_win_r": "3.2108", "avg_loss_r": "-1.1681", "profit_factor_r": "0.4581", "breakeven_win_rate": "0.2668", "edge_over_breakeven": "-0.1239", "n_eff": "5.4", "detectable_edge": "0.5333"} +{"row": "product", "arm": "off", "key": "turtle_breakout:DOGE", "n": 12, "win_rate": "0.2500", "expectancy_r": "0.4501", "avg_win_r": "5.4399", "avg_loss_r": "-1.2132", "profit_factor_r": "1.4946", "breakeven_win_rate": "0.1824", "edge_over_breakeven": "0.0676", "n_eff": "4.7", "detectable_edge": "0.5761"} +{"row": "product", "arm": "off", "key": "turtle_breakout:DOT", "n": 10, "win_rate": "0.2000", "expectancy_r": "0.0580", "avg_win_r": "5.3096", "avg_loss_r": "-1.2550", "profit_factor_r": "1.0577", "breakeven_win_rate": "0.1912", "edge_over_breakeven": "0.0088", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "off", "key": "turtle_breakout:ETH", "n": 12, "win_rate": "0.3333", "expectancy_r": "0.6614", "avg_win_r": "4.2922", "avg_loss_r": "-1.1541", "profit_factor_r": "1.8596", "breakeven_win_rate": "0.2119", "edge_over_breakeven": "0.1214", "n_eff": "4.7", "detectable_edge": "0.5761"} +{"row": "product", "arm": "off", "key": "turtle_breakout:FET", "n": 14, "win_rate": "0.3571", "expectancy_r": "0.5694", "avg_win_r": "3.4873", "avg_loss_r": "-1.0517", "profit_factor_r": "1.8422", "breakeven_win_rate": "0.2317", "edge_over_breakeven": "0.1254", "n_eff": "5.4", "detectable_edge": "0.5333"} +{"row": "product", "arm": "off", "key": "turtle_breakout:ICP", "n": 9, "win_rate": "0.2222", "expectancy_r": "0.3374", "avg_win_r": "5.2310", "avg_loss_r": "-1.0608", "profit_factor_r": "1.4089", "breakeven_win_rate": "0.1686", "edge_over_breakeven": "0.0536", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "off", "key": "turtle_breakout:LINK", "n": 13, "win_rate": "0.3077", "expectancy_r": "0.0936", "avg_win_r": "2.8539", "avg_loss_r": "-1.1331", "profit_factor_r": "1.1193", "breakeven_win_rate": "0.2842", "edge_over_breakeven": "0.0235", "n_eff": "5.0", "detectable_edge": "0.5535"} +{"row": "product", "arm": "off", "key": "turtle_breakout:LTC", "n": 5, "win_rate": "0.0000", "expectancy_r": "-1.0136", "avg_win_r": "0.0000", "avg_loss_r": "-1.0136", "profit_factor_r": "0.0000"} +{"row": "product", "arm": "off", "key": "turtle_breakout:NEAR", "n": 10, "win_rate": "0.4000", "expectancy_r": "1.4598", "avg_win_r": "5.2290", "avg_loss_r": "-1.0530", "profit_factor_r": "3.3106", "breakeven_win_rate": "0.1676", "edge_over_breakeven": "0.2324", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "off", "key": "turtle_breakout:PAXG", "n": 2, "win_rate": "0.5000", "expectancy_r": "0.8274", "avg_win_r": "3.2641", "avg_loss_r": "-1.6093", "profit_factor_r": "2.0283", "breakeven_win_rate": "0.3302", "edge_over_breakeven": "0.1698", "n_eff": "0.8", "detectable_edge": "1.4111"} +{"row": "product", "arm": "off", "key": "turtle_breakout:SOL", "n": 12, "win_rate": "0.2500", "expectancy_r": "0.1509", "avg_win_r": "4.2195", "avg_loss_r": "-1.2054", "profit_factor_r": "1.1669", "breakeven_win_rate": "0.2222", "edge_over_breakeven": "0.0278", "n_eff": "4.7", "detectable_edge": "0.5761"} +{"row": "product", "arm": "off", "key": "turtle_breakout:UNI", "n": 18, "win_rate": "0.1111", "expectancy_r": "-0.4444", "avg_win_r": "5.3660", "avg_loss_r": "-1.1707", "profit_factor_r": "0.5729", "breakeven_win_rate": "0.1791", "edge_over_breakeven": "-0.0680", "n_eff": "7.0", "detectable_edge": "0.4704"} +{"row": "product", "arm": "off", "key": "turtle_breakout:XLM", "n": 11, "win_rate": "0.2727", "expectancy_r": "0.5974", "avg_win_r": "5.4192", "avg_loss_r": "-1.2108", "profit_factor_r": "1.6785", "breakeven_win_rate": "0.1826", "edge_over_breakeven": "0.0901", "n_eff": "4.3", "detectable_edge": "0.6017"} +{"row": "product", "arm": "off", "key": "turtle_breakout:ZEC", "n": 13, "win_rate": "0.2308", "expectancy_r": "0.0575", "avg_win_r": "3.6730", "avg_loss_r": "-1.0272", "profit_factor_r": "1.0727", "breakeven_win_rate": "0.2185", "edge_over_breakeven": "0.0122", "n_eff": "5.0", "detectable_edge": "0.5535"} +{"row": "arm", "arm": "above", "n": 181, "win_rate": "0.2541", "expectancy_r": "0.2184", "avg_win_r": "4.2681", "avg_loss_r": "-1.1615", "profit_factor_r": "1.2520", "breakeven_win_rate": "0.2139", "edge_over_breakeven": "0.0402", "n_eff": "70.3", "detectable_edge": "0.1483", "evaluated": true, "diff_expectancy_r_ci95_low": "-0.6410", "diff_expectancy_r_ci95_high": "0.5397", "verdict": "no improvement distinguishable from the baseline"} +{"row": "product", "arm": "above", "key": "turtle_breakout:ADA", "n": 8, "win_rate": "0.3750", "expectancy_r": "0.6759", "avg_win_r": "3.8321", "avg_loss_r": "-1.2178", "profit_factor_r": "1.8881", "breakeven_win_rate": "0.2412", "edge_over_breakeven": "0.1338", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "above", "key": "turtle_breakout:ALGO", "n": 10, "win_rate": "0.2000", "expectancy_r": "0.0679", "avg_win_r": "5.2721", "avg_loss_r": "-1.2331", "profit_factor_r": "1.0688", "breakeven_win_rate": "0.1896", "edge_over_breakeven": "0.0104", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "above", "key": "turtle_breakout:AVAX", "n": 6, "win_rate": "0.3333", "expectancy_r": "1.0347", "avg_win_r": "5.4964", "avg_loss_r": "-1.1961", "profit_factor_r": "2.2976", "breakeven_win_rate": "0.1787", "edge_over_breakeven": "0.1546", "n_eff": "2.3", "detectable_edge": "0.8147"} +{"row": "product", "arm": "above", "key": "turtle_breakout:BCH", "n": 9, "win_rate": "0.3333", "expectancy_r": "0.1153", "avg_win_r": "2.4539", "avg_loss_r": "-1.0540", "profit_factor_r": "1.1641", "breakeven_win_rate": "0.3005", "edge_over_breakeven": "0.0329", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "above", "key": "turtle_breakout:BTC", "n": 9, "win_rate": "0.5556", "expectancy_r": "1.1128", "avg_win_r": "2.8935", "avg_loss_r": "-1.1131", "profit_factor_r": "3.2493", "breakeven_win_rate": "0.2778", "edge_over_breakeven": "0.2777", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "above", "key": "turtle_breakout:CRV", "n": 10, "win_rate": "0.1000", "expectancy_r": "-0.4780", "avg_win_r": "5.4706", "avg_loss_r": "-1.1390", "profit_factor_r": "0.5337", "breakeven_win_rate": "0.1723", "edge_over_breakeven": "-0.0723", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "above", "key": "turtle_breakout:DOGE", "n": 13, "win_rate": "0.1538", "expectancy_r": "-0.1911", "avg_win_r": "5.5529", "avg_loss_r": "-1.2354", "profit_factor_r": "0.8172", "breakeven_win_rate": "0.1820", "edge_over_breakeven": "-0.0281", "n_eff": "5.0", "detectable_edge": "0.5535"} +{"row": "product", "arm": "above", "key": "turtle_breakout:DOT", "n": 9, "win_rate": "0.2222", "expectancy_r": "0.2259", "avg_win_r": "5.3626", "avg_loss_r": "-1.2417", "profit_factor_r": "1.2339", "breakeven_win_rate": "0.1880", "edge_over_breakeven": "0.0342", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "above", "key": "turtle_breakout:ETH", "n": 9, "win_rate": "0.4444", "expectancy_r": "1.2664", "avg_win_r": "4.2922", "avg_loss_r": "-1.1543", "profit_factor_r": "2.9748", "breakeven_win_rate": "0.2119", "edge_over_breakeven": "0.2325", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "above", "key": "turtle_breakout:FET", "n": 10, "win_rate": "0.4000", "expectancy_r": "1.0541", "avg_win_r": "4.2601", "avg_loss_r": "-1.0831", "profit_factor_r": "2.6220", "breakeven_win_rate": "0.2027", "edge_over_breakeven": "0.1973", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "above", "key": "turtle_breakout:ICP", "n": 11, "win_rate": "0.0909", "expectancy_r": "-0.5965", "avg_win_r": "5.3727", "avg_loss_r": "-1.1934", "profit_factor_r": "0.4502", "breakeven_win_rate": "0.1818", "edge_over_breakeven": "-0.0908", "n_eff": "4.3", "detectable_edge": "0.6017"} +{"row": "product", "arm": "above", "key": "turtle_breakout:LINK", "n": 12, "win_rate": "0.3333", "expectancy_r": "0.2000", "avg_win_r": "2.8539", "avg_loss_r": "-1.1269", "profit_factor_r": "1.2663", "breakeven_win_rate": "0.2831", "edge_over_breakeven": "0.0502", "n_eff": "4.7", "detectable_edge": "0.5761"} +{"row": "product", "arm": "above", "key": "turtle_breakout:LTC", "n": 5, "win_rate": "0.0000", "expectancy_r": "-1.0136", "avg_win_r": "0.0000", "avg_loss_r": "-1.0136", "profit_factor_r": "0.0000"} +{"row": "product", "arm": "above", "key": "turtle_breakout:NEAR", "n": 9, "win_rate": "0.3333", "expectancy_r": "1.0624", "avg_win_r": "5.3286", "avg_loss_r": "-1.0706", "profit_factor_r": "2.4885", "breakeven_win_rate": "0.1673", "edge_over_breakeven": "0.1660", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "above", "key": "turtle_breakout:PAXG", "n": 3, "win_rate": "0.3333", "expectancy_r": "-0.0293", "avg_win_r": "3.2641", "avg_loss_r": "-1.6760", "profit_factor_r": "0.9738", "breakeven_win_rate": "0.3393", "edge_over_breakeven": "-0.0059", "n_eff": "1.2", "detectable_edge": "1.1521"} +{"row": "product", "arm": "above", "key": "turtle_breakout:SOL", "n": 11, "win_rate": "0.1818", "expectancy_r": "0.0366", "avg_win_r": "5.6256", "avg_loss_r": "-1.2054", "profit_factor_r": "1.0371", "breakeven_win_rate": "0.1765", "edge_over_breakeven": "0.0054", "n_eff": "4.3", "detectable_edge": "0.6017"} +{"row": "product", "arm": "above", "key": "turtle_breakout:UNI", "n": 15, "win_rate": "0.1333", "expectancy_r": "-0.3306", "avg_win_r": "5.3660", "avg_loss_r": "-1.2070", "profit_factor_r": "0.6839", "breakeven_win_rate": "0.1836", "edge_over_breakeven": "-0.0503", "n_eff": "5.8", "detectable_edge": "0.5152"} +{"row": "product", "arm": "above", "key": "turtle_breakout:XLM", "n": 9, "win_rate": "0.2222", "expectancy_r": "0.4116", "avg_win_r": "5.4588", "avg_loss_r": "-1.0304", "profit_factor_r": "1.5136", "breakeven_win_rate": "0.1588", "edge_over_breakeven": "0.0634", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "product", "arm": "above", "key": "turtle_breakout:ZEC", "n": 13, "win_rate": "0.2308", "expectancy_r": "0.0057", "avg_win_r": "3.6730", "avg_loss_r": "-1.0945", "profit_factor_r": "1.0067", "breakeven_win_rate": "0.2296", "edge_over_breakeven": "0.0012", "n_eff": "5.0", "detectable_edge": "0.5535"} +{"row": "arm", "arm": "slope", "n": 121, "win_rate": "0.2975", "expectancy_r": "0.4191", "avg_win_r": "4.0755", "avg_loss_r": "-1.1294", "profit_factor_r": "1.5283", "breakeven_win_rate": "0.2170", "edge_over_breakeven": "0.0805", "n_eff": "47.0", "detectable_edge": "0.1814", "evaluated": true, "diff_expectancy_r_ci95_low": "-0.5151", "diff_expectancy_r_ci95_high": "0.8289", "verdict": "no improvement distinguishable from the baseline"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:ADA", "n": 4, "win_rate": "0.5000", "expectancy_r": "2.1520", "avg_win_r": "5.4918", "avg_loss_r": "-1.1878", "profit_factor_r": "4.6236", "breakeven_win_rate": "0.1778", "edge_over_breakeven": "0.3222", "n_eff": "1.6", "detectable_edge": "0.9978"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:ALGO", "n": 5, "win_rate": "0.2000", "expectancy_r": "0.1122", "avg_win_r": "5.3868", "avg_loss_r": "-1.2064", "profit_factor_r": "1.1162", "breakeven_win_rate": "0.1830", "edge_over_breakeven": "0.0170", "n_eff": "1.9", "detectable_edge": "0.8924"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:AVAX", "n": 4, "win_rate": "0.2500", "expectancy_r": "0.4914", "avg_win_r": "5.5794", "avg_loss_r": "-1.2045", "profit_factor_r": "1.5440", "breakeven_win_rate": "0.1776", "edge_over_breakeven": "0.0724", "n_eff": "1.6", "detectable_edge": "0.9978"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:BCH", "n": 8, "win_rate": "0.2500", "expectancy_r": "0.0792", "avg_win_r": "3.4332", "avg_loss_r": "-1.0388", "profit_factor_r": "1.1016", "breakeven_win_rate": "0.2323", "edge_over_breakeven": "0.0177", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:BTC", "n": 8, "win_rate": "0.5000", "expectancy_r": "1.1072", "avg_win_r": "3.2938", "avg_loss_r": "-1.0793", "profit_factor_r": "3.0518", "breakeven_win_rate": "0.2468", "edge_over_breakeven": "0.2532", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:CRV", "n": 7, "win_rate": "0.2857", "expectancy_r": "-0.0053", "avg_win_r": "2.7820", "avg_loss_r": "-1.1202", "profit_factor_r": "0.9934", "breakeven_win_rate": "0.2871", "edge_over_breakeven": "-0.0014", "n_eff": "2.7", "detectable_edge": "0.7542"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:DOGE", "n": 6, "win_rate": "0.3333", "expectancy_r": "1.0477", "avg_win_r": "5.5529", "avg_loss_r": "-1.2049", "profit_factor_r": "2.3043", "breakeven_win_rate": "0.1783", "edge_over_breakeven": "0.1550", "n_eff": "2.3", "detectable_edge": "0.8147"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:DOT", "n": 3, "win_rate": "0.0000", "expectancy_r": "-0.8765", "avg_win_r": "0.0000", "avg_loss_r": "-0.8765", "profit_factor_r": "0.0000"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:ETH", "n": 8, "win_rate": "0.3750", "expectancy_r": "0.1625", "avg_win_r": "2.1335", "avg_loss_r": "-1.0201", "profit_factor_r": "1.2548", "breakeven_win_rate": "0.3235", "edge_over_breakeven": "0.0515", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:FET", "n": 7, "win_rate": "0.5714", "expectancy_r": "1.2505", "avg_win_r": "3.0734", "avg_loss_r": "-1.1800", "profit_factor_r": "3.4729", "breakeven_win_rate": "0.2774", "edge_over_breakeven": "0.2940", "n_eff": "2.7", "detectable_edge": "0.7542"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:ICP", "n": 5, "win_rate": "0.2000", "expectancy_r": "0.1251", "avg_win_r": "5.3564", "avg_loss_r": "-1.1828", "profit_factor_r": "1.1322", "breakeven_win_rate": "0.1809", "edge_over_breakeven": "0.0191", "n_eff": "1.9", "detectable_edge": "0.8924"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:LINK", "n": 10, "win_rate": "0.3000", "expectancy_r": "0.2635", "avg_win_r": "3.7964", "avg_loss_r": "-1.2506", "profit_factor_r": "1.3010", "breakeven_win_rate": "0.2478", "edge_over_breakeven": "0.0522", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:LTC", "n": 4, "win_rate": "0.0000", "expectancy_r": "-0.9558", "avg_win_r": "0.0000", "avg_loss_r": "-0.9558", "profit_factor_r": "0.0000"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:NEAR", "n": 5, "win_rate": "0.4000", "expectancy_r": "1.5968", "avg_win_r": "5.3564", "avg_loss_r": "-0.9096", "profit_factor_r": "3.9258", "breakeven_win_rate": "0.1452", "edge_over_breakeven": "0.2548", "n_eff": "1.9", "detectable_edge": "0.8924"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:PAXG", "n": 2, "win_rate": "0.5000", "expectancy_r": "0.8274", "avg_win_r": "3.2641", "avg_loss_r": "-1.6093", "profit_factor_r": "2.0283", "breakeven_win_rate": "0.3302", "edge_over_breakeven": "0.1698", "n_eff": "0.8", "detectable_edge": "1.4111"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:SOL", "n": 10, "win_rate": "0.2000", "expectancy_r": "0.1660", "avg_win_r": "5.6256", "avg_loss_r": "-1.1989", "profit_factor_r": "1.1730", "breakeven_win_rate": "0.1757", "edge_over_breakeven": "0.0243", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:UNI", "n": 10, "win_rate": "0.1000", "expectancy_r": "-0.5448", "avg_win_r": "5.4515", "avg_loss_r": "-1.2110", "profit_factor_r": "0.5002", "breakeven_win_rate": "0.1818", "edge_over_breakeven": "-0.0818", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:XLM", "n": 6, "win_rate": "0.3333", "expectancy_r": "0.9778", "avg_win_r": "5.4588", "avg_loss_r": "-1.2627", "profit_factor_r": "2.1615", "breakeven_win_rate": "0.1879", "edge_over_breakeven": "0.1455", "n_eff": "2.3", "detectable_edge": "0.8147"} +{"row": "product", "arm": "slope", "key": "turtle_breakout:ZEC", "n": 9, "win_rate": "0.3333", "expectancy_r": "0.5848", "avg_win_r": "3.6730", "avg_loss_r": "-0.9593", "profit_factor_r": "1.9143", "breakeven_win_rate": "0.2071", "edge_over_breakeven": "0.1262", "n_eff": "3.5", "detectable_edge": "0.6652"} +{"row": "arm", "arm": "both", "n": 121, "win_rate": "0.2893", "expectancy_r": "0.4094", "avg_win_r": "4.1883", "avg_loss_r": "-1.1285", "profit_factor_r": "1.5105", "breakeven_win_rate": "0.2122", "edge_over_breakeven": "0.0770", "n_eff": "47.0", "detectable_edge": "0.1814", "evaluated": true, "diff_expectancy_r_ci95_low": "-0.5241", "diff_expectancy_r_ci95_high": "0.8236", "verdict": "no improvement distinguishable from the baseline"} +{"row": "product", "arm": "both", "key": "turtle_breakout:ADA", "n": 4, "win_rate": "0.5000", "expectancy_r": "2.1520", "avg_win_r": "5.4918", "avg_loss_r": "-1.1878", "profit_factor_r": "4.6236", "breakeven_win_rate": "0.1778", "edge_over_breakeven": "0.3222", "n_eff": "1.6", "detectable_edge": "0.9978"} +{"row": "product", "arm": "both", "key": "turtle_breakout:ALGO", "n": 5, "win_rate": "0.2000", "expectancy_r": "0.1122", "avg_win_r": "5.3868", "avg_loss_r": "-1.2064", "profit_factor_r": "1.1162", "breakeven_win_rate": "0.1830", "edge_over_breakeven": "0.0170", "n_eff": "1.9", "detectable_edge": "0.8924"} +{"row": "product", "arm": "both", "key": "turtle_breakout:AVAX", "n": 4, "win_rate": "0.2500", "expectancy_r": "0.4914", "avg_win_r": "5.5794", "avg_loss_r": "-1.2045", "profit_factor_r": "1.5440", "breakeven_win_rate": "0.1776", "edge_over_breakeven": "0.0724", "n_eff": "1.6", "detectable_edge": "0.9978"} +{"row": "product", "arm": "both", "key": "turtle_breakout:BCH", "n": 8, "win_rate": "0.2500", "expectancy_r": "0.0792", "avg_win_r": "3.4332", "avg_loss_r": "-1.0388", "profit_factor_r": "1.1016", "breakeven_win_rate": "0.2323", "edge_over_breakeven": "0.0177", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "both", "key": "turtle_breakout:BTC", "n": 8, "win_rate": "0.5000", "expectancy_r": "1.1072", "avg_win_r": "3.2938", "avg_loss_r": "-1.0793", "profit_factor_r": "3.0518", "breakeven_win_rate": "0.2468", "edge_over_breakeven": "0.2532", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "both", "key": "turtle_breakout:CRV", "n": 7, "win_rate": "0.1429", "expectancy_r": "-0.1731", "avg_win_r": "5.4371", "avg_loss_r": "-1.1081", "profit_factor_r": "0.8178", "breakeven_win_rate": "0.1693", "edge_over_breakeven": "-0.0264", "n_eff": "2.7", "detectable_edge": "0.7542"} +{"row": "product", "arm": "both", "key": "turtle_breakout:DOGE", "n": 6, "win_rate": "0.3333", "expectancy_r": "1.0477", "avg_win_r": "5.5529", "avg_loss_r": "-1.2049", "profit_factor_r": "2.3043", "breakeven_win_rate": "0.1783", "edge_over_breakeven": "0.1550", "n_eff": "2.3", "detectable_edge": "0.8147"} +{"row": "product", "arm": "both", "key": "turtle_breakout:DOT", "n": 3, "win_rate": "0.0000", "expectancy_r": "-0.8765", "avg_win_r": "0.0000", "avg_loss_r": "-0.8765", "profit_factor_r": "0.0000"} +{"row": "product", "arm": "both", "key": "turtle_breakout:ETH", "n": 8, "win_rate": "0.3750", "expectancy_r": "0.1625", "avg_win_r": "2.1335", "avg_loss_r": "-1.0201", "profit_factor_r": "1.2548", "breakeven_win_rate": "0.3235", "edge_over_breakeven": "0.0515", "n_eff": "3.1", "detectable_edge": "0.7055"} +{"row": "product", "arm": "both", "key": "turtle_breakout:FET", "n": 7, "win_rate": "0.5714", "expectancy_r": "1.2505", "avg_win_r": "3.0734", "avg_loss_r": "-1.1800", "profit_factor_r": "3.4729", "breakeven_win_rate": "0.2774", "edge_over_breakeven": "0.2940", "n_eff": "2.7", "detectable_edge": "0.7542"} +{"row": "product", "arm": "both", "key": "turtle_breakout:ICP", "n": 5, "win_rate": "0.2000", "expectancy_r": "0.1251", "avg_win_r": "5.3564", "avg_loss_r": "-1.1828", "profit_factor_r": "1.1322", "breakeven_win_rate": "0.1809", "edge_over_breakeven": "0.0191", "n_eff": "1.9", "detectable_edge": "0.8924"} +{"row": "product", "arm": "both", "key": "turtle_breakout:LINK", "n": 10, "win_rate": "0.3000", "expectancy_r": "0.2635", "avg_win_r": "3.7964", "avg_loss_r": "-1.2506", "profit_factor_r": "1.3010", "breakeven_win_rate": "0.2478", "edge_over_breakeven": "0.0522", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "both", "key": "turtle_breakout:LTC", "n": 4, "win_rate": "0.0000", "expectancy_r": "-0.9558", "avg_win_r": "0.0000", "avg_loss_r": "-0.9558", "profit_factor_r": "0.0000"} +{"row": "product", "arm": "both", "key": "turtle_breakout:NEAR", "n": 5, "win_rate": "0.4000", "expectancy_r": "1.5968", "avg_win_r": "5.3564", "avg_loss_r": "-0.9096", "profit_factor_r": "3.9258", "breakeven_win_rate": "0.1452", "edge_over_breakeven": "0.2548", "n_eff": "1.9", "detectable_edge": "0.8924"} +{"row": "product", "arm": "both", "key": "turtle_breakout:PAXG", "n": 2, "win_rate": "0.5000", "expectancy_r": "0.8274", "avg_win_r": "3.2641", "avg_loss_r": "-1.6093", "profit_factor_r": "2.0283", "breakeven_win_rate": "0.3302", "edge_over_breakeven": "0.1698", "n_eff": "0.8", "detectable_edge": "1.4111"} +{"row": "product", "arm": "both", "key": "turtle_breakout:SOL", "n": 10, "win_rate": "0.2000", "expectancy_r": "0.1660", "avg_win_r": "5.6256", "avg_loss_r": "-1.1989", "profit_factor_r": "1.1730", "breakeven_win_rate": "0.1757", "edge_over_breakeven": "0.0243", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "both", "key": "turtle_breakout:UNI", "n": 10, "win_rate": "0.1000", "expectancy_r": "-0.5448", "avg_win_r": "5.4515", "avg_loss_r": "-1.2110", "profit_factor_r": "0.5002", "breakeven_win_rate": "0.1818", "edge_over_breakeven": "-0.0818", "n_eff": "3.9", "detectable_edge": "0.6310"} +{"row": "product", "arm": "both", "key": "turtle_breakout:XLM", "n": 6, "win_rate": "0.3333", "expectancy_r": "0.9778", "avg_win_r": "5.4588", "avg_loss_r": "-1.2627", "profit_factor_r": "2.1615", "breakeven_win_rate": "0.1879", "edge_over_breakeven": "0.1455", "n_eff": "2.3", "detectable_edge": "0.8147"} +{"row": "product", "arm": "both", "key": "turtle_breakout:ZEC", "n": 9, "win_rate": "0.3333", "expectancy_r": "0.5848", "avg_win_r": "3.6730", "avg_loss_r": "-0.9593", "profit_factor_r": "1.9143", "breakeven_win_rate": "0.2071", "edge_over_breakeven": "0.1262", "n_eff": "3.5", "detectable_edge": "0.6652"} diff --git a/docs/experiments/2026-09-27-turtle-sma200-filter.md b/docs/experiments/2026-09-27-turtle-sma200-filter.md new file mode 100644 index 00000000..bc7ea4ff --- /dev/null +++ b/docs/experiments/2026-09-27-turtle-sma200-filter.md @@ -0,0 +1,104 @@ +# A 200-day SMA filter on the daily `turtle_breakout`: no arm distinguishable from the baseline + +**Date:** 2026-09-27 +**Issue:** #830. It is evaluated under ADR 0006 (pooled sample, n ≥ 100, descriptive at that floor). +**Pre-registration:** the driver's docstring, +[`2026-09-27-turtle-sma200-filter.py`](2026-09-27-turtle-sma200-filter.py), committed and pushed +as `696bc5b` (2026-09-27 00:39 UTC) **before any arm was run**. The arms, window, statistics +and decision rule below are that docstring's. The only later edits to the driver changed how +figures are stored in the trials ledger (§6), never what was computed. +**Change:** `turtle_breakout` gains an optional `trend_filter` (`off` | `above` | `slope` | +`both`, default `off`), so no existing rule changes behaviour. No rule, config or status is +changed by this record. +**Data:** [`2026-09-27-turtle-sma200-filter.jsonl`](2026-09-27-turtle-sma200-filter.jsonl), +with per-arm and per-product rows. +**Ledger:** four `ablation` rows, `provenance=a_priori`, `decision=diagnostic_only`, session +`turtle-sma200-filter-2026-09-27`, each carrying its per-trade R series. + +## 1. Result + +The rules are the daily paper account's 19 `turtle_breakout` rows, over the window +2021-09-27 → 2026-09-26, at 1.20% taker plus per-product slippage, in R. Every arm is +evaluated on the **same** window: entries at or after each product's 206th daily bar, the +first bar a 200-day SMA with a 5-bar slope can be computed on. + +| Arm | Pooled N | Retained | Win % | E[R] | PF (R) | Edge over break-even | n_eff | Detectable edge | Δ E[R] vs `off`, 95% CI | Verdict | +|---|---|---|---|---|---|---|---|---|---|---| +| `off` (baseline) | 202 | — | 26.7% | +0.270 | 1.32 | +5.1 pts | 78.4 | 14.0 pts | — | — | +| `above` | 181 | 90% | 25.4% | +0.218 | 1.25 | +4.0 pts | 70.3 | 14.8 pts | [−0.641, +0.540] | no improvement distinguishable | +| `slope` | 121 | 60% | 29.8% | +0.419 | 1.53 | +8.1 pts | 47.0 | 18.1 pts | [−0.515, +0.829] | no improvement distinguishable | +| `both` | 121 | 60% | 28.9% | +0.409 | 1.51 | +7.7 pts | 47.0 | 18.1 pts | [−0.524, +0.824] | no improvement distinguishable | + +For reference only: the unrestricted `off` arm, including the first ~205 days, is **220 trades, +25.0%, +0.166 R**. That reproduces the baseline in #830's issue body. + +**Pre-registered verdict: no arm is promising.** Every arm clears the 100-trade floor, and no +arm's difference interval excludes zero. Nothing goes to walk-forward, and nothing is proposed +for any rule. + +## 2. What the numbers say + +- **`slope` and `both` point the hypothesised way.** They cut 40% of entries and move the + point estimate from +0.27 R to about +0.41 R, with the win rate up about 3 points. + 15 of 19 products are positive against 14 for `off`. But the 95% interval on the difference + is about 1.3 R wide and centred near +0.15 R. A difference this size is well inside what + resampling produces by chance. Removing 81 trades also costs power: the detectable edge goes + from 14 to 18 points. +- **`above` does worse than doing nothing.** It keeps 90% of entries and lowers the point + estimate. The likely reason, which this run did not test: a Donchian breakout usually closes + above its 200-day SMA anyway, so the gate rarely binds, and where it did bind it removed + winners about as often as losers. +- **The first ~205 days mattered more than any filter.** Restricting `off` to the common + window raised its expectancy from +0.166 R to +0.270 R. The 18 excluded trades + (2021-09 → 2022-04, the onset of the 2022 bear market) summed to about −18 R, an + average near −1.0 R. That + supports the hypothesis's intuition, that entries in a falling market hurt, more than any + arm here does. It is also one early episode of about 18 trades, not a measurement. +- **The baseline itself is not an edge.** +5.1 win-rate points over break-even against a + 14-point detectable edge is ADR 0006's descriptive regime. This record compares arms; it + does not establish that any of them, including `off`, makes money. + +## 3. Prediction versus outcome + +The pre-registration expected 40–60% of entries retained, expectancy moving by less than its +interval width, and "no improvement distinguishable from the baseline". The verdict matched. +Retention came out at 90% for `above` (the prediction was wrong there: price above its +200-day SMA was the normal state at breakouts) and 60% for `slope`/`both`. + +## 4. Multiple testing + +Three filtered arms were tested against one baseline. With no arm clearing an uncorrected 95% +interval, no correction changes anything. The four ledger rows are counted as trials, so any +future re-test of a trend filter on this rule family starts from M = 3 already spent. + +## 5. Pricing + +Every figure here is priced per product, as +[the per-product restatement](2026-09-01-per-product-slippage-restatement.md) established, with +the taker fee of 1.20% per leg. The flat-5 bp records this one sits beside are optimistic by +the amounts the restatement measured, and their **verdicts are unaffected**; this record's +verdict is its own and needs no correction. + +## 6. Two ledger defects found while recording this, fixed in the same change + +The first run's four ledger rows were never committed. `keel.research.ledger` accepted two +kinds of summary value that it cannot read back: + +1. **A non-numeric string** (the arm name, the verdict). `read_trials` decodes every summary + string as a `Decimal`, so it raised `InvalidOperation`, and the append-only ledger would + have been unreadable from that row on. +2. **A float** (the win rate, the interval bounds). A float is hashed as a JSON number when + written but read back as a `Decimal`, so the row's own hash never verifies: + `verify_chain` reports it as tampered, permanently. + +Both are now refused when a row is appended (`_validate_summary`), with a test each. The +driver stores words in `params` and figures as exact decimal strings. The run is +deterministic (seed 830), and the committed dataset's figures equal the first run's to the +last digit. + +## 7. What follows + +Under the pre-registered rule, nothing. If trend filtering is re-opened, the honest next +questions are about sample size, not a finer filter. A detectable edge of 14–18 points at +n = 121–202 means no filter on this rule family can be told apart from the baseline without +roughly doubling the pooled sample. That means more history, more products, or forward trades. diff --git a/docs/experiments/2026-09-27-turtle-sma200-filter.py b/docs/experiments/2026-09-27-turtle-sma200-filter.py index e69c08ab..2c39fb3b 100644 --- a/docs/experiments/2026-09-27-turtle-sma200-filter.py +++ b/docs/experiments/2026-09-27-turtle-sma200-filter.py @@ -146,7 +146,7 @@ def _stats(trades: list[Trade]) -> dict[str, Any]: s = summarize(sorted(trades, key=lambda t: t.exit_ts or 0)) out: dict[str, Any] = { "n": s.n_trades, - "win_rate": round(s.win_rate, 4), + "win_rate": str(round(Decimal(str(s.win_rate)), 4)), "expectancy_r": None if s.expectancy_r is None else str(round(s.expectancy_r, 4)), "avg_win_r": None if s.avg_win_r is None else str(round(s.avg_win_r, 4)), "avg_loss_r": None if s.avg_loss_r is None else str(round(s.avg_loss_r, 4)), @@ -251,8 +251,8 @@ def restricted(arm: str) -> dict[str, list[Trade]]: row["evaluated"] = row["n"] >= FLOOR if arm != "off": low, high = _bootstrap_difference(pooled[arm], pooled["off"]) - row["diff_expectancy_r_ci95_low"] = round(low, 4) - row["diff_expectancy_r_ci95_high"] = round(high, 4) + row["diff_expectancy_r_ci95_low"] = str(round(Decimal(str(low)), 4)) + row["diff_expectancy_r_ci95_high"] = str(round(Decimal(str(high)), 4)) row["verdict"] = ( "not evaluated (N < 100)" if not row["evaluated"] @@ -277,13 +277,16 @@ def restricted(arm: str) -> dict[str, list[Trade]]: "trend_sma_period": SMA_PERIOD, "trend_slope_lookback": SLOPE_LOOKBACK, "units": "per_trade_pnl is R, not dollars", + "verdict": row.get("verdict", "baseline"), }, provenance="a_priori", kind="ablation", decision="diagnostic_only", per_trade_pnl=series, series_missing=not series, - summary={k: v for k, v in row.items() if k not in ("row",)}, + # Numbers only: `read_trials` decodes every summary string as a Decimal, so + # the arm name and the verdict ride in `params`. + summary={k: v for k, v in row.items() if k not in ("row", "arm", "verdict")}, ) print(f"wrote {args.out}") diff --git a/docs/experiments/README.md b/docs/experiments/README.md index 9abb8f84..a1dc555e 100644 --- a/docs/experiments/README.md +++ b/docs/experiments/README.md @@ -25,6 +25,13 @@ Index is **newest first**, by the date each document carries in its filename. ## 2026-09 +- [`2026-09-27-turtle-sma200-filter.md`](2026-09-27-turtle-sma200-filter.md) — A 200-day SMA + trend filter on the daily `turtle_breakout`, **pre-registered** (driver committed before + the run): `above`, `slope` and `both` arms against the unfiltered baseline on a common + window. **No arm is distinguishable from the baseline.** `slope`/`both` lift the point + estimate from +0.27 R to about +0.41 R on 60% of the entries, inside a 95% interval about + 1.3 R wide; `above` does slightly worse. Also records two trials-ledger write-side defects, + fixed in the same change. Driver `2026-09-27-turtle-sma200-filter.py`, data `.jsonl`. - [`2026-09-26-hourly-turtle-5year-backtest.md`](2026-09-26-hourly-turtle-5year-backtest.md) — The hourly `turtle_breakout` over five years, in R at per-product slippage: **4,871 trades, 235–289 per product, 0 of 19 positive** at 1.20% or 0.60% taker, and only BTC, ETH and SOL diff --git a/docs/experiments/trials-ledger.jsonl b/docs/experiments/trials-ledger.jsonl index d8297023..ae57f78d 100644 --- a/docs/experiments/trials-ledger.jsonl +++ b/docs/experiments/trials-ledger.jsonl @@ -92,3 +92,7 @@ {"decision":"rejected","kind":"ablation","params":{"changed_nothing":"A document, a driver and a ledger row. No rule row added, nothing promoted, no config or allowlist touched. triple_barrier is registered and untraded.","control":"cusum_event, measured the same day on the SAME universe and window (cusum-event-first-measurement-2026-09-01). Both rules share an entry -- the same CUSUM filter at the same threshold -- so the exit is the only thing that changed and the difference is attributable. This is an A/B, not another level reading.","declared_before_the_run":"PRIMARY METRIC: the DELTA in profit factor against the control at the taker rate, NOT the level. The level was already a known null and asking 'does it clear 1.0' invites reading a 0.4 as encouraging. The question is how much a better exit moves a rule whose entry has no gross edge. Arm A is ONE configuration (no argmax); arm B sweeps max_holding_bars over {6,12,24,48,72} and its best is a max of 5 draws.","document":"docs/experiments/2026-09-01-triple-barrier-first-measurement.md","issue":"#342","levels":"Zero of 24 clear PF 1.0 at the taker rate (median 0.338, max 0.527) and zero at the 0.6% maker rate. n>=100 on 20 of 24, median 431 -- the vertical barrier closes positions the signal exit let run, so trade count falls against the control's 553 while staying clear of the floor.","recommended_next":"(a) NOT a barrier sweep: the horizontals are already friction-sized and the vertical is monotone across a twelvefold range for 0.30 of profit factor, all of it below 1.0. (b) The source is fully tested and fully answered -- there is no third half. (c) The null grows: 0 of 90 -> 0 of 114 -> 0 of 138.","script":"docs/experiments/2026-09-01-triple-barrier-first-measurement.py","selection_bias":"Arm A is one pre-declared configuration and carries none. Arm B's per-asset best is a maximum of five draws; its best cell overall is the n=17 TON-USD artefact above, and quoting it as anything else would be the error this field exists for.","the_answer":"THE EXIT GENUINELY WORKS AND IS WORTH NOTHING. At ZERO fee it lifts the median profit factor ACROSS BREAK-EVEN, 0.925 -> 1.001, improving 17 of 24 assets: a real, measurable gross improvement from a better exit, and the first number in this series to move the right way. At the 1.2% taker rate the median delta is -0.004 and the sign is a coin flip (11 of 24). The gross gain is smaller than the friction it has to be harvested through.","the_two_cells_above_one":"TON-USD at 48 bars (PF 1.270, n=17) and 72 bars (PF 1.063, n=16) -- a sixth of the admission floor, maxima of five draws on the thinnest asset in the universe. Not evidence of anything. The intersection of n>=100 and PF>1.0 is EMPTY across all 192 trials.","unit_trap_recorded":"`median_daily_quote_volume` returns a PER-BAR median despite its name, and `slippage_for_quote_volume` is anchored on $500M DAILY -- so feeding it the hourly figure unscaled reports every asset as maximally thin, clamps the universe to the 183.8bp cap and makes every barrier four times too wide, silently and with no error. `per_product_round_trip` scales by bars-per-day; a test asserts the consequence rather than the call.","validation":"Screening result only. No walk-forward, no out-of-sample split, no CSCV/PBO and no deflated Sharpe (series_missing). Same cached candles and ~5-year window as every other document here. slippage_pct held at 0.0005 in every cell, so 'zero fee' is zero FEE and not zero cost.","vertical_barrier_axis":"Monotone: 6/12/24/48/72 bars give median PF 0.161/0.243/0.338/0.392/0.464 with median n 520/486/431/374/338. Holding longer is better and the source's own 24-bar barrier is mid-range, not optimal -- the direction agrees with the paper's 'wide barriers beat next-bar labeling'. The magnitude does not, because 1.2% per leg is twelve times the 0.1% that paper priced.","what_this_settles_about_the_source":"Both halves of Gradzki et al. are now implemented on keel's cost structure and measured on one universe: the ENTRY half has essentially no gross edge (median 0.925 at zero cost) and the EXIT half is a real improvement (+0.033 gross, 17 of 24) that friction consumes entirely. The paper is not wrong about its own venue -- at 0.1% per leg a +0.033 gross improvement is worth keeping; at 2.5% it is not. This is the clearest measurement in the series of the difference between a result and a result AT A PRICE."},"per_bar_pnl":[],"per_trade_pnl":[],"prev_hash":"ef473b236321c2f081054fb2012860a0e34f5f195a02518eb807a88978c8b6af","provenance":"a_priori","row_hash":"c8ad6ec7e0e5eb524bd89038b841301ce7c6214707cec5609537658f2a952993","rule":"triple_barrier","series_missing":true,"session":"triple-barrier-first-measurement-2026-09-01","summary":{"arm_a_assets":24,"arm_a_n_above_floor":20,"arm_a_n_median":431,"arm_a_pf_above_one_taker":0,"arm_a_pf_above_one_zero_fee":13,"arm_a_pf_max_taker":"0.527","arm_a_pf_median_taker":"0.338","arm_a_pf_median_zero_fee":"1.001","delta_improved_taker":11,"delta_improved_zero_fee":17,"delta_vs_control_median_taker":"-0.004","delta_vs_control_median_zero_fee":"0.033","fee_pct":"0.012","gate_passed":0,"intersection_n100_and_pf1":0,"min_trades_floor":100,"n_trials":192,"pbo_available":0,"slippage_pct":"0.0005"},"timestamp":1788306964,"trial_id":"triple-barrier-first-measurement-2026-09-01"} {"decision":"diagnostic_only","kind":"ablation","params":{"changed_nothing_about_what_trades":"No rule row added, nothing promoted, no config or allowlist touched.","document":"docs/experiments/2026-09-01-per-product-slippage-restatement.md","issue":"#335 (split from #259)","not_restated":"The restated intersection, the fee curve and the hourly turtle sweep still carry flat-priced figures. Their VERDICTS are unaffected -- the correction only pushes them further from 1.0 -- but their LEVELS are optimistic by roughly the margin measured here, and a reader comparing across documents should know it. Re-running them would move no verdict and is not free.","script":"docs/experiments/2026-09-01-per-product-slippage-restatement.py","shipped_with_this":"`keel rules backtest` / `rules promote` now price per product via `rules.backtest_slippage`, from the product's cached ONE_DAY bars by the same one definition simulate.slippage_assumptions uses. No daily bars falls back to the flat floor and is FLAGGED as a fallback, never presented as a measured verdict. Both call sites (`_backtest_rule` and `backtest_resolved`) are pinned by tests: a mutation removing slippage_pct from either left every helper test green.","the_comparison_that_matters":"A median overstatement of 0.090 against the +0.033 of gross profit factor the triple barrier's better exit bought (2026-09-01-triple-barrier-first-measurement). THE ERROR IN THE COST MODEL WAS 2.7x LARGER THAN THE BEST GENUINE IMPROVEMENT ANY RULE CHANGE PRODUCED. Every strategy comparison in this repository has been made through a lens that mis-priced execution by more than the differences compared.","the_finding":"Every experiment document in this repository prices fills at slippage_pct=0.0005 -- the FLOOR of slippage_for_quote_volume, which the model reaches only at its $500M/day anchor. Measured over the 24-asset universe's own cached candles, NOT ONE ASSET REACHES IT: 1.1x the floor (BTC, 5.5bp) to 36.8x (TON, the 183.8bp cap), median near 10x, ten assets above 10x and four above 20x. #335 names the 'STX/CRO-class 1.15-1.30x floor entries' as the live example of a thin-asset candidate; the live example is the entire universe.","the_positive_cell_dies":"turtle_breakout on WLD-USD: 1.061 flat -> 0.626 per-product, at n=58 (already below the 100 floor) and 1.209% slippage, 24.2x the rate it was priced at. It was the only cell above 1.0 in 120. Zero of 120 clear PF 1.0 per-product.","unit_trap":"median_daily_quote_volume returns a PER-BAR median despite its name. Read off an hourly series and handed to a model anchored on a DAILY volume it reports every asset as maximally thin. The gate avoids it by reading ONE_DAY bars as simulate already did; triple_barrier.per_product_round_trip cannot (a pure rule has only the candles handed to it) and scales explicitly. Both say so where they do it.","validation":"Screening result only: no walk-forward, no out-of-sample split, no CSCV/PBO (series_missing). Same cached candles and ~5-year window as every other document. Fee held at the 1.2% taker rate in every cell, so slippage is the only variable. No configuration, no argmax, no free parameters -- nothing here could be selected on.","what_it_costs":"Five rules at shipped defaults x 24 assets x 2 regimes, run in ONE driver so the A/B is internally consistent. Median PF across 120 cells falls 0.309 -> 0.219. Per rule: turtle_breakout 0.336->0.267, rsi_meanrev 0.261->0.175, pullback_continuation 0.042->0.012, cusum_event 0.343->0.243, triple_barrier 0.338->0.237. Every one of the 120 deltas is negative or zero.","why_the_deferral_was_safe_and_is_not":"#259 deferred on the reasoning that the correction is CONSERVATIVE-ONLY: real cost is higher, a corrected profit factor can only fall, and per-product pricing can never manufacture an edge. Confirmed -- all 120 deltas are <= 0. What it does not survive is the magnitude: a correction assumed to be a rounding adjustment is worth 0.090 of median PF and kills the corpus's only positive cell. A gate pricing PROMOTION decisions at the best rate the model can produce is not conservative."},"per_bar_pnl":[],"per_trade_pnl":[],"prev_hash":"c8ad6ec7e0e5eb524bd89038b841301ce7c6214707cec5609537658f2a952993","provenance":"a_priori","row_hash":"a6b9dbc9ed6bba23f0c2c2280ad7859958eb0f0108bc9ae0bd06af54268a509c","rule":"all_shipped","series_missing":true,"session":"per-product-slippage-restatement-2026-09-01","summary":{"assets":24,"assets_at_the_floor":0,"cells":120,"cells_above_one_flat":1,"cells_above_one_per_product":0,"fee_pct":"0.012","flat_slippage_pct":"0.0005","floor_multiple_max":"36.8","floor_multiple_min":"1.1","gate_passed":0,"n_trials":240,"pbo_available":0,"pf_median_delta":"-0.090","pf_median_flat":"0.309","pf_median_per_product":"0.219","rules":5},"timestamp":1788311146,"trial_id":"per-product-slippage-restatement-2026-09-01"} {"decision":"diagnostic_only","kind":"ablation","params":{"and_per_product_pricing_deletes_the_episode":"ZEC-USD is the fourth-thinnest name in the universe: 107.3bp per leg, 21.5x the floor, on $1.08M/day. Priced at its own liquidity, turtle ZEC gross falls to 0.889 -- a loss before any fee. The maker crossing was an artifact of charging 5bp to cross a book that costs 107.","arm_c_checks_reproduce":"08-13's declared trigger (fewer than 8 of 24 assets reaching n>=100 at oversold=40) did not fire: 22 of 24, the same figure 08-13 measured. Monotonicity of trade count in oversold: 0 of 24 non-monotonic, matching 08-13 sec.4.1 on a corpus 565 bars longer. Pooled across the axis, 83 of 120 per-product cells reach n>=100, median PF 0.1208, 0 of 83 clear 1.0.","break_even_re_derived":"docs/research/2026-08-20-quant-lab-note-cross-verification.md sec.5 prices its inside-allowance row at psi=5bp -- the floor 0 of 24 assets reach. Re-derived at the measured median psi=52.3bp with the note's own p_be=(1+k)/(1+b), k=2(phi+psi)/s, s=2.40%, b=6: INSIDE the allowance k=0.436 and p_be=20.52% against a reconstructed 14.9% win rate -- 5.62 POINTS UNDERWATER, not the +0.02 the note reports. Outside, k=1.436 and p_be=34.80% (-19.90 pts). 'Indistinguishable from break-even inside the fee-free allowance' does not survive its own cost model. Rail 14 is still worth 14.3 points of break-even -- more than any rule change measured in this directory -- but it is the boundary between decisively and clearly negative, not between negative and break-even.","changed_nothing_about_what_trades":"No rule row added, nothing promoted, no config, allowlist or shipped parameter touched. 08-13 keeps its numbers; records here are appended to, never revised (#247).","corrections_of_record_against_08_13":"(1) sec.6's scope claim 'every signal rule the codebase ships has been measured' is FALSE -- three were measured, five ship; this document makes it true again. (2) sec.3's 'all seven die at the maker rate' was an artifact of the flat floor: six of those seven are Arm A cells re-priced here and FIVE OF THE SIX are dead at ZERO fee (XRP-USD excepted at 1.109); the seventh is an Arm B cell this run priced only at the taker rate (turtle ZEC 0.889, FET 0.865, PAXG-USDT 0.186, CRV 0.626; pullback ZEC 0.438), and so is the WLD/TON observation (WLD taker 1.061 flat -> 0.626 per-product at 120.9bp; TON 0.774 -> 0.317 at the 183.8bp cap). (3) sec.3.2's 0.034 transfer gap re-measures at 0.1172 flat / 0.1362 per-product -- 'stably unprofitable' holds, 'not overfit' is weaker than stated. (4) NEITHER #442 NOR #523 MOVED ANY NUMBER, recorded because the honest expectation was that they would.","document":"docs/experiments/2026-09-05-restatement-restated.md","engine":"main; every path exercised byte-identical to v0.13.3 (strategy/backtest.py, exit_policy.py, stats.py, all five rule modules, compliance.screen.median_daily_quote_volume)","independent_reproduction_of_09_01":"Arm A re-derives the per-product restatement's headline from scratch four days later on a longer corpus: median PF across cells 0.3120 flat -> 0.2192 per-product, delta -0.0928 against 09-01's -0.090, with every per-family figure within 0.005 of its 09-01 counterpart.","restates":"docs/experiments/2026-08-13-restated-under-a-production-faithful-engine.md","scripts":"docs/experiments/2026-09-05-restatement-restated.py (960 trials), docs/experiments/2026-09-05-restatement-restated-control.py (the corpus control)","slippage_measured":"0 of 24 assets reach the 5bp floor. Median 10.5x (52.3bp). Dearest TON-USD 183.8bp/36.8x on $0.28M/day (the cap); cheapest BTC-USD 5.5bp/1.1x on $419.73M/day. Thirteen assets above 10x, four above 20x.","the_deletion_test_converse":"08-13 sec.2 argued ZEC's edge was tail-carried and evidenced it by DELETING three trades. This run ran it the other way on real data: ADDING three trades moved pullback_continuation on ZEC-USD from gross 1.025 to 1.695, and six moved turtle_breakout on ZEC from 1.411 to 1.700 -- across the maker rate at 1.139, which 08-13 states none of its seven gross-positive cells survived. A profit factor a fortnight of ordinary data moves by +0.67 on 174 trades is measuring three trades, not an edge.","the_finding":"THE ENGINE NEVER MOVED THE 08-13 NUMBERS; 565 NEW CANDLES DID. Run over the truncated 08-13 corpus, today's engine reproduces three of the six cells 08-13 printed BIT-IDENTICALLY (turtle XRP-USD 157/1.223, turtle PAXG-USDT 238/1.145, turtle FET-USD 269/1.206), plus rsi_meanrev's 24-asset anchor exactly (median gross 1.1251 at median n=42). #442's gap-through-stop exit fill and its ratchet policy, and #523's re-derived cap, are inert for these rules: turtle carries a static stop and neither rule declares trail_atr_mult/be_roll_rr.","the_one_cell_above_one":"Arm B turtle_breakout on ZEC-USD, per-product taker, PF 1.0275 at n=96 -- FOUR TRADES BELOW the 100-trade admission floor, on the asset 08-13 already established is regime-bound (92.7% of lifetime PnL in 2025-26) and tail-carried, priced at 21.5x the floor. Reported because it exists, not because it means anything.","the_verdict":"ZERO of 240 configurations clear n>=100 AND profit_factor>1.0 at the 1.2% taker rate priced per product; zero at the 0.6% maker rate. Arm A 0/120 (5 shipped signal families x 24 assets, shipped defaults), Arm B 0/24 (the sweep-winner transfer check), Arm C 0/96 (rsi_meanrev's oversold axis). The 08-13 null holds over a population 2.7x larger, a strictly more expensive cost model, and a corpus 565 bars longer.","validation":"Screening result only: no walk-forward, no PBO/CSCV (series_missing), no out-of-sample split beyond Arm B's fixed 6/18. No argmax in Arm A -- one pre-declared configuration per rule, 120 cells, zero free parameters. Arm C is a disclosed sweep whose per-asset best is a maximum of five draws; no cell in it approaches 1.0. The slippage model is an assumption, not a measurement (#626: the curve prices a $26k-$165k clip against the deployment's $50). The 14.9% win rate is a reconstruction inherited from the 08-20 note."},"per_bar_pnl":[],"per_trade_pnl":[],"prev_hash":"a6b9dbc9ed6bba23f0c2c2280ad7859958eb0f0108bc9ae0bd06af54268a509c","provenance":"a_priori","row_hash":"7a31e85d57acb94f69b28c88c69d4c027155f4cccf252a909b90227366b0657e","rule":"all_shipped","series_missing":true,"session":"restatement-restated-2026-09-05","summary":{"arm_a_cells":120,"arm_b_cells":24,"arm_c_cells":96,"assets":24,"assets_at_the_floor":0,"bars_added_since_08_13":565,"cells":240,"cells_clearing_maker_per_product":0,"cells_clearing_taker_flat":0,"cells_clearing_taker_per_product":0,"cells_reproducing_bit_identically":3,"fee_pct":"0.012","flat_slippage_pct":"0.0005","floor_multiple_max":"36.8","floor_multiple_min":"1.1","gate_passed":0,"n_trials":960,"p_be_inside_allowance_5bp":"0.1488","p_be_inside_allowance_measured":"0.2052","p_be_outside_allowance_measured":"0.3480","pbo_available":0,"pf_median_delta":"-0.0928","pf_median_flat":"0.3120","pf_median_per_product":"0.2192","reconstructed_win_rate":"0.149","rules":5,"slippage_median_bp":"52.3","slippage_median_floor_multiple":"10.5","transfer_gap_08_13":"0.0343","transfer_gap_flat":"0.1172","transfer_gap_per_product":"0.1362"},"timestamp":1788728801,"trial_id":"restatement-restated-2026-09-05"} +{"decision":"diagnostic_only","kind":"ablation","params":{"trend_filter":"off","trend_slope_lookback":5,"trend_sma_period":200,"units":"per_trade_pnl is R, not dollars","verdict":"baseline"},"per_bar_pnl":[],"per_trade_pnl":["1.292148086974900141034908216","-1.376522086104324129707675681","-1.004370921023420863167429861","-0.7718713698114655561104454677","5.487170764056078383764899962","5.493664764750066872915912537","-1.299690231648594099144316316","1.785747660760297998360903885","0.4085455393963267418523947947","-1.416128469991380426250705656","-1.412950966806760150966914791","-0.9742744627014982980881786938","-1.250790675423654120136662756","0.6523413546901512780818913355","-1.031197158425031829389798502","5.449226759550496583622967309","5.502521068077155671375529692","-1.275331898160541569921941879","-1.291222326887620023795142400","-0.9353339277160885326520271417","-1.236189281393256692933185961","5.564759175377587348497785643","-1.238295027475741349601029412","3.264129262820485021583199482","-1.609310596730684773954941731","1.407474895000424862348192570","-1.201671811002749542032264133","5.589787907045651543731066488","5.661338319607414320018035642","-1.172603430258700601274637530","-1.188243195613420001666257105","-1.251618985975241181932772389","-1.222306056116387201636581789","-1.222714239767558290652595563","-1.256773763166714161581309906","-1.041438927173170441567936113","-1.290844640972520547612357855","-1.319393757991386712823999340","-0.6979693952241269560394188548","-1.367001175686148659782174997","-1.216966009678026955018584496","-1.185595601330898020471993522","-1.281358518437332914626847925","-1.280293140206222008162885895","-1.337456446839613019376554851","5.410587140417745437983677532","5.507083305038216011719715426","5.339886556206973803885266660","-0.7064960272910583424305993955","-1.231913953360633913130207035","-1.244756565913745447789845540","-1.203694966538309045790725213","-0.6810792537319196751317532369","-0.5666190939060682500571452133","-1.257758170880544324830115888","5.259127088058230742391349913","5.438328711357408502039271205","-1.227988069412323218548238378","5.581184227460517172362939405","-1.147583946640052749598797667","-1.221639500995219228038574337","-1.183219826034969867049072742","-1.223607196849596306794217141","-1.218026449970101319911501800","-1.248042221459613779682104513","5.432265599267714288015511929","0.3721561981966611058603056874","-1.215652746372685467335586364","-1.231498037788113013029157337","-1.226846335014515514050752680","5.584881856312180205723411540","-1.241529632723700505223230387","-0.4098855657145455679345851622","0.02615681626801874585898427896","-1.390368472302519821228579362","-1.314200235613775990355800490","-0.5671836130198057083635330954","-1.244163994778885712036608541","-1.183635729985173300716044753","-1.233816628738071483289033667","-0.3189887925447340638323405106","-0.6790763688915749551671504224","5.135940927848700029977821552","5.433959990368987073375621070","-1.145346892237493453743478771","-1.195106255935717199349928868","0.4489549667683345825148424608","-1.203911258616214724511164711","-1.189782124293966130656678216","-1.195632720527757737116898441","-1.231377252246935556760924509","5.213786210695079807711940060","-1.254126019172109863916639482","5.489777605588644141157323092","-1.131825914782156916925168072","-1.219971732684665488140123770","5.616002533957632519573393544","-1.201499160076091882346590586","-1.290562003536827292532456644","4.930143450013907854421546959","5.411676783079823890805598874","-1.175184384738606216440151708","-0.3207530983306938150034591523","-1.198737880809693207790945474","-1.180838147491307734099564122","-1.206595357408776247329541972","5.196638075122606248954568210","-1.235818964299967906523607597","5.377562152961955305281788265","-1.251105764362469180761981992","0.1876415382667780308737912500","5.382647154081230858176717229","5.579361739612056265169368626","-1.184615506749192693680489720","-1.194412099269006553252697508","-1.234555807616695321679150549","-0.5132550463644290605094108585","-1.280901334789467991155456966","-1.227722529249636513493367578","-1.291562764621049893422828068","5.280443581684204985442667474","-1.237767142166843104343134667","-1.161152453384780176896137705","-1.183512034448321938975866417","-1.173558821072350083810407598","-1.213128858931700628468283322","-1.186389082242234767552876491","-1.180654371091019496704231797","-1.195409641111178355209552949","-1.168982826045336174422605325","-1.284625103414456032382131606","-1.243401727911904640599239608","5.451506597083003264426719038","-1.189286830920358936151086870","-1.161648826547384513177314967","5.539458018351884364187857795","0.9006694602109121694531127235","5.231765470000697985022302055","-0.6487392703946249321342160979","5.368325501971480328564369458","-1.196549982610472632423318898","-1.158708430339273606624725211","-1.161725388349091494973004389","-1.151786003684574923920567824","-0.5628975652690938464686439711","0.3965123579070597359117638561","-1.181443140619646075925126764","-1.241439105231389502541634753","-0.3524013110828079501999547900","5.321492147606953930087244225","5.140553373057102829596044025","-1.152840021822224207915158330","-0.9640341139411848930553264909","-1.143899141190035047495123762","-1.253825240432750183323247178","-1.309145155980422565480432203","-1.249361218988967007827277209","-1.185525674763719684644336123","-1.318214140313703728445400125","-1.373948462951869181255154855","5.165821196422609889244092208","-1.254630962995711296630751280","-1.205409778567168132199099040","-1.248040352547708868340343490","-1.194335115243085157179799084","5.453432591853407940871466544","-1.259511833164055200969661146","-1.127819047887652529857778779","0.9510214039787395357044763299","-1.166032908224816408094599918","-1.269010557316971556262508750","-1.221334716439968238664116908","-1.228591524286868083226594138","-1.195601269661885869737049181","5.470598581939795942355655909","-1.174497321081851286178195203","-0.8015767908941090340421152604","-1.078865571368224980279129744","-1.289996894642511429159249924","-1.257526582032900401643430867","-1.205996994502027875512488776","-1.252634771038423653177145998","-1.242507766223074261660100207","-1.220808266766760701726087576","5.100208926119005468364750631","-1.228542653469393763443983481","-1.184804186109972412382417524","-1.243673653347884086688024949","5.336845288474578881433300381","-1.180227297397513444852130396","-1.219211550766279775072315677","-1.240491515412467749411824247","-1.175962587275978948462887548","-1.277421091908469052534784351","5.180351203695749485168809659","-0.08453576755520204712395934131","-1.270135739018584468157658834","-1.267468335535270539995610787","-1.260995743091046368732804280","-1.163388648619721085612197134","0.4954012584758866086407883462","1.685973633422451115868775142"],"prev_hash":"7a31e85d57acb94f69b28c88c69d4c027155f4cccf252a909b90227366b0657e","provenance":"a_priori","row_hash":"dcf0a0aec96b78dfddf38989f0cf4dc8d19563a1f29e0a0ad8d77a8e259a1942","rule":"turtle_breakout","series_missing":false,"session":"turtle-sma200-filter-2026-09-27","summary":{"avg_loss_r":"-1.1480","avg_win_r":"4.1553","breakeven_win_rate":"0.2165","detectable_edge":"0.1404","edge_over_breakeven":"0.0509","evaluated":true,"expectancy_r":"0.2697","n":202,"n_eff":"78.4","profit_factor_r":"1.3206","win_rate":"0.2673"},"timestamp":1790469740,"trial_id":"turtle-sma200-filter-2026-09-27-off"} +{"decision":"diagnostic_only","kind":"ablation","params":{"trend_filter":"above","trend_slope_lookback":5,"trend_sma_period":200,"units":"per_trade_pnl is R, not dollars","verdict":"no improvement distinguishable from the baseline"},"per_bar_pnl":[],"per_trade_pnl":["1.292148086974900141034908216","-1.376522086104324129707675681","-1.004370921023420863167429861","-0.7718713698114655561104454677","5.487170764056078383764899962","5.493664764750066872915912537","-1.299690231648594099144316316","1.785747660760297998360903885","0.4085455393963267418523947947","0.6523413546901512780818913355","-1.031197158425031829389798502","5.449226759550496583622967309","5.502521068077155671375529692","-1.275331898160541569921941879","-1.291222326887620023795142400","-0.9353339277160885326520271417","5.564759175377587348497785643","-1.238295027475741349601029412","3.264129262820485021583199482","-1.609310596730684773954941731","-1.742755441101091883819509982","-1.201671811002749542032264133","5.589787907045651543731066488","5.661338319607414320018035642","-1.172603430258700601274637530","-1.188243195613420001666257105","-1.251618985975241181932772389","-1.222306056116387201636581789","-1.222714239767558290652595563","-1.256773763166714161581309906","-1.041438927173170441567936113","-1.290844640972520547612357855","-0.6979693952241269560394188548","-1.367001175686148659782174997","-1.216966009678026955018584496","-1.185595601330898020471993522","-1.281358518437332914626847925","-1.337456446839613019376554851","5.410587140417745437983677532","5.507083305038216011719715426","-0.1264895652952541177690146447","-0.7064960272910583424305993955","-1.231913953360633913130207035","-1.244756565913745447789845540","-1.203694966538309045790725213","-0.6810792537319196751317532369","-1.233880666704040378685529770","-1.257758170880544324830115888","5.323391080936292855843732546","0.5916218731158806167814979758","-1.227988069412323218548238378","5.581184227460517172362939405","-1.147583946640052749598797667","-1.221639500995219228038574337","-1.223607196849596306794217141","-1.218026449970101319911501800","-1.248042221459613779682104513","5.432265599267714288015511929","0.3721561981966611058603056874","-1.215652746372685467335586364","-1.231498037788113013029157337","-1.226846335014515514050752680","5.584881856312180205723411540","-1.241529632723700505223230387","-0.4098855657145455679345851622","0.02615681626801874585898427896","-1.390368472302519821228579362","-1.314200235613775990355800490","-1.240513390324864350709477586","-1.244163994778885712036608541","-1.183635729985173300716044753","-1.233816628738071483289033667","-0.3189887925447340638323405106","-0.6790763688915749551671504224","5.135940927848700029977821552","5.433959990368987073375621070","-1.145346892237493453743478771","-1.195106255935717199349928868","0.4489549667683345825148424608","-1.189782124293966130656678216","-1.195632720527757737116898441","-1.231377252246935556760924509","-1.341572688452216252511459184","-1.313792948059957653521572020","-1.263680324035083528141242787","-1.239773813839844929698029767","-1.260733480410325162630853668","5.489777605588644141157323092","-1.131825914782156916925168072","-1.219971732684665488140123770","5.616002533957632519573393544","-1.201499160076091882346590586","-1.308699067259459052882732417","-1.202595582503312798867709105","5.411676783079823890805598874","-1.175184384738606216440151708","-0.3207530983306938150034591523","-1.180838147491307734099564122","5.196638075122606248954568210","-1.235818964299967906523607597","5.377562152961955305281788265","-1.196009707594184898852082210","5.424113220286212744680656040","5.568677125893870279003434310","-1.159424329637310106576677191","-1.194412099269006553252697508","-1.234555807616695321679150549","-1.157280422043950879252122554","-1.280901334789467991155456966","-1.227722529249636513493367578","-1.291562764621049893422828068","5.280443581684204985442667474","-1.237767142166843104343134667","-1.161152453384780176896137705","-1.183512034448321938975866417","-1.173558821072350083810407598","-1.180654371091019496704231797","-1.195409641111178355209552949","-1.168982826045336174422605325","-1.243401727911904640599239608","5.451506597083003264426719038","-1.189286830920358936151086870","-1.161648826547384513177314967","5.539458018351884364187857795","0.9006694602109121694531127235","5.231765470000697985022302055","-0.6487392703946249321342160979","5.368325501971480328564369458","-1.196549982610472632423318898","-1.158708430339273606624725211","-1.151786003684574923920567824","-1.181443140619646075925126764","-1.203031766522364494024121423","-1.200768957280433729677255265","-1.271920830531581980987632196","5.372719827045794442975062199","-1.185453446943265618695707083","-1.152840021822224207915158330","-0.9640341139411848930553264909","-1.143899141190035047495123762","-1.253825240432750183323247178","-1.309145155980422565480432203","-1.249361218988967007827277209","-1.258738824640275286834370120","-1.229481138072890335176775613","-1.308329413954474304078950294","5.271765050123315563346679925","-1.193487777949478817730590845","-1.248040352547708868340343490","-1.194335115243085157179799084","5.453432591853407940871466544","-1.259511833164055200969661146","-1.047826864120045039670274990","-1.166032908224816408094599918","-1.228591524286868083226594138","5.470598581939795942355655909","-1.174497321081851286178195203","-0.8015767908941090340421152604","-1.078865571368224980279129744","-1.289996894642511429159249924","-1.257526582032900401643430867","-1.205996994502027875512488776","-1.252634771038423653177145998","-1.236476665774456249229611013","-1.307561185711953909514529982","5.207415526052777467498179006","-1.184804186109972412382417524","-1.243673653347884086688024949","5.336845288474578881433300381","-1.180227297397513444852130396","-1.219211550766279775072315677","-1.240491515412467749411824247","-1.277421091908469052534784351","5.180351203695749485168809659","-0.08453576755520204712395934131","-1.270135739018584468157658834","-1.267468335535270539995610787","-1.260995743091046368732804280","-1.163388648619721085612197134","0.4954012584758866086407883462","1.685973633422451115868775142"],"prev_hash":"dcf0a0aec96b78dfddf38989f0cf4dc8d19563a1f29e0a0ad8d77a8e259a1942","provenance":"a_priori","row_hash":"7eff3ea4b7119c256cad57989f653441f775f8a832f0694edd93808fae7c4d20","rule":"turtle_breakout","series_missing":false,"session":"turtle-sma200-filter-2026-09-27","summary":{"avg_loss_r":"-1.1615","avg_win_r":"4.2681","breakeven_win_rate":"0.2139","detectable_edge":"0.1483","diff_expectancy_r_ci95_high":"0.5397","diff_expectancy_r_ci95_low":"-0.6410","edge_over_breakeven":"0.0402","evaluated":true,"expectancy_r":"0.2184","n":181,"n_eff":"70.3","profit_factor_r":"1.2520","win_rate":"0.2541"},"timestamp":1790469741,"trial_id":"turtle-sma200-filter-2026-09-27-above"} +{"decision":"diagnostic_only","kind":"ablation","params":{"trend_filter":"slope","trend_slope_lookback":5,"trend_sma_period":200,"units":"per_trade_pnl is R, not dollars","verdict":"no improvement distinguishable from the baseline"},"per_bar_pnl":[],"per_trade_pnl":["-1.241245958550353540129722424","-1.004370921023420863167429861","-0.7718713698114655561104454677","5.487170764056078383764899962","5.493664764750066872915912537","-1.299690231648594099144316316","1.785747660760297998360903885","0.4085455393963267418523947947","0.6523413546901512780818913355","-1.031197158425031829389798502","-1.387167603782142265366038721","5.502141579706489594539158670","-0.4588021471691066693524730196","-0.9353339277160885326520271417","-1.288239925923764676545487387","0.2460945060624967123966774733","3.264129262820485021583199482","-1.609310596730684773954941731","-1.201671811002749542032264133","5.589787907045651543731066488","5.661338319607414320018035642","-1.172603430258700601274637530","-1.188243195613420001666257105","-1.251618985975241181932772389","-1.222306056116387201636581789","-1.222714239767558290652595563","-1.041438927173170441567936113","-1.290844640972520547612357855","-1.367001175686148659782174997","-1.216966009678026955018584496","-1.185595601330898020471993522","-1.281358518437332914626847925","5.410587140417745437983677532","5.507083305038216011719715426","-0.7064960272910583424305993955","-1.231913953360633913130207035","-1.203694966538309045790725213","-0.6810792537319196751317532369","5.402461528152645729378636713","-1.227988069412323218548238378","5.581184227460517172362939405","-1.147583946640052749598797667","-1.223607196849596306794217141","-1.248042221459613779682104513","5.432265599267714288015511929","0.3721561981966611058603056874","-1.215652746372685467335586364","-1.231498037788113013029157337","-1.226846335014515514050752680","5.584881856312180205723411540","-1.336712834654472077257673808","-1.272058768146113115220848141","-1.183635729985173300716044753","-1.233816628738071483289033667","-0.3189887925447340638323405106","-0.6790763688915749551671504224","5.135940927848700029977821552","5.433959990368987073375621070","-1.145346892237493453743478771","-1.195106255935717199349928868","0.4489549667683345825148424608","-1.195632720527757737116898441","-1.231377252246935556760924509","-1.260733480410325162630853668","5.489777605588644141157323092","-1.131825914782156916925168072","5.616002533957632519573393544","5.335189434094858364489053846","-1.172214898782804103122629416","-0.3207530983306938150034591523","-1.235818964299967906523607597","5.377562152961955305281788265","5.579361739612056265169368626","-1.184615506749192693680489720","-1.194412099269006553252697508","-1.234555807616695321679150549","-1.280901334789467991155456966","-1.291562764621049893422828068","-1.212678607166546162223055419","-1.237767142166843104343134667","-1.161152453384780176896137705","-1.183512034448321938975866417","-1.173558821072350083810407598","-1.168982826045336174422605325","5.451506597083003264426719038","-1.189286830920358936151086870","5.539458018351884364187857795","0.9006694602109121694531127235","-1.184623334245156098175315735","0.4850432409603459827393866322","5.368325501971480328564369458","-1.196549982610472632423318898","-1.158708430339273606624725211","5.356412735383928913719350248","-1.152840021822224207915158330","-1.178759522737595293838863098","-1.150076100886395101969425008","-1.249361218988967007827277209","-0.1870805882988304826130244598","-1.248040352547708868340343490","-1.194335115243085157179799084","0.1268110057510902454823510767","-1.213201333919988166281777670","-1.212183797991150779081799465","-1.168010177888181547928101032","5.437133392437201735570808777","-0.8015767908941090340421152604","-1.205996994502027875512488776","-1.228542653469393763443983481","-1.184804186109972412382417524","-1.243673653347884086688024949","5.386779429538590313163963948","-1.168773441221553935399796292","5.180351203695749485168809659","-0.08453576755520204712395934131","-1.270135739018584468157658834","-1.267468335535270539995610787","-1.260995743091046368732804280","-1.163388648619721085612197134","-1.186307564910810433194974640","1.685973633422451115868775142"],"prev_hash":"7eff3ea4b7119c256cad57989f653441f775f8a832f0694edd93808fae7c4d20","provenance":"a_priori","row_hash":"a2968bdd11421fc1437220230b6524e2a4709e3253b4c09dc4ae104d5d586cfe","rule":"turtle_breakout","series_missing":false,"session":"turtle-sma200-filter-2026-09-27","summary":{"avg_loss_r":"-1.1294","avg_win_r":"4.0755","breakeven_win_rate":"0.2170","detectable_edge":"0.1814","diff_expectancy_r_ci95_high":"0.8289","diff_expectancy_r_ci95_low":"-0.5151","edge_over_breakeven":"0.0805","evaluated":true,"expectancy_r":"0.4191","n":121,"n_eff":"47.0","profit_factor_r":"1.5283","win_rate":"0.2975"},"timestamp":1790469742,"trial_id":"turtle-sma200-filter-2026-09-27-slope"} +{"decision":"diagnostic_only","kind":"ablation","params":{"trend_filter":"both","trend_slope_lookback":5,"trend_sma_period":200,"units":"per_trade_pnl is R, not dollars","verdict":"no improvement distinguishable from the baseline"},"per_bar_pnl":[],"per_trade_pnl":["-1.241245958550353540129722424","-1.004370921023420863167429861","-0.7718713698114655561104454677","5.487170764056078383764899962","5.493664764750066872915912537","-1.299690231648594099144316316","1.785747660760297998360903885","0.4085455393963267418523947947","0.6523413546901512780818913355","-1.031197158425031829389798502","-1.387167603782142265366038721","5.502141579706489594539158670","-0.4588021471691066693524730196","-0.9353339277160885326520271417","-1.288239925923764676545487387","0.2460945060624967123966774733","3.264129262820485021583199482","-1.609310596730684773954941731","-1.201671811002749542032264133","5.589787907045651543731066488","5.661338319607414320018035642","-1.172603430258700601274637530","-1.188243195613420001666257105","-1.251618985975241181932772389","-1.222306056116387201636581789","-1.222714239767558290652595563","-1.041438927173170441567936113","-1.290844640972520547612357855","-1.367001175686148659782174997","-1.216966009678026955018584496","-1.185595601330898020471993522","-1.281358518437332914626847925","5.410587140417745437983677532","5.507083305038216011719715426","-0.7064960272910583424305993955","-1.231913953360633913130207035","-1.203694966538309045790725213","-0.6810792537319196751317532369","5.402461528152645729378636713","-1.227988069412323218548238378","5.581184227460517172362939405","-1.147583946640052749598797667","-1.223607196849596306794217141","-1.248042221459613779682104513","5.432265599267714288015511929","0.3721561981966611058603056874","-1.215652746372685467335586364","-1.231498037788113013029157337","-1.226846335014515514050752680","5.584881856312180205723411540","-1.336712834654472077257673808","-1.272058768146113115220848141","-1.183635729985173300716044753","-1.233816628738071483289033667","-0.3189887925447340638323405106","-0.6790763688915749551671504224","5.135940927848700029977821552","5.433959990368987073375621070","-1.145346892237493453743478771","-1.195106255935717199349928868","0.4489549667683345825148424608","-1.195632720527757737116898441","-1.231377252246935556760924509","-1.260733480410325162630853668","5.489777605588644141157323092","-1.131825914782156916925168072","5.616002533957632519573393544","5.335189434094858364489053846","-1.172214898782804103122629416","-0.3207530983306938150034591523","-1.235818964299967906523607597","5.377562152961955305281788265","5.579361739612056265169368626","-1.184615506749192693680489720","-1.194412099269006553252697508","-1.234555807616695321679150549","-1.280901334789467991155456966","-1.291562764621049893422828068","-1.212678607166546162223055419","-1.237767142166843104343134667","-1.161152453384780176896137705","-1.183512034448321938975866417","-1.173558821072350083810407598","-1.168982826045336174422605325","5.451506597083003264426719038","-1.189286830920358936151086870","5.539458018351884364187857795","0.9006694602109121694531127235","-1.184623334245156098175315735","0.4850432409603459827393866322","5.368325501971480328564369458","-1.196549982610472632423318898","-1.158708430339273606624725211","5.356412735383928913719350248","-1.152840021822224207915158330","-1.178759522737595293838863098","-1.150076100886395101969425008","-1.249361218988967007827277209","-0.1870805882988304826130244598","-1.248040352547708868340343490","-1.194335115243085157179799084","-1.047826864120045039670274990","-1.213201333919988166281777670","-1.212183797991150779081799465","-1.168010177888181547928101032","5.437133392437201735570808777","-0.8015767908941090340421152604","-1.205996994502027875512488776","-1.228542653469393763443983481","-1.184804186109972412382417524","-1.243673653347884086688024949","5.386779429538590313163963948","-1.168773441221553935399796292","5.180351203695749485168809659","-0.08453576755520204712395934131","-1.270135739018584468157658834","-1.267468335535270539995610787","-1.260995743091046368732804280","-1.163388648619721085612197134","-1.186307564910810433194974640","1.685973633422451115868775142"],"prev_hash":"a2968bdd11421fc1437220230b6524e2a4709e3253b4c09dc4ae104d5d586cfe","provenance":"a_priori","row_hash":"aae6ace934798acdfca05c7ead2fb7b98e20d7201373a20f0e4db33f481cf608","rule":"turtle_breakout","series_missing":false,"session":"turtle-sma200-filter-2026-09-27","summary":{"avg_loss_r":"-1.1285","avg_win_r":"4.1883","breakeven_win_rate":"0.2122","detectable_edge":"0.1814","diff_expectancy_r_ci95_high":"0.8236","diff_expectancy_r_ci95_low":"-0.5241","edge_over_breakeven":"0.0770","evaluated":true,"expectancy_r":"0.4094","n":121,"n_eff":"47.0","profit_factor_r":"1.5105","win_rate":"0.2893"},"timestamp":1790469743,"trial_id":"turtle-sma200-filter-2026-09-27-both"}