You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The rule's own entry price is recorded as expected_fill and then ignored for execution.
For rules that enter at the signal-bar close this is approximately right — turtle_breakout and rsi_meanrev both set entry = close, so a market order on the next cycle lands a hair away.
For a rule that encodes a condition in its entry price it is not. pullback_continuation sets entry = signal_candle.high + buffer_ticks specifically to demand follow-through: if price never
takes out the signal bar's high, the rule wants no trade. Production takes it anyway.
The measurement
#258 made the simulator faithful to this behaviour, which quantifies what live has been doing. pullback_continuation at shipped defaults across 24 assets, touch-fill vs market-fill:
median trade count 58 -> 124 (more than doubled)
median gross PF 0.9219 -> 0.7736
assets clearing n>=100 1 -> 14
The doubling is the count of trades the live box would take that the rule intended to decline,
and the profit-factor collapse is their quality. The offset entry was not decoration; it was
filtering, and production removes the filter.
Why this is logged rather than fixed
Deliberately declined for now, and the reasoning should survive: fixing it means changing
money-moving order routing, and the only rule that currently exercises the gap
(pullback_continuation) is independently measured as alpha-deficient — median gross PF 0.7736
under the faithful engine. Upgrading live execution to rescue a dead strategy is a bad trade.
Why it still matters
The landmine is not pullback_continuation. It is the next rule. Any future strategy that
expresses a condition through its entry price — a limit resting at a support band, a stop capturing
a volatility breakout — will be silently converted into a market order at signal time, and will
behave in production nothing like it behaved in research. Nothing warns; the order row even
records the intended price next to the one that was actually used.
Remediation, when a price-conditional rule is actually on the table
Route Setup.entry as a genuine resting limit (entry below market) or stop (entry above
market) order rather than short-circuiting to market.
Track and reconcile that resting order across cycles — execution/reconcile.py already owns
order lifecycle, and an unfilled entry order is a new state it does not currently model.
Decide a cancel/replace policy, since engine.evaluate re-derives the setup every cycle and
would otherwise stack orders.
Minimum viable mitigation short of all that: have the executor log loudly when intent.entry differs materially from the market price at routing time, so the override is
visible rather than silent — the same principle as #247 printing the fee rate.
"Option B" from #257, logged as agreed after Option A shipped in #258. Not scheduled — recorded so
it cannot be rediscovered the hard way.
Component:
keel/execution/executor.py(_order_row), live routing path.Found: while matching the simulator to production under #257.
The defect
Every signal is routed as an immediate market order:
The rule's own entry price is recorded as
expected_filland then ignored for execution.For rules that enter at the signal-bar close this is approximately right —
turtle_breakoutandrsi_meanrevboth setentry = close, so a market order on the next cycle lands a hair away.For a rule that encodes a condition in its entry price it is not.
pullback_continuationsetsentry = signal_candle.high + buffer_ticksspecifically to demand follow-through: if price nevertakes out the signal bar's high, the rule wants no trade. Production takes it anyway.
The measurement
#258 made the simulator faithful to this behaviour, which quantifies what live has been doing.
pullback_continuationat shipped defaults across 24 assets, touch-fill vs market-fill:The doubling is the count of trades the live box would take that the rule intended to decline,
and the profit-factor collapse is their quality. The offset entry was not decoration; it was
filtering, and production removes the filter.
Why this is logged rather than fixed
Deliberately declined for now, and the reasoning should survive: fixing it means changing
money-moving order routing, and the only rule that currently exercises the gap
(
pullback_continuation) is independently measured as alpha-deficient — median gross PF 0.7736under the faithful engine. Upgrading live execution to rescue a dead strategy is a bad trade.
Why it still matters
The landmine is not
pullback_continuation. It is the next rule. Any future strategy thatexpresses a condition through its entry price — a limit resting at a support band, a stop capturing
a volatility breakout — will be silently converted into a market order at signal time, and will
behave in production nothing like it behaved in research. Nothing warns; the order row even
records the intended price next to the one that was actually used.
Remediation, when a price-conditional rule is actually on the table
Setup.entryas a genuine resting limit (entry below market) or stop (entry abovemarket) order rather than short-circuiting to market.
execution/reconcile.pyalready ownsorder lifecycle, and an unfilled entry order is a new state it does not currently model.
engine.evaluatere-derives the setup every cycle andwould otherwise stack orders.
touch-fill semantics become approximately correct again for those rules, and the two engines
would need to agree per-rule rather than globally.
Minimum viable mitigation short of all that: have the executor log loudly when
intent.entrydiffers materially from the market price at routing time, so the override isvisible rather than silent — the same principle as #247 printing the fee rate.