feat(rules): triple-barrier exits with a vertical time stop, and the measurement (#342) - #685
Merged
Merged
Conversation
…measurement (#342) Research brief B-2, prerequisite-gated on #339 (closed 2026-08-17). A sixth `RULE_REGISTRY` kind and the A/B it exists to make possible. WHAT IS NEW. Three barriers close a position: upper, lower, and a VERTICAL one -- sell at the close after N bars, whatever price has done. No shipped rule has the third, and it is the leg of the source's method that TRANSFERS, because it is executable under keel's market fills where a resting order at a horizontal barrier is not. THE BARRIERS ARE MULTIPLES OF THE PRODUCT'S OWN ROUND TRIP, which is #342's design ask. The source's +/-2.5-5% sit at or below one round trip here, so transplanted as percentages they are mechanically dead. A fixed percentage is also wrong per asset: since #259 the backtest prices thin books up to 183.8bp per leg, so the same 5% is four round trips on BTC and barely one on the corpus tail. `per_product_round_trip` computes it from the product's own candles. A UNIT TRAP, RECORDED BECAUSE IT PRODUCES NO ERROR. `median_daily_quote_volume` returns a PER-BAR median despite its name, and `slippage_for_quote_volume` is anchored on $500M DAILY -- so the hourly figure handed over unscaled reports every asset as maximally thin, clamps the universe to the cap and makes every barrier four times too wide, silently. The bars-per-day scaling is the fix and a test asserts its consequence, not the call. A BUG THE MUTATION RUN FOUND IN MY OWN GUARD. `if not stop < entry < target` passes a NEGATIVE stop: a large enough `stop_friction_mult` drives the barrier below zero, the ordering still holds, and `risk = entry - stop` then exceeds the entry itself -- sizing a position off a loss larger than the whole holding. The guard is `0 < stop < entry < target`. It is unreachable inside the declared param space and reachable from a stored row, which is why it is not dead code. THE MEASUREMENT, AND IT IS THE POINT OF THE ISSUE. 192 trials against `cusum_event` as a CONTROL: same entry, same universe, same window, so the exit is the only thing that changed. At ZERO fee the exit lifts the median profit factor ACROSS BREAK-EVEN, 0.925 -> 1.001, improving 17 of 24 assets. At the 1.2% taker rate the median delta is -0.004 and the sign is a coin flip. That is a real, measurable gross improvement from a better exit -- the first number in this series to move the right way -- and friction consumes all of it. Zero of 24 clear PF 1.0 at the taker rate; the only two cells above 1.0 anywhere are TON-USD at n=17 and n=16, a sixth of the admission floor and maxima of five draws. The intersection stays empty across all 192. The vertical axis is monotone (6->72 bars: median PF 0.161 -> 0.464) and the source's own 24 is mid-range, not optimal -- the DIRECTION agrees with its "wide barriers beat next-bar labeling" finding, the magnitude does not, because 1.2% per leg is twelve times the 0.1% that paper priced. Both halves of the source are now implemented and measured. 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. That is the clearest measurement in the series of the difference between a result and a result AT A PRICE. 9 mutants, 9 killed. Nothing promoted, no rule row added, config and allowlist untouched. The null grows: 0 of 90 -> 0 of 114 -> 0 of 138. Ledger verifies at 92 rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Research brief B-2, prerequisite-gated on #339 (closed 2026-08-17). A sixth
RULE_REGISTRYkind, and the A/B it exists to make possible.The result, in two numbers
cusum_eventandtriple_barriershare an entry — same CUSUM filter, same threshold, same universe, same window. Only the exit differs, so the difference is attributable.cusum_event) median PFtriple_barriermedian PFThe exit genuinely works. At zero fee it lifts the median profit factor across break-even and improves 17 of 24 assets — the first number in this series of documents to move the right way.
And it is worth nothing. At the rate the account pays, the median delta is −0.004. The gross gain is smaller than the friction it has to be harvested through.
Zero of 24 clear PF 1.0 at the taker rate. The only two cells above 1.0 anywhere are TON-USD at n=17 and n=16 — a sixth of the admission floor, maxima of five draws on the thinnest asset. The intersection is empty across all 192 trials.
What's new in the rule
Two things worth your eye
A unit trap that produces no error.
median_daily_quote_volumereturns a per-bar median despite its name, andslippage_for_quote_volumeis anchored on $500M daily. Handing 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.per_product_round_tripscales by bars-per-day; a test asserts the consequence rather than the call.A bug the mutation run found in my own guard.
if not stop < entry < targetpasses a negative stop: a large enoughstop_friction_multdrives the barrier below zero, the ordering still holds, andrisk = entry − stopthen exceeds the entry itself — sizing a position off a loss larger than the whole holding. It's0 < stop < entry < targetnow. Unreachable inside the declared param space, reachable from a stored row, which is why it isn't dead code.The vertical axis
Monotone, and the source's own 24-bar barrier is mid-range rather than optimal. The direction agrees with its "wide barriers beat next-bar labeling" finding; the magnitude doesn't, because 1.2% per leg is twelve times the 0.1% that paper priced.
What this settles
Both halves of Grądzki et al. are now implemented on keel's cost structure and measured on one universe: the entry half has essentially no gross edge, and the exit half is a real improvement 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. That's the clearest measurement in the series of the difference between a result and a result at a price.
Verification
9 mutants, 9 killed. Full suite 5241 passed / 3 skipped; ruff and mypy clean. Ledger verifies at 92 rows.
Nothing promoted, no rule row added, config and allowlist untouched. The null grows: 0 of 90 → 0 of 114 → 0 of 138.