feat: add Quant OS A-share baseline

This commit is contained in:
2026-07-26 12:54:04 +08:00
commit 48c5f64bbd
98 changed files with 31874 additions and 0 deletions
+255
View File
@@ -0,0 +1,255 @@
"""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())