fix: three Tradier sandbox incident fixes (2026-07-28 reject loop)

Incident: entry-timeout cancels raced lazy sandbox fills -> untracked
positions; naked-stop guard then force-sold but every sell rejected
('more shares than your current long position') for hours.

1. translate_order: Tradier OTOCO/OTO parents carry no top-level
   symbol/side/qty (legs only) — inherit from the entry leg so
   symbol-scoped lookups can see brackets; get_orders symbols filter
   also matches by leg symbols. Without this the stop-leg search read
   a fully-protected position as naked.
2. close_position: cancel the symbol's resting sell orders/legs first —
   Tradier reserves held shares against open sells, so selling without
   releasing them always rejects.
3. cancel_entry (broker-agnostic): after cancelling, re-check the
   broker's final word by client_order_id; journal a fill for any
   executed shares instead of orphaning a real position (2026-07-06
   class, same race exists on Alpaca).

49 tests pass incl. new coverage for all three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-28 09:11:00 -05:00
1 parent bd7183c861
commit a2de50479a
3 files changed
+146 -4

No files matched your search

+66
View File
@@ -63,6 +63,31 @@ class TranslateOrderTests(unittest.TestCase):
self.assertEqual(o["legs"][0]["limit_price"], "9.0")
self.assertEqual(o["legs"][0]["status"], "new")
def test_otoco_parent_inherits_symbol_from_entry_leg(self):
# Tradier bracket parents carry no top-level symbol/side/qty — the
# 2026-07-28 naked-stop false-positive
o = translate_order({
"id": 1, "class": "otoco", "status": "open",
"leg": [
{"id": 2, "symbol": "DAMD", "side": "buy", "quantity": 8000,
"type": "limit", "status": "filled", "exec_quantity": 8000,
"avg_fill_price": 1.96},
{"id": 3, "symbol": "DAMD", "side": "sell", "quantity": 8000,
"type": "limit", "status": "open", "price": 2.5},
{"id": 4, "symbol": "DAMD", "side": "sell", "quantity": 8000,
"type": "stop", "status": "open", "stop_price": 1.8},
],
})
self.assertEqual(o["symbol"], "DAMD")
self.assertEqual(o["side"], "buy")
self.assertEqual(o["qty"], "8000")
self.assertEqual(o["filled_qty"], "8000")
# the stop leg is findable the way exit_manager._stop_leg looks
stops = [leg for leg in o["legs"] if leg["type"] == "stop"
and leg["side"] == "sell" and leg["symbol"] == "DAMD"]
self.assertEqual(len(stops), 1)
self.assertEqual(stops[0]["stop_price"], "1.8")
def test_single_leg_dict_normalized(self):
o = translate_order({"class": "oto", "status": "open",
"leg": {"id": 9, "type": "stop", "stop": 4.5,
@@ -175,6 +200,17 @@ class ClientPathTests(unittest.TestCase):
open_pn = client.get_orders(status="open", symbols=["PN"])
self.assertEqual([o["id"] for o in open_pn], ["1"])
def test_symbols_filter_matches_bracket_by_legs(self):
client = self._client_with_orders({"order": [
{"id": 1, "class": "otoco", "status": "open",
"leg": [{"id": 2, "symbol": "DAMD", "side": "buy", "status": "filled"},
{"id": 3, "symbol": "DAMD", "side": "sell", "type": "stop",
"status": "open", "stop_price": 1.8}]},
{"id": 9, "symbol": "MUZ", "status": "open"},
]})
got = client.get_orders(status="all", symbols=["DAMD"])
self.assertEqual([o["id"] for o in got], ["1"])
def test_lookup_by_tag(self):
client = self._client_with_orders({"order": [
{"id": 1, "symbol": "PN", "status": "open", "tag": "want"},
@@ -205,3 +241,33 @@ class ClientPathTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class CancelRacedFillTests(unittest.TestCase):
"""cancel_entry must journal a fill the cancel raced (2026-07-28 incident)."""
def _executor(self, found_order):
from unittest.mock import MagicMock
from alpaca_paper.execution import AlpacaPaperExecutor
client = TradierClient(SETTINGS)
client.cancel_order = lambda oid: None
client.get_order_by_client_order_id = lambda cid: found_order
store = MagicMock()
return AlpacaPaperExecutor(store, client=client), store
def test_raced_fill_emits_fill_not_cancel(self):
ex, store = self._executor(
{"filled_qty": "8000", "filled_avg_price": "1.96", "side": "buy"})
res = ex.cancel_entry("cid-1", "36082713", "DAMD", "entry timed out")
self.assertEqual(res.status, "filled")
emitted = [c.args[0].__class__.__name__ for c in store.emit.call_args_list]
self.assertIn("OrderFilledEvent", emitted)
self.assertNotIn("OrderCancelledEvent", emitted)
def test_clean_cancel_still_emits_cancel(self):
ex, store = self._executor({"filled_qty": "0", "status": "canceled"})
res = ex.cancel_entry("cid-1", "36082713", "DAMD", "entry timed out")
self.assertEqual(res.status, "cancelled")
emitted = [c.args[0].__class__.__name__ for c in store.emit.call_args_list]
self.assertIn("OrderCancelledEvent", emitted)
self.assertNotIn("OrderFilledEvent", emitted)