"""Guarded XtTrader live adapter. The adapter is intentionally inert by default. Real submissions require both ``allow_live_orders=True`` at construction and ``confirm_live=True`` on every submit/cancel call. Queries, callback normalization, idempotency and fresh instance reconnects remain available independently. """ from __future__ import annotations import datetime as dt import hashlib import importlib import json import math import numbers import operator import os import re import tempfile import threading from pathlib import Path from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional from quant60.portable_core import convert_symbol class XtTraderAdapterError(RuntimeError): """Base adapter failure.""" class NotConnectedError(XtTraderAdapterError): """Operation requires a ready and subscribed QMT connection.""" class LiveOrderBlocked(XtTraderAdapterError): """Safety gate prevented a real broker mutation.""" class IdempotencyConflict(XtTraderAdapterError): """A client order id was reused for a different payload.""" class ReconnectFailed(XtTraderAdapterError): """Fresh-client reconnect attempts were exhausted.""" STATUS_BY_CODE = { 48: "NEW", # ORDER_UNREPORTED 49: "NEW", # ORDER_WAIT_REPORTING 50: "ACCEPTED", # ORDER_REPORTED 51: "CANCEL_PENDING", # ORDER_REPORTED_CANCEL 52: "CANCEL_PENDING", # ORDER_PARTSUCC_CANCEL 53: "CANCELLED", # ORDER_PART_CANCEL 54: "CANCELLED", 55: "PARTIALLY_FILLED", 56: "FILLED", 57: "REJECTED", 255: "UNKNOWN", } TERMINAL_STATES = { "CANCELLED", "FILLED", "REJECTED", } _BROKER_STATE_TRANSITIONS = { "SUBMITTING": { "NEW", "ACCEPTED", "PARTIALLY_FILLED", "FILLED", "CANCEL_PENDING", "CANCELLED", "REJECTED", "UNKNOWN", }, "NEW": { "NEW", "ACCEPTED", "PARTIALLY_FILLED", "FILLED", "CANCEL_PENDING", "CANCELLED", "REJECTED", "UNKNOWN", }, "ACCEPTED": { "ACCEPTED", "PARTIALLY_FILLED", "FILLED", "CANCEL_PENDING", "CANCELLED", "REJECTED", "UNKNOWN", }, "PARTIALLY_FILLED": { "PARTIALLY_FILLED", "FILLED", "CANCEL_PENDING", "CANCELLED", "UNKNOWN", }, "CANCEL_PENDING": { "CANCEL_PENDING", "PARTIALLY_FILLED", "FILLED", "CANCELLED", "UNKNOWN", }, "UNKNOWN": { "NEW", "ACCEPTED", "PARTIALLY_FILLED", "FILLED", "CANCEL_PENDING", "CANCELLED", "REJECTED", "UNKNOWN", }, "FILLED": {"FILLED"}, "CANCELLED": {"CANCELLED"}, "REJECTED": {"REJECTED"}, } # QMT remarks are commonly limited to 24 ASCII characters. Reserve four for # ``q60:`` so broker round-trips preserve the full idempotency key. _CLIENT_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,20}$") _REMARK_PREFIX = "q60:" _LIVE_POLICY_FLAGS = ( "reconciliation_safe", "account_allowed", "within_notional_limit", "within_order_rate_limit", "operator_approved", "kill_switch_ready", "compliance_approved", ) _MAX_POLICY_AGE_SECONDS = 60.0 _MAX_POLICY_FUTURE_SECONDS = 5.0 def map_order_status(code: Any) -> str: """Map the documented XtQuant order status constants to domain states.""" try: return STATUS_BY_CODE[int(code)] except (KeyError, TypeError, ValueError): return "UNKNOWN" def _field(obj: Any, names: Iterable[str], default: Any = None) -> Any: for name in names: if isinstance(obj, Mapping) and name in obj: value = obj[name] elif hasattr(obj, name): value = getattr(obj, name) else: continue if value is not None: return value return default def _payload_hash(payload: Mapping[str, Any]) -> str: encoded = json.dumps( dict(payload), ensure_ascii=True, separators=(",", ":"), sort_keys=True, ).encode("ascii") return hashlib.sha256(encoded).hexdigest() def _client_id_from_remark(remark: Any) -> Optional[str]: text = str(remark or "") if not text.startswith(_REMARK_PREFIX): return None value = text[len(_REMARK_PREFIX) :] return value or None def _strict_bool(value: Any, name: str) -> bool: if type(value) is not bool: raise TypeError(f"{name} must be a bool, not {type(value).__name__}") return value class IdempotencyJournal: """Thread-safe JSON journal; an omitted path gives an in-memory journal.""" def __init__(self, path: Optional[os.PathLike[str] | str] = None): self.path = Path(path).expanduser().resolve() if path else None self._lock = threading.RLock() self._records: Dict[str, Dict[str, Any]] = {} if self.path and self.path.exists(): with self.path.open("r", encoding="utf-8") as handle: data = json.load(handle) if not isinstance(data, dict): raise ValueError("idempotency journal must contain a JSON object") self._records = data def get(self, client_order_id: str) -> Optional[Dict[str, Any]]: with self._lock: record = self._records.get(client_order_id) return dict(record) if record is not None else None def put(self, client_order_id: str, record: Mapping[str, Any]) -> None: with self._lock: self._records[client_order_id] = dict(record) self._persist() def update(self, client_order_id: str, **updates: Any) -> Dict[str, Any]: with self._lock: record = dict(self._records.get(client_order_id, {})) record.update(updates) self._records[client_order_id] = record self._persist() return dict(record) def find_by_seq(self, seq: Any) -> Optional[tuple[str, Dict[str, Any]]]: with self._lock: for client_id, record in self._records.items(): if record.get("request_seq") == seq: return client_id, dict(record) return None def find_by_broker_order_id( self, broker_order_id: Any ) -> Optional[tuple[str, Dict[str, Any]]]: with self._lock: for client_id, record in self._records.items(): if record.get("broker_order_id") == broker_order_id: return client_id, dict(record) return None def _persist(self) -> None: if self.path is None: return self.path.parent.mkdir(parents=True, exist_ok=True) payload = json.dumps( self._records, ensure_ascii=False, indent=2, sort_keys=True, ) descriptor, temp_name = tempfile.mkstemp( prefix=self.path.name + ".", suffix=".tmp", dir=str(self.path.parent), ) try: with os.fdopen(descriptor, "w", encoding="utf-8") as handle: handle.write(payload) handle.flush() os.fsync(handle.fileno()) os.replace(temp_name, self.path) finally: if os.path.exists(temp_name): os.unlink(temp_name) class _CallbackProxy: def __init__(self, adapter: "XtTraderLiveAdapter"): self.adapter = adapter def on_connected(self) -> None: self.adapter._callback_connected() def on_disconnected(self) -> None: self.adapter._callback_disconnected() def on_account_status(self, status: Any) -> None: self.adapter._account_status = status def on_stock_asset(self, asset: Any) -> None: self.adapter._asset = asset def on_stock_order(self, order: Any) -> None: self.adapter._ingest_order(order) def on_stock_trade(self, trade: Any) -> None: self.adapter._ingest_trade(trade) def on_stock_position(self, position: Any) -> None: code = str(_field(position, ["stock_code"], "")) if code: snapshot = self.adapter._normalize_position(position) self.adapter._positions[snapshot["symbol"]] = snapshot def on_order_error(self, error: Any) -> None: self.adapter._ingest_order_error(error) def on_cancel_error(self, error: Any) -> None: self.adapter._errors.append( { "kind": "cancel", "order_id": _field(error, ["order_id"], None), "error_id": _field(error, ["error_id"], None), "message": str(_field(error, ["error_msg"], "")), } ) def on_order_stock_async_response(self, response: Any) -> None: self.adapter._ingest_submit_response(response) def on_cancel_order_stock_async_response(self, response: Any) -> None: self.adapter._cancel_responses.append( { "order_id": _field(response, ["order_id"], None), "result": _field(response, ["cancel_result"], None), "seq": _field(response, ["seq"], None), } ) def on_smt_appointment_async_response(self, response: Any) -> None: del response class XtTraderLiveAdapter: """Normalized, reconnectable and idempotent XtTrader boundary.""" def __init__( self, *, trader_factory: Callable[[], Any], account: Any, constants: Any, allow_live_orders: bool = False, live_guard: Optional[ Callable[[Mapping[str, Any]], Mapping[str, Any]] ] = None, journal_path: Optional[os.PathLike[str] | str] = None, strategy_name: str = "quant60", ): self.allow_live_orders = _strict_bool( allow_live_orders, "allow_live_orders" ) if live_guard is not None and not callable(live_guard): raise TypeError("live_guard must be callable") if self.allow_live_orders and live_guard is None: raise TypeError( "allow_live_orders=True requires a callable live_guard" ) self.live_guard = live_guard self._trader_factory = trader_factory self.account = account self.constants = constants self.strategy_name = str(strategy_name) self.journal = IdempotencyJournal(journal_path) self._lock = threading.RLock() self._callback_lock = threading.RLock() self._trader: Any = None self._state = "NEW" self._orders: Dict[Any, Dict[str, Any]] = {} self._trades: Dict[Any, Dict[str, Any]] = {} self._positions: Dict[str, Dict[str, Any]] = {} self._asset: Any = None self._account_status: Any = None self._errors: List[Dict[str, Any]] = [] self._cancel_responses: List[Dict[str, Any]] = [] self._orphan_submit_responses: Dict[Any, Any] = {} self._orphan_order_errors: List[Dict[str, Any]] = [] self._live_authorization_audit: List[Dict[str, Any]] = [] self._disconnect_seen = False @classmethod def from_xtquant( cls, *, userdata_path: os.PathLike[str] | str, session_id: int, account_id: str, allow_live_orders: bool = False, live_guard: Optional[ Callable[[Mapping[str, Any]], Mapping[str, Any]] ] = None, journal_path: Optional[os.PathLike[str] | str] = None, strategy_name: str = "quant60", ) -> "XtTraderLiveAdapter": """Create a lazy real-runtime adapter without connecting or ordering.""" _strict_bool(allow_live_orders, "allow_live_orders") if live_guard is not None and not callable(live_guard): raise TypeError("live_guard must be callable") if allow_live_orders and live_guard is None: raise TypeError( "allow_live_orders=True requires a callable live_guard" ) try: xttrader = importlib.import_module("xtquant.xttrader") xttype = importlib.import_module("xtquant.xttype") constants = importlib.import_module("xtquant.xtconstant") except Exception as exc: raise XtTraderAdapterError(f"xtquant import failed: {exc}") from exc path = str(Path(userdata_path).expanduser()) account = xttype.StockAccount(account_id) def factory() -> Any: return xttrader.XtQuantTrader(path, int(session_id)) return cls( trader_factory=factory, account=account, constants=constants, allow_live_orders=allow_live_orders, live_guard=live_guard, journal_path=journal_path, strategy_name=strategy_name, ) @property def state(self) -> str: return self._state @property def errors(self) -> List[Dict[str, Any]]: with self._callback_lock: return [dict(item) for item in self._errors] @property def live_authorization_audit(self) -> List[Dict[str, Any]]: with self._callback_lock: return [ { "intent": dict(item["intent"]), "decision": dict(item["decision"]), "outcome": item["outcome"], } for item in self._live_authorization_audit ] def connect(self) -> None: """Create, start, connect, subscribe and verify a fresh trader.""" with self._lock: if self._state == "READY": return self._connect_fresh() def _connect_fresh(self) -> None: self._dispose_trader() self._state = "CONNECTING" self._disconnect_seen = False trader = self._trader_factory() if trader is None: self._state = "FAILED" raise NotConnectedError("trader_factory returned None") self._trader = trader trader.register_callback(_CallbackProxy(self)) trader.start() connect_code = trader.connect() if connect_code != 0: self._state = "FAILED" self._dispose_trader() raise NotConnectedError(f"XtTrader connect failed with code {connect_code}") subscribe_code = trader.subscribe(self.account) if subscribe_code != 0: self._state = "FAILED" self._dispose_trader() raise NotConnectedError( f"XtTrader account subscribe failed with code {subscribe_code}" ) # Read-only broker truth is the readiness barrier, not merely TCP state. asset = trader.query_stock_asset(self.account) orders = trader.query_stock_orders(self.account) if asset is None or orders is None or self._disconnect_seen: self._state = "FAILED" self._dispose_trader() raise NotConnectedError( "XtTrader connected but account snapshot query failed" ) try: self._require_query_account(asset, "asset") self._asset = asset for order in orders: self._ingest_order(order) except Exception: self._state = "FAILED" self._dispose_trader() raise self._state = "READY" def reconnect(self, attempts: int = 1) -> None: """Reconnect with fresh XtTrader instances or fail before any order.""" if int(attempts) < 1: raise ValueError("attempts must be at least one") last_error: Optional[Exception] = None for _ in range(int(attempts)): try: with self._lock: self._connect_fresh() return except Exception as exc: last_error = exc self._state = "FAILED" raise ReconnectFailed( f"XtTrader reconnect failed after {attempts} fresh attempt(s): {last_error}" ) from last_error def close(self) -> None: with self._lock: self._dispose_trader() self._state = "CLOSED" def _dispose_trader(self) -> None: trader, self._trader = self._trader, None if trader is not None: try: trader.stop() except Exception: pass def _callback_connected(self) -> None: if self._state not in {"READY", "CONNECTING"}: self._state = "CONNECTED_UNVERIFIED" def _callback_disconnected(self) -> None: # Never auto-submit/reconnect from a callback. The caller must invoke # reconnect and pass the account snapshot readiness barrier again. self._disconnect_seen = True self._state = "DISCONNECTED" def _require_ready(self) -> Any: if self._state != "READY" or self._trader is None: raise NotConnectedError( f"XtTrader is not ready (state={self._state}); no broker call made" ) return self._trader def _authorize_live_intent( self, intent: Mapping[str, Any] ) -> Dict[str, Any]: guard = self.live_guard if not callable(guard): raise LiveOrderBlocked( "live mutation blocked: callable live_guard is unavailable" ) normalized_intent = dict(intent) try: # The guard receives a copy so it cannot rewrite the account/action # that the adapter subsequently binds into its audit evidence. decision = guard(dict(normalized_intent)) except Exception as exc: raise LiveOrderBlocked( f"live_guard raised {type(exc).__name__}: {exc}" ) from exc if not isinstance(decision, Mapping): raise LiveOrderBlocked( "live_guard must return a structured mapping" ) account_id = normalized_intent.get("account_id") if not isinstance(account_id, str) or not account_id.strip(): raise LiveOrderBlocked( "live intent account_id must be a non-empty string" ) normalized_intent["account_id"] = account_id.strip() authorization_id = decision.get("authorization_id") snapshot_id = decision.get("snapshot_id") if not isinstance(authorization_id, str) or not authorization_id.strip(): raise LiveOrderBlocked( "live_guard authorization_id must be a non-empty string" ) if not isinstance(snapshot_id, str) or not snapshot_id.strip(): raise LiveOrderBlocked( "live_guard snapshot_id must be a non-empty string" ) raw_as_of = decision.get("as_of") if isinstance(raw_as_of, dt.datetime): as_of = raw_as_of elif isinstance(raw_as_of, str): value = raw_as_of.strip() if value.endswith(("Z", "z")): value = value[:-1] + "+00:00" try: as_of = dt.datetime.fromisoformat(value) except ValueError as exc: raise LiveOrderBlocked( "live_guard as_of must be an ISO-8601 datetime" ) from exc else: raise LiveOrderBlocked( "live_guard as_of must be a timezone-aware datetime" ) if as_of.tzinfo is None or as_of.utcoffset() is None: raise LiveOrderBlocked( "live_guard as_of must be timezone-aware" ) now = dt.datetime.now(dt.timezone.utc) age_seconds = ( now - as_of.astimezone(dt.timezone.utc) ).total_seconds() if age_seconds > _MAX_POLICY_AGE_SECONDS: raise LiveOrderBlocked( "live_guard decision is stale (older than 60 seconds)" ) if age_seconds < -_MAX_POLICY_FUTURE_SECONDS: raise LiveOrderBlocked( "live_guard as_of is more than 5 seconds in the future" ) for field in _LIVE_POLICY_FLAGS: value = decision.get(field) if type(value) is not bool or value is not True: raise LiveOrderBlocked( f"live_guard {field} must be exactly bool True" ) normalized_decision = { "authorization_id": authorization_id.strip(), "snapshot_id": snapshot_id.strip(), "as_of": as_of.astimezone(dt.timezone.utc).isoformat(), "account_id": normalized_intent["account_id"], **{field: True for field in _LIVE_POLICY_FLAGS}, } with self._callback_lock: self._live_authorization_audit.append( { "intent": normalized_intent, "decision": normalized_decision, "outcome": "AUTHORIZED", } ) return normalized_decision def _record_live_authorization_outcome( self, authorization_id: str, outcome: str ) -> None: with self._callback_lock: for item in reversed(self._live_authorization_audit): if ( item["decision"].get("authorization_id") == authorization_id ): item["outcome"] = outcome return raise RuntimeError( f"authorization audit record not found: {authorization_id}" ) def _normalized_account_id(self) -> str: value = _field(self.account, ["account_id"], None) if not isinstance(value, str) or not value.strip(): raise LiveOrderBlocked( "live mutation requires a non-empty account_id" ) return value.strip() def _require_query_account(self, item: Any, kind: str) -> str: expected = str( _field(self.account, ["account_id"], "") or "" ).strip() observed = str(_field(item, ["account_id"], "") or "").strip() if not expected or observed != expected: self._state = "DEGRADED" raise NotConnectedError( f"{kind} account_id does not match the configured account" ) return observed def query_asset(self) -> Dict[str, Any]: trader = self._require_ready() raw = trader.query_stock_asset(self.account) if raw is None: self._state = "DEGRADED" raise NotConnectedError("asset query returned None") self._require_query_account(raw, "asset") self._asset = raw return { "account_id": str(_field(raw, ["account_id"], "")), "cash": float(_field(raw, ["cash"], 0.0) or 0.0), "market_value": float(_field(raw, ["market_value"], 0.0) or 0.0), "total_asset": float(_field(raw, ["total_asset"], 0.0) or 0.0), } def query_positions(self) -> List[Dict[str, Any]]: trader = self._require_ready() raw = trader.query_stock_positions(self.account) if raw is None: self._state = "DEGRADED" raise NotConnectedError("positions query returned None") positions = [self._normalize_position(item) for item in raw] self._positions = {item["symbol"]: item for item in positions} return positions def query_orders(self, cancelable_only: bool = False) -> List[Dict[str, Any]]: trader = self._require_ready() raw = trader.query_stock_orders(self.account, bool(cancelable_only)) if raw is None: self._state = "DEGRADED" raise NotConnectedError("orders query returned None") output = [] for item in raw: output.append(self._ingest_order(item)) return output def query_trades(self) -> List[Dict[str, Any]]: trader = self._require_ready() raw = trader.query_stock_trades(self.account) if raw is None: self._state = "DEGRADED" raise NotConnectedError("trades query returned None") return [self._ingest_trade(item) for item in raw] def _normalize_position(self, item: Any) -> Dict[str, Any]: self._require_query_account(item, "position") raw_symbol = str(_field(item, ["stock_code"], "")) return { "account_id": str(_field(item, ["account_id"], "") or ""), "symbol": convert_symbol(raw_symbol, "canonical") if raw_symbol else "", "volume": int(_field(item, ["volume"], 0) or 0), "sellable": int(_field(item, ["can_use_volume"], 0) or 0), "market_value": float(_field(item, ["market_value"], 0.0) or 0.0), "avg_price": float(_field(item, ["open_price", "avg_price"], 0.0) or 0.0), } def _normalize_order(self, item: Any) -> Dict[str, Any]: self._require_query_account(item, "order") order_type = int(_field(item, ["order_type"], 0) or 0) buy_type = int(getattr(self.constants, "STOCK_BUY", 23)) sell_type = int(getattr(self.constants, "STOCK_SELL", 24)) side = "BUY" if order_type == buy_type else "SELL" if order_type == sell_type else "UNKNOWN" remark = str(_field(item, ["order_remark"], "") or "") raw_symbol = str(_field(item, ["stock_code"], "")) return { "account_id": str(_field(item, ["account_id"], "") or ""), "broker_order_id": _field(item, ["order_id"], None), "broker_system_id": str(_field(item, ["order_sysid"], "") or ""), "client_order_id": _client_id_from_remark(remark), "symbol": convert_symbol(raw_symbol, "canonical") if raw_symbol else "", "side": side, "quantity": int(_field(item, ["order_volume"], 0) or 0), "filled_quantity": int(_field(item, ["traded_volume"], 0) or 0), "limit_price": float(_field(item, ["price"], 0.0) or 0.0), "average_fill_price": float( _field(item, ["traded_price"], 0.0) or 0.0 ), "state": map_order_status(_field(item, ["order_status"], 255)), "status_message": str(_field(item, ["status_msg"], "") or ""), "order_remark": remark, } def _normalize_trade(self, item: Any) -> Dict[str, Any]: self._require_query_account(item, "trade") trade_id = _field(item, ["traded_id", "trade_id"], None) raw_symbol = str(_field(item, ["stock_code"], "")) order_type = int(_field(item, ["order_type"], 0) or 0) buy_type = int(getattr(self.constants, "STOCK_BUY", 23)) sell_type = int(getattr(self.constants, "STOCK_SELL", 24)) side = ( "BUY" if order_type == buy_type else "SELL" if order_type == sell_type else "UNKNOWN" ) return { "account_id": str(_field(item, ["account_id"], "") or ""), "trade_id": trade_id, "broker_order_id": _field(item, ["order_id"], None), "symbol": convert_symbol(raw_symbol, "canonical") if raw_symbol else "", "side": side, "quantity": int(_field(item, ["traded_volume"], 0) or 0), "price": float(_field(item, ["traded_price"], 0.0) or 0.0), "amount": float(_field(item, ["traded_amount"], 0.0) or 0.0), } def _ingest_order(self, raw: Any) -> Dict[str, Any]: order = self._normalize_order(raw) order_id = order["broker_order_id"] quantity = order["quantity"] filled_quantity = order["filled_quantity"] if ( quantity <= 0 or filled_quantity < 0 or filled_quantity > quantity or ( order["state"] == "FILLED" and filled_quantity != quantity ) or ( order["state"] == "PARTIALLY_FILLED" and not 0 < filled_quantity < quantity ) ): self._state = "DEGRADED" raise IdempotencyConflict( f"invalid broker order quantities/state for {order_id}" ) prior_order = ( self._orders.get(order_id) if order_id is not None else None ) client_id = order.get("client_order_id") if prior_order is not None: prior_client_id = prior_order.get("client_order_id") if client_id is None: client_id = prior_client_id order["client_order_id"] = prior_client_id immutable_fields = ( "symbol", "side", "quantity", "limit_price", ) changed = [ field for field in immutable_fields if prior_order.get(field) != order.get(field) ] prior_system_id = prior_order.get("broker_system_id") current_system_id = order.get("broker_system_id") if ( prior_client_id and client_id and prior_client_id != client_id ): changed.append("client_order_id") if ( prior_system_id and current_system_id and prior_system_id != current_system_id ): changed.append("broker_system_id") if changed: self._state = "DEGRADED" raise IdempotencyConflict( f"broker order {order_id} changed immutable " f"identity/payload fields: {sorted(set(changed))}" ) if filled_quantity < prior_order["filled_quantity"]: self._state = "DEGRADED" raise IdempotencyConflict( f"broker order {order_id} cumulative fill regressed " f"from {prior_order['filled_quantity']} " f"to {filled_quantity}" ) allowed = _BROKER_STATE_TRANSITIONS.get( prior_order["state"], set() ) if order["state"] not in allowed: self._state = "DEGRADED" raise IdempotencyConflict( f"broker order {order_id} state regressed illegally " f"from {prior_order['state']} to {order['state']}" ) if order == prior_order: return dict(prior_order) if client_id: existing = self.journal.get(client_id) payload = { "symbol": order["symbol"], "side": order["side"], "quantity": order["quantity"], "limit_price": order["limit_price"], } payload_digest = _payload_hash(payload) if existing is None: existing = { "client_order_id": client_id, "payload": payload, "payload_hash": payload_digest, } self.journal.put(client_id, existing) elif existing.get("payload_hash") != payload_digest: self._state = "DEGRADED" raise IdempotencyConflict( f"broker payload conflicts with journal for {client_id}" ) prior_snapshot = existing.get("broker_snapshot") prior_filled = int(existing.get("filled_quantity", 0) or 0) if filled_quantity < prior_filled: self._state = "DEGRADED" raise IdempotencyConflict( f"broker order {order_id} journal cumulative fill " f"regressed from {prior_filled} to {filled_quantity}" ) prior_state = existing.get("state") if prior_snapshot is not None and prior_state is not None: allowed = _BROKER_STATE_TRANSITIONS.get( str(prior_state), set() ) if order["state"] not in allowed: self._state = "DEGRADED" raise IdempotencyConflict( f"broker order {order_id} journal state regressed " f"illegally from {prior_state} to {order['state']}" ) elif ( prior_state in TERMINAL_STATES and order["state"] != prior_state ): self._state = "DEGRADED" raise IdempotencyConflict( f"terminal journal state {prior_state} conflicts " f"with broker state {order['state']}" ) prior_broker_id = existing.get("broker_order_id") if ( prior_broker_id not in {None, order_id} and order_id is not None ): self._state = "DEGRADED" raise IdempotencyConflict( f"multiple broker orders share client id {client_id}" ) self.journal.update( client_id, broker_order_id=order_id, state=order["state"], filled_quantity=filled_quantity, broker_snapshot=order, ) if order_id is not None: self._orders[order_id] = order return dict(order) def _ingest_trade(self, raw: Any) -> Dict[str, Any]: trade = self._normalize_trade(raw) key = trade["trade_id"] if key is None: key = ( trade["broker_order_id"], trade["symbol"], trade["quantity"], trade["price"], ) self._trades[key] = trade return dict(trade) def _apply_submit_response_locked(self, response: Any) -> None: seq = _field(response, ["seq"], None) found = self.journal.find_by_seq(seq) if found is None: # The callback may race ahead of order_stock_async returning seq. self._orphan_submit_responses[seq] = response return client_id, unused_record = found del unused_record broker_id = _field(response, ["order_id"], None) message = str(_field(response, ["error_msg"], "") or "") state = "ACCEPTED" if broker_id not in {None, -1, 0} and not message else "REJECTED" current = self.journal.get(client_id) or {} updates = { "broker_order_id": broker_id, "error_message": message, } current_state = current.get("state") allowed = _BROKER_STATE_TRANSITIONS.get( str(current_state), set() ) if current_state is not None and state not in allowed: self._state = "DEGRADED" updates["callback_state_conflict"] = ( f"submit response {state} after {current_state}" ) else: updates["state"] = state self.journal.update(client_id, **updates) def _ingest_submit_response(self, response: Any) -> None: with self._callback_lock: self._apply_submit_response_locked(response) def _apply_order_error_locked(self, item: Mapping[str, Any]) -> bool: client_id = item.get("client_order_id") found = None if client_id: record = self.journal.get(str(client_id)) if record is not None: found = (str(client_id), record) order_id = item.get("order_id") if found is None and order_id not in {None, -1, 0}: found = self.journal.find_by_broker_order_id(order_id) seq = item.get("seq") if found is None and seq is not None: found = self.journal.find_by_seq(seq) if found is None: return False matched_client_id, record = found if record is None: return False current_state = record.get("state") allowed = _BROKER_STATE_TRANSITIONS.get( str(current_state), set() ) if current_state is not None and "REJECTED" not in allowed: self._state = "DEGRADED" self.journal.update( matched_client_id, callback_state_conflict=( f"order error REJECTED after {current_state}" ), error_id=item.get("error_id"), error_message=item.get("message", ""), ) return True updates = { "state": "REJECTED", "error_id": item.get("error_id"), "error_message": item.get("message", ""), } if order_id not in {None, -1, 0}: updates["broker_order_id"] = order_id self.journal.update(matched_client_id, **updates) return True def _ingest_order_error(self, error: Any) -> None: remark = _field(error, ["order_remark"], "") item = { "kind": "order", "order_id": _field(error, ["order_id"], None), "seq": _field(error, ["seq"], None), "client_order_id": _client_id_from_remark(remark), "error_id": _field(error, ["error_id"], None), "message": str(_field(error, ["error_msg"], "")), } with self._callback_lock: self._errors.append(item) if not self._apply_order_error_locked(item): # XtTrader may invoke the callback synchronously before # order_stock_async returns its request sequence. self._orphan_order_errors.append(item) def _drain_order_errors_locked(self) -> None: remaining = [] for item in self._orphan_order_errors: if not self._apply_order_error_locked(item): remaining.append(item) self._orphan_order_errors = remaining def _existing_broker_order( self, client_order_id: str, payload: Mapping[str, Any] ) -> Optional[Dict[str, Any]]: for order in self.query_orders(): if order.get("client_order_id") != client_order_id: continue comparable = { "symbol": order["symbol"], "side": order["side"], "quantity": order["quantity"], "limit_price": order["limit_price"], } if _payload_hash(comparable) != _payload_hash(payload): raise IdempotencyConflict( f"broker already has {client_order_id} with another payload" ) return order return None def submit_order( self, *, client_order_id: str, symbol: str, side: str, quantity: int, limit_price: float, confirm_live: bool = False, ) -> Dict[str, Any]: """Idempotently plan or asynchronously submit one limit order.""" confirmed_live = _strict_bool(confirm_live, "confirm_live") if not _CLIENT_ID_RE.fullmatch(client_order_id): raise ValueError( "client_order_id must be 1-20 ASCII letters/digits/_.-" ) normalized_symbol = str(symbol).upper() if not re.fullmatch(r"\d{6}\.(SH|SZ)", normalized_symbol): raise ValueError("symbol must use QMT 600000.SH/000001.SZ form") normalized_side = str(side).upper() if normalized_side not in {"BUY", "SELL"}: raise ValueError("side must be BUY or SELL") if isinstance(quantity, bool): raise ValueError( "A-share quantity must be a positive integer 100-share lot" ) try: volume = operator.index(quantity) except TypeError as exc: raise ValueError( "A-share quantity must be a positive integer 100-share lot" ) from exc if volume <= 0 or volume % 100: raise ValueError( "A-share quantity must be a positive integer 100-share lot" ) if isinstance(limit_price, bool) or not isinstance( limit_price, numbers.Real ): raise ValueError( "limit_price must be a non-bool real number" ) price = float(limit_price) if not math.isfinite(price) or price <= 0: raise ValueError("limit_price must be finite and positive") payload = { "symbol": convert_symbol(normalized_symbol, "canonical"), "side": normalized_side, "quantity": volume, "limit_price": price, } digest = _payload_hash(payload) # The entire read/check/recover/reserve/submit/persist transaction is # serialized. The journal's own lock only protects individual calls; # it cannot by itself prevent two threads from both seeing "missing". with self._lock: existing = self.journal.get(client_order_id) if existing: if existing.get("payload_hash") != digest: raise IdempotencyConflict( f"client_order_id {client_order_id} has another payload" ) return existing if not self.allow_live_orders: record = { "client_order_id": client_order_id, "payload": payload, "payload_hash": digest, "state": "DRY_RUN", "broker_order_id": None, } self.journal.put(client_order_id, record) return record if not confirmed_live: raise LiveOrderBlocked( "real order requires confirm_live=True for this submission" ) trader = self._require_ready() broker_existing = self._existing_broker_order( client_order_id, payload ) if broker_existing is not None: record = { "client_order_id": client_order_id, "payload": payload, "payload_hash": digest, "state": broker_existing["state"], "broker_order_id": broker_existing["broker_order_id"], "recovered_from_broker": True, } self.journal.put(client_order_id, record) return record order_type = ( getattr(self.constants, "STOCK_BUY", 23) if normalized_side == "BUY" else getattr(self.constants, "STOCK_SELL", 24) ) price_type = getattr(self.constants, "FIX_PRICE", 11) live_policy = self._authorize_live_intent( { "action": "SUBMIT_ORDER", "account_id": self._normalized_account_id(), "client_order_id": client_order_id, "symbol": payload["symbol"], "side": normalized_side, "quantity": volume, "limit_price": price, "notional": price * volume, "strategy_name": self.strategy_name, } ) reservation = { "client_order_id": client_order_id, "payload": payload, "payload_hash": digest, "state": "SUBMITTING", "request_seq": None, "broker_order_id": None, "live_policy": live_policy, } # Persist before crossing the broker mutation boundary. A crash or # callback can then never turn a duplicate retry into a new order. self.journal.put(client_order_id, reservation) try: seq = trader.order_stock_async( self.account, normalized_symbol, order_type, volume, price_type, price, self.strategy_name, _REMARK_PREFIX + client_order_id, ) except BaseException as exc: current = self.journal.get(client_order_id) or reservation if current.get("state") == "SUBMITTING": self.journal.update( client_order_id, state="UNKNOWN", error_message=( f"order_stock_async raised " f"{type(exc).__name__}: {exc}" ), ) raise try: if isinstance(seq, bool): raise TypeError("bool is not a request sequence") request_seq = operator.index(seq) except Exception as exc: self.journal.update( client_order_id, state="UNKNOWN", error_message=( "XtTrader returned a malformed async request " f"sequence: {seq!r}" ), ) raise XtTraderAdapterError( "XtTrader returned a malformed async request " f"sequence: {seq!r}" ) from exc if request_seq <= 0: self.journal.update( client_order_id, state="REJECTED", error_message=( "XtTrader rejected async submit before " f"acknowledgement: {seq}" ), ) raise XtTraderAdapterError( f"XtTrader rejected async submit before acknowledgement: {seq}" ) with self._callback_lock: self.journal.update( client_order_id, request_seq=request_seq ) raced_response = self._orphan_submit_responses.pop( request_seq, None ) if raced_response is not None: self._apply_submit_response_locked(raced_response) self._drain_order_errors_locked() current = self.journal.get(client_order_id) or reservation if current.get("state") == "SUBMITTING": current = self.journal.update( client_order_id, state="NEW" ) return current def cancel_order( self, broker_order_id: int, *, confirm_live: bool = False ) -> int: """Cancel synchronously behind the same explicit live safety gates.""" confirmed_live = _strict_bool(confirm_live, "confirm_live") if isinstance(broker_order_id, bool): raise ValueError( "broker_order_id must be a positive integer" ) try: normalized_order_id = operator.index(broker_order_id) except TypeError as exc: raise ValueError( "broker_order_id must be a positive integer" ) from exc if normalized_order_id <= 0: raise ValueError( "broker_order_id must be a positive integer" ) if not self.allow_live_orders or not confirmed_live: raise LiveOrderBlocked( "real cancel requires allow_live_orders and confirm_live=True" ) trader = self._require_ready() live_policy = self._authorize_live_intent( { "action": "CANCEL_ORDER", "account_id": self._normalized_account_id(), "broker_order_id": normalized_order_id, "strategy_name": self.strategy_name, } ) try: result = trader.cancel_order_stock( self.account, normalized_order_id ) except BaseException: self._record_live_authorization_outcome( live_policy["authorization_id"], "BROKER_CALL_EXCEPTION", ) raise if result != 0: self._record_live_authorization_outcome( live_policy["authorization_id"], "BROKER_REJECTED", ) raise XtTraderAdapterError( f"XtTrader cancel failed for {normalized_order_id}: {result}" ) self._record_live_authorization_outcome( live_policy["authorization_id"], "BROKER_ACCEPTED", ) # A zero code accepts the cancel request; a later order snapshot or # callback must still confirm CANCELLED before funds/quantity are reused. return 0