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:
1 parent
bd7183c861
commit
a2de50479a
3 files changed
+146
-4
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user