fix(strategy): fill entries at the next bar's open, as production's market orders do - #258
Merged
Merged
Conversation
…arket orders do
The simulator held a Setup until a later bar's range TOUCHED its entry, then filled AT that
level. Production does not do that. `execution/executor.py::_order_row` writes:
order_type="market", limit_price=None, expected_fill=intent.entry
so live never rests an order at `Setup.entry` and never waits for price to come to it -- when
`engine.evaluate` emits a signal and the rails pass, the executor buys at market that cycle.
The old model therefore granted the backtest two things the live box does not have:
1. Free optionality on the entry price. A setup only became a trade if the market offered the
chosen level, so unfavourable entries were silently declined. Live pays whatever the market
is doing, favourable or not.
2. Unbounded patience. Any later bar touching the level filled there.
Both flatter results, and the bias runs OPPOSITE to #254's, which suppressed trades.
Entries now fill at `candles[i+1].open` plus slippage -- the first price obtainable once the
signal exists, and the earliest fill involving no lookahead. `Setup.entry` becomes informational,
exactly as `expected_fill` is live; risk is measured from the achieved fill against the setup's
stop, never from the quoted entry.
This subsumes #254: every pending now resolves on the very next bar, so the state that froze the
detector is unreachable rather than merely handled. It also removes the wrinkle in #247's fee
justification -- "touching entry is marketable, therefore taker" held for a breakout entry above
the market but not for a mean-reversion entry below it, which touching would make a MAKER fill.
With every entry a market order, taker is unconditionally the right rate.
Consequence named rather than hidden: a rule encoding a CONFIRMATION condition in its entry price
no longer gets one. `pullback_continuation` sets `entry = signal_candle.high + buffer` precisely
to demand follow-through, and a market fill takes trades it meant to decline. That is not
introduced here -- it is what the live box already does. Making the executor honour a stop/limit
entry is the alternative (#257 "Option B"); it changes money-moving code to accommodate a rule
already measured as alpha-deficient, so it is deliberately not taken.
Tests: entry-vs-stop intrabar resolution is deleted -- that ambiguity only arises for an entry
seeking a level, and is now unreachable. `_resolve_order` is still exercised for stop-vs-target.
#254's regression class is restated as `TestEntryFillsAtNextBarOpen`, asserting a setup whose
entry is never touched fills anyway, and that `detect_calls` is 1 rather than #256's 3 -- the
number that distinguishes the two fill models.
Golden regenerated. On the committed BTC daily corpus the move is tiny (profit_factor
1.269371 -> 1.269287, n_trades 13 unchanged) because `turtle_breakout` enters at the close and
24/7 crypto barely gaps. That is this corpus being insensitive, not the fill model being
unimportant -- it does not exercise an offset entry.
Closes #257.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 13, 2026
…ine, and record why the defects were invisible (#261) Both experiments re-run under #256 (pending setups no longer freeze the detector) and #258 (entries fill at the next bar's open, as production's market orders do). Same designs, same combinations, same fee grid and slippage pin -- only the engine differs. EVERY CONCLUSION SURVIVES EXCEPT ONE. ZEC-turtle no longer clears the maker line (1.034 -> 0.968), so #252 section 6's three-probe narrative describes a survivor the faithful engine never produces. The replacement is simpler and worse for the library: the viable quadrant is empty at every reachable fee -- 0 of 90 in #252, 0 of 82 in #255 -- with nothing needing three gates to die. #255 strengthens: the level shift across the trade floor widens from 1.1631 -> 0.8938 to 1.1251 -> 0.8396, gross-positive cells at the floor nearly halve (11/76 -> 6/82), and the 34x UNI-USD monotonicity anomaly that exposed both defects is structurally gone (3 assets -> 0). The two defects pushed in OPPOSITE directions -- #256 suppressed opportunity, #258 flattered execution -- so correcting both moved everything one way rather than adding noise: trade counts rose in 87 of 90, gross profit factors fell in 69 of 90. Arm B's transfer is restated on a single engine: 0.5770 in-sample vs 0.5427 out-of-sample, a gap of 0.034. #252's 0.6335 vs 0.6346 compared figures from one engine and was partly luck. The conclusion is unchanged -- the sweep winner is not overfit, it is stably unprofitable. Records the operational takeaway as section 5: NEITHER DEFECT WAS FOUND BY LOOKING FOR DEFECTS, and neither was findable by the means we had. 2,712 tests passed throughout. A frozen backtest and a highly selective strategy produce identical-looking output, so no summary ledger distinguishes them. The fix is invariants the engine reports about itself -- a dead-tail warning, intent-vs-fill divergence logging (#260), and cost anchored to output (#247, shipped, and the model for the rest) -- not more unit tests, which only assert behaviour someone already imagined. Annotates rather than rewrites, per the convention #247 set: the original numbers were real outputs of the code as it stood. Both documents keep their figures and carry a banner pointing here. Two ledger rows; chain verifies clean at 85. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 13, 2026
… measurements in it The scout skill lived only in ~/.claude/skills/, outside version control, with a single .bak as its entire history. Most of it is not procedure -- it is recorded reasoning about measurements that cost real compute to produce. An untracked file loses the argument along with the rule. Tracked via `git add -f` because `.gitignore:34` ignores `.claude/`. That is the same exception the two review-fix-merge-pr skills already use, so this follows existing precedent rather than inventing a location. Content added today, all additive -- all 13 original sections are intact, including the guardrails (Do NOT, Rationalizations, the citation-fabrication warning, Output 2's rejection history): - "Taker math controls the field": round-trip friction is ~2.5% of notional, levied on the SEARCH rather than the edge, and the viable intersection is vacant across all 24 assets. Liquidity and admissibility are entry conditions, not evidence. - "What has already been measured": points at the three experiment documents, and separates asset expansion as a refuted PERFORMANCE fix from its still-valid uses (statistical power, the compliance pipeline, future rules) -- which is why the skill is not retired. - Four new required citation fields: `cost_regime`, `sample_size`, `evaluation_window` and `not_already_covered`. Each is anchored to a specific way this project was misled -- a result measured at 5bp that does not transfer to 1.2%, rsi_meanrev's 1.1631 on n=38 collapsing to 0.8396 at the floor, ZEC compressing 92.7% of lifetime PnL into one regime, and entry_lookback=55 sitting inside an already-swept grid. - Fill-model framing: entries fill at the next bar's open as market orders (#258), so a technique assuming a resting limit or stop entry will not execute as described. - Four Rationalizations rows, including "this backtest looks clean, the numbers are all plausible" -- two defects produced plausible, internally consistent output for the life of the project while 2,712 tests passed. Two things deliberately NOT added, with the reasoning recorded so the next proposal to add them can see why: a check on the pending-lifespan invariant (it raises rather than logs, so a violation ends the run and is unobservable from a proposer), and a stationarity rejection threshold (ZEC under pullback_continuation is the only pair in the study with no losing complete year, at 0.875 gross -- any such gate passes that and rejects the one thing that made money gross). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 13, 2026
… measurements in it (#263) The scout skill lived only in ~/.claude/skills/, outside version control, with a single .bak as its entire history. Most of it is not procedure -- it is recorded reasoning about measurements that cost real compute to produce. An untracked file loses the argument along with the rule. Tracked via `git add -f` because `.gitignore:34` ignores `.claude/`. That is the same exception the two review-fix-merge-pr skills already use, so this follows existing precedent rather than inventing a location. Content added today, all additive -- all 13 original sections are intact, including the guardrails (Do NOT, Rationalizations, the citation-fabrication warning, Output 2's rejection history): - "Taker math controls the field": round-trip friction is ~2.5% of notional, levied on the SEARCH rather than the edge, and the viable intersection is vacant across all 24 assets. Liquidity and admissibility are entry conditions, not evidence. - "What has already been measured": points at the three experiment documents, and separates asset expansion as a refuted PERFORMANCE fix from its still-valid uses (statistical power, the compliance pipeline, future rules) -- which is why the skill is not retired. - Four new required citation fields: `cost_regime`, `sample_size`, `evaluation_window` and `not_already_covered`. Each is anchored to a specific way this project was misled -- a result measured at 5bp that does not transfer to 1.2%, rsi_meanrev's 1.1631 on n=38 collapsing to 0.8396 at the floor, ZEC compressing 92.7% of lifetime PnL into one regime, and entry_lookback=55 sitting inside an already-swept grid. - Fill-model framing: entries fill at the next bar's open as market orders (#258), so a technique assuming a resting limit or stop entry will not execute as described. - Four Rationalizations rows, including "this backtest looks clean, the numbers are all plausible" -- two defects produced plausible, internally consistent output for the life of the project while 2,712 tests passed. Two things deliberately NOT added, with the reasoning recorded so the next proposal to add them can see why: a check on the pending-lifespan invariant (it raises rather than logs, so a violation ends the run and is unobservable from a proposer), and a stationarity rejection threshold (ZEC under pullback_continuation is the only pair in the study with no losing complete year, at 0.875 gross -- any such gate passes that and rejects the one thing that made money gross). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 17, 2026
Closed
C3 — trail_stop_atr, roll_to_break_even and scale_out are implemented, tested, and never called
#442
Closed
eaitbrahim
added a commit
that referenced
this pull request
Aug 24, 2026
#530) * feat(db): record the stop a tranche was sized against, because the current one ratchets away from it (#520) v12 adds positions.initial_stop, written at tranche open and never rewritten. The break-even arm of exit_policy.next_stop computes its threshold from the trade's ORIGINAL per-unit risk: entry + be_roll_rr * (entry - initial_stop). Live state carried entry_fill (the ledger) and open_stop:<product_id> (the CURRENT, already-ratcheted stop) and nothing else -- so the number that threshold is most sensitive to was simply absent. Substituting the current stop is not an approximation, it is a DIFFERENT POLICY. The current stop rises on every ratchet, shrinking (entry - stop), so the threshold creeps toward entry and the arm fires earlier each time, drifting further from the measured policy the longer a trade runs. Live and sim would then encode two different break-even rules while appearing to share exit_policy's functions -- the exact failure sharing them was meant to prevent. NULL means UNKNOWN and readers must switch the break-even arm OFF for that tranche rather than guess. Zero would be a real number -- a stop 100% below entry -- and a catastrophic one to compute a threshold from, so the column is nullable and _position_row_to_dict is careful not to let _text_to_dec invent a zero. The trailing arm is unaffected either way: it needs no original risk, so a tranche without one keeps trailing and simply never break-even-rolls. NO BACKFILL, deliberately: the honest value for every pre-v12 tranche is NULL. Idempotent by the v8/v11 PRAGMA table_info guard, because a database stamped at v11 got positions from v4's DDL and CREATE TABLE IF NOT EXISTS never adds a column. DCA passes None and that is legitimate, not a degraded case -- it carries no stop by design, and requiring one would refuse a real tranche. The three schema-version tripwires were bumped 11 -> 12 consciously, which is what they exist to force. Gates: pytest 4559 passed / 3 skipped; ruff check keel tests packages clean; mypy clean across 354 source files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6 * docs(prd): what keel can learn from Jesse, which is not its strategies (#528, #529) Compares keel's Rule ABC against the Jesse framework's Strategy API and its published example strategies (jesse-ai/example-strategies -- jesse.trade is Cloudflare-protected, and reading the source is the better record anyway: what matters is what the framework lets a strategy SAY, not what its strategies claim to earn). The conclusion is deliberately narrow. keel is cost-bound, not signal-bound -- round-trip friction ~2.5%, no shipped rule family net-positive, turtle_breakout negative on all 24 assets measured -- so porting DUAL_THRUST, KDJ, Bollinger and the rest is the known dead end. A rule negative on 24 assets is not fixed by a 25th signal. What Jesse's API can express that keel's cannot: conditional entry at a chosen price, a per-bar update_position hook, pyramiding, partial exits, fill-event hooks, per-strategy state, and a declared hyperparameter space. Four of those are ALREADY keel's open issues (#333, #447, #502) -- an external framework independently re-deriving the same gaps is corroboration of the roadmap, not an addition to it, and the PRD explicitly declines to re-file them. Two things are genuinely new, and are filed: - #528: Jesse declares each strategy's parameter space. keel already consumes n_trials in research/deflate.py and reasons about the trials budget in source comments, but the number itself is hand-recorded. Declaring the space makes the input to keel's own overfitting correction derivable rather than remembered. Explicit non-goal: no optimiser -- running one against a cost-bound engine manufactures exactly what the deflated-Sharpe machinery exists to detect. - #529: Jesse strategies name their entry price and assume the fill. keel priced that assumption when #258 corrected the fill model and pullback_continuation's gross PF fell 0.92 -> 0.77. Any foreign strategy ported naively inherits an optimism this project has already measured. The document also records what keel does that Jesse does not -- rails, a detect() with no account access, fill fidelity, trials discipline -- so the comparison reads as a trade rather than a wishlist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #257. Option A of the fork raised on that issue, per your call: match the live path rather
than change money-moving code to accommodate a rule already measured as alpha-deficient.
The divergence
The simulator held a
Setupuntil a later bar's range touched its entry, then filled atthat level. Production does not do that:
Live never rests an order at
Setup.entryand never waits for price to come to it. Whenengine.evaluateemits a signal and the rails pass, the executor buys at market that cycle.The old model therefore gave the backtest two things the live box does not have:
chosen level, so unfavourable entries were silently declined.
Both flatter results, and the bias runs opposite to #254's, which suppressed trades.
The change
Entries fill at
candles[i+1].openplus slippage — the first price obtainable once the signalexists, and the earliest fill involving no lookahead.
Setup.entrybecomes informational,exactly as
expected_fillis live; risk is measured from the achieved fill against the setup'sstop, never from the quoted entry.
This subsumes #254. Every pending now resolves on the very next bar, so the state that froze
the detector is unreachable rather than merely handled.
It also removes the wrinkle in #247's fee justification. "Touching entry is marketable,
therefore taker" held for a breakout entry above the market but not for a mean-reversion entry
below it, which touching would make a maker fill. With every entry a market order, taker is
unconditionally correct.
The consequence, named rather than hidden
A rule encoding a confirmation condition in its entry price no longer gets one.
pullback_continuationsetsentry = signal_candle.high + buffer_ticksprecisely to demandfollow-through, and a market fill takes trades it meant to decline.
That is not introduced here — it is what the live box already does, and modelling it faithfully
is the point. Option B (make the executor honour a stop/limit entry) is the alternative and stays
open on #257; it was declined because it changes live order routing to rescue a rule we have
already measured as alpha-deficient.
Tests
TestIntrabarResolutionEntryVsStopdeleted — that ambiguity only arises for an entry seekinga level and is now unreachable. A note in its place says so.
_resolve_orderis still exercisedfor stop-vs-target, which remains real, including on the fill bar.
TestEntryFillsAtNextBarOpen: a setup whose entry is nevertouched now fills anyway, and
detect_calls == 1rather than fix(strategy): re-detect while flat, so an unfilled setup cannot switch the detector off #256's 3 — the number thatdistinguishes the two fill models.
ruff check keel tests packagesclean.Golden regenerated (second time ever; #247 was the first)
#247's reasoning for why
n_tradesheld does not carry over — this is a fill-logic change,so the count was free to move and simply didn't. It held because
turtle_breakoutenters at theclose and 24/7 crypto barely gaps, so both models pick nearly the same price on this corpus. Read
those deltas as "this corpus is insensitive to the fill model", not as "the fill model barely
matters" — the daily BTC baseline never exercises an offset entry. The docstring now says exactly
that, so the small numbers can't be misread later as evidence the change was cosmetic.
Pre-registered prediction for the re-run (in flight)
Recorded before results, so it can be wrong: n rises (every signal becomes a trade, including
the gap-away entries previously declined) and gross PF falls (the declined entries were
disproportionately the unfavourable ones). Largest effect expected on
pullback_continuation,whose entry is offset above the signal bar; smallest on
turtle/rsi, which enter at the close.🤖 Generated with Claude Code