Skip to content

feat(rules): triple-barrier exits with a vertical time stop, and the measurement (#342) - #685

Merged
eaitbrahim merged 1 commit into
mainfrom
feat-342-triple-barrier
Sep 2, 2026
Merged

feat(rules): triple-barrier exits with a vertical time stop, and the measurement (#342)#685
eaitbrahim merged 1 commit into
mainfrom
feat-342-triple-barrier

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

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.

The result, in two numbers

cusum_event and triple_barrier share an entry — same CUSUM filter, same threshold, same universe, same window. Only the exit differs, so the difference is attributable.

zero fee 1.2% taker
control (cusum_event) median PF 0.925 0.343
triple_barrier median PF 1.001 0.338
median delta +0.033 −0.004
assets improved 17 of 24 11 of 24 (a coin flip)

The 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_volume returns a per-bar median despite its name, and slippage_for_quote_volume is 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_trip scales 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 < 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. It's 0 < stop < entry < target now. Unreachable inside the declared param space, reachable from a stored row, which is why it isn't dead code.

The vertical axis

bars median n PF median above 1.0
6 520 0.161 0
12 486 0.243 0
24 431 0.338 0
48 374 0.392 1
72 338 0.464 1

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.

…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
@eaitbrahim eaitbrahim self-assigned this Sep 2, 2026
@eaitbrahim
eaitbrahim merged commit f1a3087 into main Sep 2, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-342-triple-barrier branch September 2, 2026 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant