أَغْنِنَا بِحَلَالِكَ عَنْ حَرَامِكَ

Experiment: trailing vs static exits

Fetched at build time from CodeGateSoftware/keel at v0.21.0. If a document moves, the build fails — this site never renders stale docs. Last fetched 2026-10-18. View source on GitHub.

Fee note (added 2026-09-27, #836). This record describes a fee-free regime inside a venue’s monthly allowance. For keel’s orders that regime is hypothetical: they go through Coinbase Advanced Trade, which has no fee-waiver tier. On 2026-09-27 the account reported the Intro tier (0.9% taker, 0.5% maker, has_promo_fee: false), and every live fill had paid a fee; Coinbase One’s zero trading fees apply on the main Coinbase platform, not Advanced Trade. Figures priced at the taker fee are the realistic ones; figures priced at zero fee describe a regime keel cannot reach. Nothing on this page has been rewritten.

Cost note (added 2026-09-02). The figures below are priced at the flat 5bp slippage floor. the per-product restatement later measured that no asset in keel’s universe reaches that floor — the range is 1.1× to 36.8× — so every profit factor here is optimistic by roughly 0.09 at the median. The verdict is unaffected: the correction only ever moves a number down, and every result here was already negative. Nothing on this page has been rewritten; records are appended to, not revised.

Issue #442. Driver: 2026-08-22-trailing-vs-static-exits.py (pre-registered in its docstring before the run). Artifact: every number below is a row of 2026-08-22-trailing-vs-static-exits.jsonl — 240 cells (2 families x 30 products x 4 arms) plus 8 pooled rows.

Headline, stated first because it is the point: the pre-registered expectation held — trailing and break-even rolls do NOT improve either family at the 120 bp taker fee, and the trailing arm makes both families clearly worse. The constructor defaults therefore stay OFF (trail_atr_mult=None, be_roll_rr=None): the capability is wired and measured, and the measurement says not to turn it on.

Why this run exists

#442 found executor.trail_stop_atr / roll_to_break_even / scale_out implemented, unit-tested, and never called. The PR that ships alongside this record wires the exit POLICY (ratchet-only ATR trailing + break-even roll) into the engines that drive exits per bar (strategy.backtest, sim.portfolio_sim, via strategy/exit_policy.py and the per-family trail_atr_mult/be_roll_rr params), with turtle deliberately excluded. This run measures what the wiring changes, before any default is flipped.

Method (frozen in the driver’s docstring before the run)

  • Same engine as the #475 significance reconstruction: keel.strategy.backtest.backtest over every product’s full cached ONE_HOUR history (2021-07-18 to 2026-08-21 UTC; 30 products with

    = 2,000 hourly bars, 5,902 to 44,623 bars each), next-bar-open market fills, 5 bp per-leg slippage, and the 120 bp taker fee per leg (backtest.TAKER_FEE_PCT) — the rate outside the fee-free allowance, because the question is whether exit management changes the family’s real-cost economics.

  • Four pre-registered arms, constructor defaults for everything except the exit knobs: static (no knobs — the shipped default and the pre-#442 behavior), trail_1_5 (trail_atr_mult=1.5 — the live primitive’s own default multiplier), be_1 (be_roll_rr=1), trail_1_5_be_1 (both).
  • turtle_breakout is out of scope BY DESIGN: its real exit is the Donchian channel and the family carries no exit-policy knobs (#442 hypothesis 3) — there is no trailing arm to measure.
  • Read-only against the deployment cache (file:...?mode=ro); the only writes are the JSONL artifact and stdout.

Results — pooled per (family, arm), the pre-registered question

familyarmn (pooled)win rateexpectancy (1-unit notional)profit factoravg R
pullback_continuationstatic3,2610.1435−68.000.0075−2.438
pullback_continuationtrail_1_53,6970.0933−61.890.0017−2.195
pullback_continuationbe_13,3030.1150−69.190.0016−2.424
pullback_continuationtrail_1_5_be_13,7040.0886−62.030.0016−2.196
rsi_meanrevstatic1,2010.2714−108.890.0776−1.394
rsi_meanrevtrail_1_51,2130.1665−111.400.0197−1.377
rsi_meanrevbe_11,2070.2030−118.300.0336−1.409
rsi_meanrevtrail_1_5_be_11,2150.1597−110.330.0198−1.375

Wall clock 5,189 s for 240 cells. Per-product direction (arm expectancy vs static expectancy, 30 products each cell):

familyarmproducts better / worse / same
pullback_continuationtrail_1_517 / 13 / 0
pullback_continuationbe_116 / 13 / 1
pullback_continuationtrail_1_5_be_116 / 14 / 0
rsi_meanrevtrail_1_514 / 16 / 0
rsi_meanrevbe_113 / 15 / 2
rsi_meanrevtrail_1_5_be_113 / 17 / 0

What the numbers say

  • Every arm of every family is deeply negative at 120 bp/leg — the same wall #475 hit; exit management does not move it. This run was never going to find an edge that survives a fee the families do not survive, and it does not.
  • The trailing arm is clearly worse, not differently-bad. Win rate collapses (pullback 14.4% → 9.3%; rsi 27.1% → 16.7%) and profit factor drops ~4x in both families: the trail cuts winners before the fixed-R target they were sized to reach — exactly the pre-registered mechanism, and exactly why turtle (whose entire payoff is the long tail) was excluded by design.
  • rsi_meanrev’s expectancy is strictly worse in every arm (−108.89 → −110.33..−118.30): a mean-reverter’s edge is the quick full move; every form of early exit gives back part of it and pays the same round-trip fee.
  • pullback_continuation’s expectancy moves −68.00 → −61.89..−69.19 — a less-bad loss on the same sign, not an improvement. The trailing arms’ n grows 3,261 → 3,697 (+436 trades, +13%): earlier exits free the slot sooner, each re-entry paying the full round-trip friction again. The trail also halves what little profit factor the static arm retains.
  • The per-product counts are a coin flip (best cell 17/30 better, worst 13/30) — there is no consistent per-product direction to appeal to either.

Decision the evidence supports

  • Constructor defaults stay OFF for both families (trail_atr_mult=None, be_roll_rr=None). The knobs remain available for research sweeps and for any future rule row that wants to carry them, but nothing shipped turns them on: at the fee actually paid, the wiring’s measured effect is negative-to-neutral, exactly the direction the pre-registration predicted and for the reasons it predicted.
  • The wiring itself is NOT reverted — the capability was the point of #442, and a default-off policy whose knobs measurably do what they say (ratchet, exit earlier) is now tested behavior rather than a dormant claim.
  • Live stop management remains issue #502 (the broker port has no bracket/OCO OrderSpec kind), independent of this result.
  • A stated limitation: the paper trader (strategy/paper.py) is NOT wired. It is signal-driven — it holds no Rule instance to read params from, and its on_candle receives one candle at a time, so the trailing ATR has no series to compute from. With the defaults OFF no paper row can carry the knobs into a divergence today; a paper row that sets them would backtest with management and paper-trade without it, which the knobs’ PARAM_DOCS state outright (“Sim/backtest engines only”). Paper should follow #502.

View source on GitHub — docs/experiments/2026-08-22-trailing-vs-static-exits.md