import datetime as dt import sys import threading import unittest from pathlib import Path from types import SimpleNamespace ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "src")) from adapters.xttrader_live import ( IdempotencyConflict, LiveOrderBlocked, NotConnectedError, ReconnectFailed, XtTraderLiveAdapter, map_order_status, ) class Constants: STOCK_BUY = 23 STOCK_SELL = 24 FIX_PRICE = 11 class FakeTrader: def __init__(self, connect_code=0, orders=None): self.connect_code = connect_code self.callback = None self.started = False self.stopped = False self.order_calls = [] self.cancel_calls = [] self.orders = list(orders or []) def register_callback(self, callback): self.callback = callback def start(self): self.started = True def connect(self): return self.connect_code def subscribe(self, account): self.account = account return 0 def stop(self): self.stopped = True def query_stock_asset(self, account): del account return SimpleNamespace( account_id="test", cash=10_000, market_value=20_000, total_asset=30_000 ) def query_stock_orders(self, account, cancelable_only=False): del account, cancelable_only return list(self.orders) def query_stock_positions(self, account): del account return [ SimpleNamespace( account_id="test", stock_code="600000.SH", volume=1000, can_use_volume=700, market_value=10_000, open_price=10, ) ] def query_stock_trades(self, account): del account return [] def order_stock_async(self, *args): self.order_calls.append(args) return 101 def cancel_order_stock(self, account, order_id): self.cancel_calls.append((account, order_id)) return 0 def passing_live_decision(**overrides): decision = { "authorization_id": "auth-test-001", "snapshot_id": "snapshot-test-001", "as_of": dt.datetime.now(dt.timezone.utc).isoformat(), "reconciliation_safe": True, "account_allowed": True, "within_notional_limit": True, "within_order_rate_limit": True, "operator_approved": True, "kill_switch_ready": True, "compliance_approved": True, } decision.update(overrides) return decision def adapter_for(trader, allow=False, live_guard=None): if allow is True and live_guard is None: live_guard = lambda intent: passing_live_decision() return XtTraderLiveAdapter( trader_factory=lambda: trader, account=SimpleNamespace(account_id="test"), constants=Constants, allow_live_orders=allow, live_guard=live_guard, ) class XtTraderLiveAdapterTest(unittest.TestCase): def test_real_status_mapping_uses_domain_spelling(self): self.assertEqual(map_order_status(48), "NEW") self.assertEqual(map_order_status(50), "ACCEPTED") self.assertEqual(map_order_status(51), "CANCEL_PENDING") self.assertEqual(map_order_status(54), "CANCELLED") self.assertEqual(map_order_status(55), "PARTIALLY_FILLED") self.assertEqual(map_order_status(56), "FILLED") self.assertEqual(map_order_status(57), "REJECTED") self.assertEqual(map_order_status(999), "UNKNOWN") def test_default_is_dry_run_and_idempotent_without_broker_call(self): trader = FakeTrader() adapter = adapter_for(trader) first = adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, ) second = adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, ) self.assertEqual(first, second) self.assertEqual(first["state"], "DRY_RUN") self.assertEqual(trader.order_calls, []) def test_allow_live_orders_requires_a_real_bool(self): trader = FakeTrader() for value in ("False", "true", 1, None): with self.subTest(value=value): with self.assertRaisesRegex(TypeError, "must be a bool"): adapter_for(trader, allow=value) self.assertEqual(trader.order_calls, []) def test_live_mode_requires_callable_guard_but_dry_run_does_not(self): trader = FakeTrader() dry_run = XtTraderLiveAdapter( trader_factory=lambda: trader, account=SimpleNamespace(account_id="test"), constants=Constants, ) self.assertFalse(dry_run.allow_live_orders) with self.assertRaisesRegex(TypeError, "requires a callable live_guard"): XtTraderLiveAdapter( trader_factory=lambda: trader, account=SimpleNamespace(account_id="test"), constants=Constants, allow_live_orders=True, ) with self.assertRaisesRegex(TypeError, "live_guard must be callable"): XtTraderLiveAdapter( trader_factory=lambda: trader, account=SimpleNamespace(account_id="test"), constants=Constants, allow_live_orders=True, live_guard="allow", ) def test_idempotency_key_conflict_is_rejected(self): adapter = adapter_for(FakeTrader()) adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, ) with self.assertRaises(IdempotencyConflict): adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=200, limit_price=10.5, ) def test_non_finite_live_price_is_rejected_before_broker_call(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() for value in (float("nan"), float("inf"), float("-inf")): with self.subTest(value=value): with self.assertRaisesRegex(ValueError, "finite and positive"): adapter.submit_order( client_order_id="bad-price", symbol="600000.SH", side="BUY", quantity=100, limit_price=value, confirm_live=True, ) self.assertEqual(trader.order_calls, []) def test_limit_price_rejects_bool_non_real_and_non_positive(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() for value in (True, False, "10.5", None, 10 + 0j, 0, -1): with self.subTest(value=value): with self.assertRaisesRegex(ValueError, "limit_price"): adapter.submit_order( client_order_id="strict-price", symbol="600000.SH", side="SELL", quantity=100, limit_price=value, confirm_live=True, ) self.assertEqual(trader.order_calls, []) def test_submit_confirm_live_requires_a_real_bool(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() for value in ("False", "true", 1, None): with self.subTest(value=value): with self.assertRaisesRegex(TypeError, "must be a bool"): adapter.submit_order( client_order_id="strict-confirm", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=value, ) self.assertEqual(trader.order_calls, []) def test_quantity_rejects_bool_fraction_float_and_non_positive(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() for value in (True, False, 100.0, 100.5, "100", None, 0, -100): with self.subTest(value=value): with self.assertRaisesRegex(ValueError, "positive integer"): adapter.submit_order( client_order_id="strict-quantity", symbol="600000.SH", side="BUY", quantity=value, limit_price=10.5, confirm_live=True, ) self.assertEqual(trader.order_calls, []) def test_real_order_requires_double_opt_in_and_remark_round_trip(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() with self.assertRaises(LiveOrderBlocked): adapter.submit_order( client_order_id="12345678901234567890", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, ) result = adapter.submit_order( client_order_id="12345678901234567890", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) self.assertEqual(result["state"], "NEW") self.assertEqual(len(trader.order_calls), 1) remark = trader.order_calls[0][-1] self.assertEqual(remark, "q60:12345678901234567890") self.assertLessEqual(len(remark), 24) self.assertEqual( result["live_policy"]["authorization_id"], "auth-test-001" ) def test_live_guard_receives_normalized_submit_and_cancel_intents(self): intents = [] def guard(intent): intents.append(intent) return passing_live_decision( authorization_id=f"auth-{len(intents)}" ) trader = FakeTrader() adapter = adapter_for(trader, allow=True, live_guard=guard) adapter.connect() result = adapter.submit_order( client_order_id="guard-intent", symbol="600000.SH", side="buy", quantity=100, limit_price=10.5, confirm_live=True, ) adapter.cancel_order(88, confirm_live=True) self.assertEqual( intents, [ { "action": "SUBMIT_ORDER", "account_id": "test", "client_order_id": "guard-intent", "symbol": "600000.XSHG", "side": "BUY", "quantity": 100, "limit_price": 10.5, "notional": 1050.0, "strategy_name": "quant60", }, { "action": "CANCEL_ORDER", "account_id": "test", "broker_order_id": 88, "strategy_name": "quant60", }, ], ) self.assertEqual(result["live_policy"]["authorization_id"], "auth-1") self.assertEqual(len(trader.order_calls), 1) self.assertEqual(len(trader.cancel_calls), 1) audit = adapter.live_authorization_audit self.assertEqual(len(audit), 2) self.assertEqual(audit[0]["decision"]["account_id"], "test") self.assertEqual(audit[1]["intent"]["action"], "CANCEL_ORDER") self.assertEqual(audit[1]["outcome"], "BROKER_ACCEPTED") def test_live_guard_is_bound_to_a_non_empty_account_id(self): trader = FakeTrader() calls = [] adapter = XtTraderLiveAdapter( trader_factory=lambda: trader, account=SimpleNamespace(account_id=" "), constants=Constants, allow_live_orders=True, live_guard=lambda intent: ( calls.append(intent) or passing_live_decision() ), ) with self.assertRaisesRegex(NotConnectedError, "account_id"): adapter.connect() self.assertEqual(calls, []) self.assertEqual(trader.order_calls, []) def test_live_guard_missing_false_non_bool_or_exception_blocks_submit(self): def raising_guard(intent): del intent raise RuntimeError("policy backend unavailable") cases = [ ( "missing", lambda intent: { key: value for key, value in passing_live_decision().items() if key != "compliance_approved" }, ), ( "false", lambda intent: passing_live_decision( reconciliation_safe=False ), ), ( "non-bool", lambda intent: passing_live_decision(account_allowed=1), ), ( "empty-id", lambda intent: passing_live_decision( authorization_id=" " ), ), ("exception", raising_guard), ] for name, guard in cases: with self.subTest(name=name): trader = FakeTrader() adapter = adapter_for( trader, allow=True, live_guard=guard ) adapter.connect() with self.assertRaises(LiveOrderBlocked): adapter.submit_order( client_order_id=f"blocked-{name}", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) self.assertEqual(trader.order_calls, []) def test_live_guard_rejects_stale_naive_and_future_decisions(self): now = dt.datetime.now(dt.timezone.utc) cases = { "stale": (now - dt.timedelta(seconds=61)).isoformat(), "naive": now.replace(tzinfo=None).isoformat(), "future": (now + dt.timedelta(seconds=6)).isoformat(), } for name, as_of in cases.items(): with self.subTest(name=name): trader = FakeTrader() adapter = adapter_for( trader, allow=True, live_guard=lambda intent, value=as_of: ( passing_live_decision(as_of=value) ), ) adapter.connect() with self.assertRaises(LiveOrderBlocked): adapter.submit_order( client_order_id=f"clock-{name}", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) self.assertEqual(trader.order_calls, []) def test_live_guard_failure_blocks_cancel_before_broker(self): trader = FakeTrader() adapter = adapter_for( trader, allow=True, live_guard=lambda intent: passing_live_decision( kill_switch_ready=False ), ) adapter.connect() with self.assertRaisesRegex( LiveOrderBlocked, "kill_switch_ready" ): adapter.cancel_order(88, confirm_live=True) self.assertEqual(trader.cancel_calls, []) def test_concurrent_same_client_id_submits_once(self): class BlockingTrader(FakeTrader): def __init__(self): super().__init__() self.submit_entered = threading.Event() self.release_submit = threading.Event() def order_stock_async(self, *args): self.order_calls.append(args) self.submit_entered.set() if not self.release_submit.wait(2): raise AssertionError("test did not release broker submit") return 101 trader = BlockingTrader() adapter = adapter_for(trader, allow=True) adapter.connect() results = [] failures = [] def submit(): try: results.append( adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) ) except BaseException as exc: failures.append(exc) first = threading.Thread(target=submit) first.start() self.assertTrue(trader.submit_entered.wait(1)) # The broker boundary is inside the idempotency critical section. acquired = adapter._lock.acquire(blocking=False) if acquired: adapter._lock.release() self.assertFalse(acquired) second = threading.Thread(target=submit) second.start() trader.release_submit.set() first.join(2) second.join(2) self.assertFalse(first.is_alive()) self.assertFalse(second.is_alive()) self.assertEqual(failures, []) self.assertEqual(len(trader.order_calls), 1) self.assertEqual(len(results), 2) self.assertEqual(results[0], results[1]) def test_synchronous_order_error_matches_pre_submit_reservation(self): class SynchronousErrorTrader(FakeTrader): def order_stock_async(self, *args): self.order_calls.append(args) self.callback.on_order_error( SimpleNamespace( order_id=0, order_remark=args[-1], error_id=42, error_msg="synchronous rejection", ) ) return 101 trader = SynchronousErrorTrader() adapter = adapter_for(trader, allow=True) adapter.connect() result = adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) self.assertEqual(result["state"], "REJECTED") self.assertEqual(result["error_id"], 42) self.assertEqual(len(trader.order_calls), 1) def test_synchronous_order_error_by_seq_is_drained_from_orphans(self): class SynchronousErrorTrader(FakeTrader): def order_stock_async(self, *args): self.order_calls.append(args) self.callback.on_order_error( SimpleNamespace( order_id=None, order_remark="", seq=101, error_id=43, error_msg="fast seq rejection", ) ) return 101 trader = SynchronousErrorTrader() adapter = adapter_for(trader, allow=True) adapter.connect() result = adapter.submit_order( client_order_id="decision-002", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) self.assertEqual(result["state"], "REJECTED") self.assertEqual(result["error_id"], 43) self.assertEqual(adapter._orphan_order_errors, []) def test_malformed_async_seq_never_leaves_submitting_reservation(self): class MalformedSeqTrader(FakeTrader): def __init__(self, seq): super().__init__() self.seq = seq def order_stock_async(self, *args): self.order_calls.append(args) return self.seq for index, seq in enumerate(("bad", 1.5, True, object())): with self.subTest(seq=repr(seq)): trader = MalformedSeqTrader(seq) adapter = adapter_for(trader, allow=True) adapter.connect() client_id = f"bad-seq-{index}" with self.assertRaisesRegex( Exception, "malformed async request sequence" ): adapter.submit_order( client_order_id=client_id, symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) record = adapter.journal.get(client_id) self.assertEqual(record["state"], "UNKNOWN") self.assertNotEqual(record["state"], "SUBMITTING") self.assertEqual(len(trader.order_calls), 1) def test_query_normalizes_position_and_disconnect_fails_fast(self): trader = FakeTrader() adapter = adapter_for(trader) adapter.connect() position = adapter.query_positions()[0] self.assertEqual(position["sellable"], 700) self.assertEqual(position["symbol"], "600000.XSHG") trader.callback.on_disconnected() with self.assertRaises(NotConnectedError): adapter.query_orders() def test_connect_recovers_idempotency_from_broker_remark(self): broker_order = SimpleNamespace( account_id="test", order_id=88, order_sysid="sys-88", order_remark="q60:decision-001", stock_code="600000.SH", order_type=23, order_volume=100, traded_volume=0, price=10.5, traded_price=0, order_status=50, status_msg="", ) trader = FakeTrader(orders=[broker_order]) adapter = adapter_for(trader, allow=True) adapter.connect() recovered = adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) self.assertEqual(recovered["broker_order_id"], 88) self.assertEqual(recovered["state"], "ACCEPTED") self.assertEqual(trader.order_calls, []) def test_broker_payload_conflict_for_same_remark_fails_closed(self): orders = [ SimpleNamespace( account_id="test", order_id=88 + index, order_sysid="sys", order_remark="q60:decision-001", stock_code="600000.SH", order_type=23, order_volume=quantity, traded_volume=0, price=10.5, traded_price=0, order_status=50, status_msg="", ) for index, quantity in enumerate((100, 200)) ] adapter = adapter_for(FakeTrader(orders=orders), allow=True) with self.assertRaises(IdempotencyConflict): adapter.connect() self.assertNotEqual(adapter.state, "READY") def test_order_error_updates_matching_journal_to_rejected(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() adapter.submit_order( client_order_id="decision-001", symbol="600000.SH", side="BUY", quantity=100, limit_price=10.5, confirm_live=True, ) trader.callback.on_order_error( SimpleNamespace( order_id=999, order_remark="q60:decision-001", error_id=42, error_msg="rejected", ) ) record = adapter.journal.get("decision-001") self.assertEqual(record["state"], "REJECTED") self.assertEqual(record["error_id"], 42) def test_cancel_none_is_not_reported_as_success(self): trader = FakeTrader() trader.cancel_order_stock = lambda account, order_id: None adapter = adapter_for(trader, allow=True) adapter.connect() with self.assertRaisesRegex(Exception, "cancel failed"): adapter.cancel_order(88, confirm_live=True) def test_cancel_confirm_live_requires_a_real_bool(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() for value in ("False", "true", 1, None): with self.subTest(value=value): with self.assertRaisesRegex(TypeError, "must be a bool"): adapter.cancel_order(88, confirm_live=value) self.assertEqual(trader.cancel_calls, []) def test_cancel_order_id_requires_exact_positive_integer(self): trader = FakeTrader() adapter = adapter_for(trader, allow=True) adapter.connect() for value in (True, False, 88.0, 88.9, "88", None, 0, -1): with self.subTest(value=value): with self.assertRaisesRegex( ValueError, "positive integer" ): adapter.cancel_order(value, confirm_live=True) self.assertEqual(trader.cancel_calls, []) def test_stale_order_callback_cannot_regress_filled_terminal_state(self): filled = SimpleNamespace( account_id="test", order_id=88, order_sysid="sys-88", order_remark="q60:decision-001", stock_code="600000.SH", order_type=23, order_volume=100, traded_volume=100, price=10.5, traded_price=10.5, order_status=56, status_msg="filled", ) stale = SimpleNamespace( account_id="test", order_id=88, order_sysid="sys-88", order_remark="q60:decision-001", stock_code="600000.SH", order_type=23, order_volume=100, traded_volume=0, price=10.5, traded_price=0, order_status=50, status_msg="stale accepted", ) trader = FakeTrader(orders=[filled]) adapter = adapter_for(trader) adapter.connect() # An exact duplicate is harmless and idempotent. trader.callback.on_stock_order(filled) with self.assertRaisesRegex( IdempotencyConflict, "cumulative fill regressed" ): trader.callback.on_stock_order(stale) record = adapter.journal.get("decision-001") self.assertEqual(adapter.state, "DEGRADED") self.assertEqual(record["state"], "FILLED") self.assertEqual(record["filled_quantity"], 100) self.assertEqual(adapter._orders[88]["state"], "FILLED") def test_configured_account_anchors_every_broker_query(self): class WrongAccountTrader(FakeTrader): def query_stock_asset(self, account): del account return SimpleNamespace( account_id="wrong", cash=10_000, market_value=20_000, total_asset=30_000, ) trader = WrongAccountTrader() adapter = adapter_for(trader) with self.assertRaisesRegex( NotConnectedError, "configured account", ): adapter.connect() self.assertEqual(adapter.state, "FAILED") self.assertTrue(trader.stopped) def test_trade_query_preserves_account_side_and_stable_trade_id(self): class TradeTrader(FakeTrader): def query_stock_trades(self, account): del account return [ SimpleNamespace( account_id="test", traded_id="trade-1", order_id=88, stock_code="600000.SH", order_type=23, traded_volume=100, traded_price=10.5, traded_amount=1050, ) ] adapter = adapter_for(TradeTrader()) adapter.connect() self.assertEqual( adapter.query_trades(), [ { "account_id": "test", "trade_id": "trade-1", "broker_order_id": 88, "symbol": "600000.XSHG", "side": "BUY", "quantity": 100, "price": 10.5, "amount": 1050.0, } ], ) def test_reconnect_uses_fresh_instance_and_exhausts_bounded_attempts(self): created = [] def factory(): trader = FakeTrader(connect_code=9) created.append(trader) return trader adapter = XtTraderLiveAdapter( trader_factory=factory, account=SimpleNamespace(account_id="test"), constants=Constants, ) with self.assertRaises(ReconnectFailed): adapter.reconnect(attempts=2) self.assertEqual(len(created), 2) self.assertEqual(adapter.state, "FAILED") def test_bj_is_explicitly_outside_v1_live_adapter(self): adapter = adapter_for(FakeTrader()) with self.assertRaises(ValueError): adapter.submit_order( client_order_id="bj", symbol="430001.BJ", side="BUY", quantity=100, limit_price=10, ) if __name__ == "__main__": unittest.main()