Files
momentum-system/tests/test_tradier_client.py
T
momentum devandClaude Fable 5 a2de50479a 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>
2026-07-28 09:11:00 -05:00

274 lines
12 KiB
Python

"""Tradier mirror translation tests — pure functions plus fake-network client
paths. No DB, no network."""
import unittest
from unittest.mock import patch
from tradier_broker.client import (
TradierClient,
build_order_form,
translate_balances,
translate_order,
translate_position,
)
from tradier_broker.settings import TradierSettings
SETTINGS = TradierSettings(access_token="t", account_id="VA000")
class TranslateOrderTests(unittest.TestCase):
def test_filled_equity_order_maps_alpaca_keys(self):
o = translate_order({
"id": 123456, "type": "limit", "symbol": "AAPL", "side": "buy",
"quantity": 10, "status": "filled", "duration": "day",
"price": 175.0, "avg_fill_price": 128.25, "exec_quantity": 10,
"create_date": "2026-07-25T15:25:47.000Z",
"transaction_date": "2026-07-25T15:25:50.000Z",
"class": "equity", "tag": "abc-123",
})
self.assertEqual(o["id"], "123456")
self.assertEqual(o["client_order_id"], "abc-123")
self.assertEqual(o["qty"], "10")
self.assertEqual(o["filled_qty"], "10")
self.assertEqual(o["filled_avg_price"], "128.25")
self.assertEqual(o["limit_price"], "175.0")
self.assertEqual(o["status"], "filled")
self.assertEqual(o["submitted_at"], "2026-07-25T15:25:47.000Z")
self.assertEqual(o["filled_at"], "2026-07-25T15:25:50.000Z")
def test_status_mapping(self):
for tradier, alpaca in [("open", "new"), ("pending", "pending_new"),
("error", "rejected"), ("submitted", "accepted"),
("canceled", "canceled")]:
self.assertEqual(translate_order({"status": tradier})["status"], alpaca)
def test_unfilled_has_no_filled_at(self):
o = translate_order({"status": "open", "transaction_date": "x"})
self.assertIsNone(o["filled_at"])
def test_otoco_legs_become_alpaca_legs(self):
o = translate_order({
"id": 1, "class": "otoco", "status": "filled", "symbol": "PN",
"leg": [
{"id": 2, "type": "limit", "side": "sell", "status": "open",
"symbol": "PN", "quantity": 5, "price": 9.0},
{"id": 3, "type": "stop", "side": "sell", "status": "open",
"symbol": "PN", "quantity": 5, "stop_price": 7.0},
],
})
self.assertEqual(o["order_class"], "bracket")
self.assertEqual(len(o["legs"]), 2)
self.assertEqual(o["legs"][1]["stop_price"], "7.0")
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,
"status": "open"}})
self.assertEqual(len(o["legs"]), 1)
self.assertEqual(o["legs"][0]["stop_price"], "4.5")
class TranslatePositionTests(unittest.TestCase):
def test_marked_position(self):
p = translate_position(
{"symbol": "ADVB", "quantity": 100, "cost_basis": 500.0}, 6.25)
self.assertEqual(p["qty"], "100")
self.assertEqual(p["avg_entry_price"], "5.0")
self.assertEqual(p["market_value"], "625.0")
self.assertEqual(p["unrealized_pl"], "125.0")
self.assertEqual(p["side"], "long")
def test_unmarked_position_still_reports(self):
p = translate_position({"symbol": "X", "quantity": 10, "cost_basis": 50}, None)
self.assertIsNone(p["current_price"])
self.assertIsNone(p["unrealized_pl"])
self.assertEqual(p["avg_entry_price"], "5.0")
class TranslateBalancesTests(unittest.TestCase):
def test_margin_account(self):
a = translate_balances({"total_equity": 100000.5, "total_cash": 40000,
"margin": {"stock_buying_power": 80000}})
self.assertEqual(a["equity"], "100000.5")
self.assertEqual(a["buying_power"], "80000")
def test_cash_account_fallback(self):
a = translate_balances({"total_equity": 100, "total_cash": 100,
"cash": {"cash_available": 90}})
self.assertEqual(a["buying_power"], "90")
class BuildOrderFormTests(unittest.TestCase):
def test_bracket_becomes_otoco(self):
f = build_order_form("PN", 50, "buy", "limit", "day",
limit_price=8.5, stop_price=None,
stop_loss_price=7.9, take_profit_price=10.2,
client_order_id="cid-1")
self.assertEqual(f["class"], "otoco")
self.assertEqual(f["type[0]"], "limit")
self.assertEqual(f["price[0]"], "8.5")
self.assertEqual(f["side[1]"], "sell")
self.assertEqual(f["price[1]"], "10.2")
self.assertEqual(f["type[2]"], "stop")
self.assertEqual(f["stop[2]"], "7.9")
self.assertEqual(f["tag"], "cid-1")
# OCO legs must share symbol and differ in type (Tradier hard rule)
self.assertEqual(f["symbol[1]"], f["symbol[2]"])
self.assertNotEqual(f["type[1]"], f["type[2]"])
def test_stop_only_becomes_oto(self):
f = build_order_form("PN", 50, "buy", "limit", "day",
limit_price=8.5, stop_price=None,
stop_loss_price=7.9, take_profit_price=None,
client_order_id=None)
self.assertEqual(f["class"], "oto")
self.assertEqual(f["type[1]"], "stop")
self.assertEqual(f["stop[1]"], "7.9")
def test_plain_stop_sell_gtc(self):
# exit_manager's standalone protective stop
f = build_order_form("PN", 50, "sell", "stop", "gtc",
limit_price=None, stop_price=7.5,
stop_loss_price=None, take_profit_price=None,
client_order_id=None)
self.assertEqual(f["class"], "equity")
self.assertEqual(f["type"], "stop")
self.assertEqual(f["stop"], "7.5")
self.assertEqual(f["duration"], "gtc")
def test_plain_market(self):
f = build_order_form("PN", 50, "sell", "market", "day",
None, None, None, None, None)
self.assertEqual(f["class"], "equity")
self.assertNotIn("price", f)
class ClientPathTests(unittest.TestCase):
"""Fake _request — verify filtering/normalization the live loop relies on."""
def _client_with_orders(self, orders_node):
client = TradierClient(SETTINGS)
patcher = patch.object(
client, "_request",
side_effect=lambda method, path, **kw: (
{"orders": orders_node}
if kw.get("params", {}).get("page", 0) == 1 or orders_node in (None, "null")
else {"orders": "null"}),
)
patcher.start()
self.addCleanup(patcher.stop)
return client
def test_orders_null_quirk(self):
client = self._client_with_orders("null")
self.assertEqual(client.get_orders(), [])
def test_symbols_and_status_filter(self):
client = self._client_with_orders({"order": [
{"id": 1, "symbol": "PN", "status": "open"},
{"id": 2, "symbol": "PN", "status": "filled"},
{"id": 3, "symbol": "ADVB", "status": "open"},
]})
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"},
{"id": 2, "symbol": "PN", "status": "open", "tag": "other"},
]})
found = client.get_order_by_client_order_id("want")
self.assertIsNotNone(found)
self.assertEqual(found["id"], "1")
self.assertIsNone(client.get_order_by_client_order_id("missing"))
def test_submit_translates_response(self):
client = TradierClient(SETTINGS)
with patch.object(client, "_request",
return_value={"order": {"id": 77, "status": "ok"}}):
resp = client.submit_order(symbol="PN", qty=5, side="buy",
order_type="limit", limit_price=8.5,
stop_loss_price=7.9, take_profit_price=10.0,
client_order_id="cid")
self.assertEqual(resp["id"], "77")
self.assertEqual(resp["status"], "accepted")
self.assertEqual(resp["filled_qty"], "0")
def test_replace_qty_unsupported(self):
client = TradierClient(SETTINGS)
with self.assertRaises(ValueError):
client.replace_order("1", qty=10)
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)