"""Run or verify the strictly read-only QMT/XtTrader shadow preflight. Environment fallbacks: QMT_USERDATA_PATH QMT_ACCOUNT_ID QMT_SESSION_ID QMT_ACCOUNT_HASH_KEY_FILE The runtime adapter is always constructed with ``allow_live_orders=False``. No command in this tool submits or cancels an order. """ from __future__ import annotations import argparse import json import os import secrets import stat import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] for import_root in (PROJECT_ROOT, PROJECT_ROOT / "src"): if str(import_root) not in sys.path: sys.path.insert(0, str(import_root)) from adapters.xttrader_live import XtTraderLiveAdapter from quant60.qmt_shadow import ( MAX_BROKER_ASSET_ABSOLUTE_TOLERANCE, MAX_SHADOW_QUERY_DURATION_SECONDS, build_qmt_shadow_plan, verify_qmt_shadow_plan, write_qmt_shadow_plan, ) def _required(value: str | None, name: str) -> str: if value is None or not value.strip(): raise ValueError(f"{name} is required") return value.strip() def _bounded_non_negative_float( *, name: str, maximum: float, ): def parse(value: str) -> float: try: parsed = float(value) except ValueError as exc: raise argparse.ArgumentTypeError( f"{name} must be a number" ) from exc if not 0 <= parsed <= maximum: raise argparse.ArgumentTypeError( f"{name} must be between 0 and {maximum:g}" ) return parsed return parse def _load_or_create_account_hash_key(path: str | Path) -> bytes: """Load a stable local HMAC key without ever exposing it in artifacts.""" key_path = Path(path).expanduser() parent_existed = key_path.parent.exists() key_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) if not parent_existed: try: key_path.parent.chmod(0o700) except OSError: pass create_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL create_flags |= getattr(os, "O_CLOEXEC", 0) try: descriptor = os.open(str(key_path), create_flags, 0o600) except FileExistsError: descriptor = None if descriptor is not None: key = secrets.token_bytes(32) with os.fdopen(descriptor, "wb") as handle: handle.write(key) handle.flush() os.fsync(handle.fileno()) return _load_existing_account_hash_key(key_path) def _load_existing_account_hash_key(path: str | Path) -> bytes: key_path = Path(path).expanduser() if not key_path.is_file(): raise ValueError( "QMT account hash key file does not exist; verification never " "creates a replacement key" ) read_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) read_flags |= getattr(os, "O_NOFOLLOW", 0) descriptor = os.open(str(key_path), read_flags) with os.fdopen(descriptor, "rb") as handle: key = handle.read() if len(key) < 32: raise ValueError("QMT account hash key must contain at least 32 bytes") try: mode = stat.S_IMODE(key_path.stat().st_mode) if os.name != "nt" and mode & 0o077: raise ValueError( "QMT account hash key file must not be group/world accessible" ) except OSError as exc: raise ValueError("cannot validate QMT account hash key file") from exc return key def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) plan = subparsers.add_parser("plan", help="query QMT and write a shadow plan") plan.add_argument("--decision", required=True) plan.add_argument("--output", required=True) plan.add_argument( "--userdata-path", default=os.environ.get("QMT_USERDATA_PATH") ) plan.add_argument("--account-id", default=os.environ.get("QMT_ACCOUNT_ID")) plan.add_argument( "--session-id", type=int, default=( int(os.environ["QMT_SESSION_ID"]) if os.environ.get("QMT_SESSION_ID") else None ), ) plan.add_argument( "--max-query-duration-seconds", type=_bounded_non_negative_float( name="max-query-duration-seconds", maximum=MAX_SHADOW_QUERY_DURATION_SECONDS, ), default=MAX_SHADOW_QUERY_DURATION_SECONDS, help=( "query-window ceiling; may be tightened but cannot exceed " f"{MAX_SHADOW_QUERY_DURATION_SECONDS:g}s" ), ) plan.add_argument( "--market-value-tolerance", type=_bounded_non_negative_float( name="market-value-tolerance", maximum=MAX_BROKER_ASSET_ABSOLUTE_TOLERANCE, ), default=MAX_BROKER_ASSET_ABSOLUTE_TOLERANCE, help=( "asset identity tolerance; may be tightened but cannot exceed " f"{MAX_BROKER_ASSET_ABSOLUTE_TOLERANCE:g} CNY" ), ) plan.add_argument( "--account-hash-key-file", default=os.environ.get( "QMT_ACCOUNT_HASH_KEY_FILE", str(Path.home() / ".config" / "quant-os" / "account-hash.key"), ), help=( "local 0600 HMAC key file; generated once when absent and never " "written to artifacts" ), ) verify = subparsers.add_parser( "verify", help="verify a completed shadow-plan artifact set" ) verify.add_argument("--manifest", required=True) verify.add_argument("--decision", required=True) verify.add_argument( "--account-hash-key-file", default=os.environ.get( "QMT_ACCOUNT_HASH_KEY_FILE", str(Path.home() / ".config" / "quant-os" / "account-hash.key"), ), help=( "existing local 0600 HMAC key used to authenticate the broker " "observation; verify never creates a key" ), ) return parser def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) if args.command == "verify": account_hash_key = _load_existing_account_hash_key( args.account_hash_key_file ) payload = verify_qmt_shadow_plan( args.manifest, decision_manifest=args.decision, account_hash_key=account_hash_key, ) print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)) return 0 userdata_path = _required(args.userdata_path, "QMT userdata path") account_id = _required(args.account_id, "QMT account id") session_id = ( args.session_id if args.session_id is not None else secrets.randbelow(2_000_000_000) + 1 ) account_hash_key = _load_or_create_account_hash_key( args.account_hash_key_file ) adapter = XtTraderLiveAdapter.from_xtquant( userdata_path=userdata_path, session_id=session_id, account_id=account_id, allow_live_orders=False, strategy_name="quant-os-shadow", ) try: adapter.connect() result = build_qmt_shadow_plan( decision_manifest=args.decision, adapter=adapter, max_query_duration_seconds=args.max_query_duration_seconds, market_value_tolerance=args.market_value_tolerance, account_hash_key=account_hash_key, ) paths = write_qmt_shadow_plan(result, args.output) verification = verify_qmt_shadow_plan( paths["manifest"], decision_manifest=args.decision, account_hash_key=account_hash_key, ) ready = result["plan"]["status"] == "SHADOW_READY" payload = { "ok": ready, "artifact_written": True, "artifact_verified": verification["ok"], "plan_id": result["plan"]["plan_id"], "status": result["plan"]["status"], "blockers": result["plan"]["blockers"], "artifacts": paths, "read_only": True, } finally: adapter.close() print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)) return 0 if payload["ok"] else 2 if __name__ == "__main__": raise SystemExit(main())