Skip to content

The live executor discards a rule's conditional entry price and routes a market order regardless #260

Description

@eaitbrahim

"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:

order_type="market",
limit_price=None,
expected_fill=intent.entry,

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.
  • Only then revisit The simulator fills at a price live never waits for: entries are market orders in production, resting orders in backtest #257's fill model: with real resting orders, the simulator's original
    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.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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions