RPL-1 — the marketable-limit market-replace is the one order path outside the safety chokepoint
status: open · raised 2026-08-29 (external design review finding 5 + maintainer verification) · class: execution-safety (standing-covenant violation) · judgement: YES · effort ~0.5–1 pd · horizon: live on momentum + meanrev every rebalance with an unfilled limit; P low (minutes-wide, BUY-only window)
Plain-language summary for an owner reading one paragraph. When a
touch-priced limit order doesn't fully fill within the fill window,
_resolve_unfilled_limits cancels it and re-submits the remainder as a
market order. That replacement re-checks only the manual/fleet halt — not the
daily-loss breaker or any per-order reject that materialized during the
minutes-long wait — and it submits via broker.submit_market_order
directly. That makes it the single order path in the codebase that bypasses
pipeline.execute_orders, which the shop's own covenant
(memory/execution-safety-layer, 2026-06-06) names THE fail-closed
chokepoint: "never add an order path that bypasses it." A second facet
shares the site: remaining = shares − filled_qty reads filled_qty
BEFORE the cancel; fills landing between the read and the cancel taking
effect over-buy the remainder (check-then-act race).
Mechanism — verified at HEAD (external review, re-verified by the maintainer)
src/thales/execution/daily.py_resolve_unfilled_limits: top-of-function re-check ismanual_halt_engagedonly; the replacement goes out viabroker.submit_market_order(..., client_order_id=…-mr)with noevaluate_safetypass over the remainder batch.filled_qtyis read fromget_order_statusbeforecancel_order; the remainder is sized from that pre-cancel read.- Reachability:
config/settings.yaml(momentum) andconfig/meanrev.yamlboth runorder_type: "marketable_limit"; only vrp ismarket. - Credit where due (also verified): the surrounding code is careful — unknown status → leave the resting limit untouched (fail closed, 2026-07-18 fix); failed cancel → no replacement. The gap is exactly the missing full-gate re-check and the stale read.
Why P is low (and why it still matters)
The trigger needs a >10% intraday move or a fresh reject to materialize inside a ≤5-minute fill window, and only bites a BUY remainder — a de-risking SELL remainder should complete. But the site is live on both equity sleeves on every rebalance, and it is a covenant violation independent of P: the gate's guarantee is only as good as the paths that go through it.
Fix shape (PROPOSE-only — this row does not implement it)
- Route the replacement through the full gate: run
evaluate_safetyover the remainder batch (fresh account snapshot) before any market replace; a HALT verdict cancels-without-replacing (the fail-closed arm already exists for the manual-halt case). - Re-read
filled_qtyAFTER the cancel is confirmed, size the remainder from that.
Test design + kill criterion (pre-registered, negative-controlled)
- Stub broker whose account crosses the −10% daily-loss line between the initial submit and the resolve pass, with an unfilled BUY limit: the market replacement must NOT be submitted (deny-asserted — red today).
- Race test: the stubbed order reports more
filled_qtyon the post-cancel read than the pre-cancel read; the replacement must be sized from the post-cancel value (red today). - De-risking SELL remainder with the breaker tripped: replacement still goes out (the exemption must survive the rewiring).
Kill criterion: all three green post-fix; revert the gate re-route and
test 1 reds; revert the re-read and test 2 reds. If routing through the gate
proves impossible without submitting a second time through
execute_orders's idempotency machinery, that is a finding about the
chokepoint's shape — record it, don't work around it.
Scheduling note
Deliberately NOT built in the 2026-08-29 review-response PR: rewiring order-path plumbing the weekend before a selection day + owner absence is worse risk management than the defect (minutes-wide, BUY-only exposure). First unhurried week.