"""Daily live-trading update for all registered strategies. Iterates STRATEGY_REGISTRY and calls trader.cmd_auto for each, sharing fetched price panels across strategies (we still call cmd_auto so the state-file format stays consistent with manual runs). Usage: uv run python scripts/run_daily.py [--market us|cn|all] [--capital 10000] [--only NAME ...] [--skip NAME ...] Designed for cron: # 5pm ET weekdays, after US close 0 17 * * 1-5 cd /path/to/quant && uv run python scripts/run_daily.py --market us # 4pm CST weekdays, after CN close 0 16 * * 1-5 cd /path/to/quant && uv run python scripts/run_daily.py --market cn """ from __future__ import annotations import argparse import os import sys import time import types from datetime import datetime # Make the project root importable when invoked as `python scripts/run_daily.py` PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) # We change into the project root so trader's relative `data/` paths resolve. os.chdir(PROJECT_ROOT) from trader import ( # noqa: E402 ETF_STRATEGY_UNIVERSES, STRATEGY_REGISTRY, cmd_auto, ) from bridge import STRATEGY_META # noqa: E402 def _strategy_markets(name: str) -> list[str]: """Return the markets a strategy targets (per its META declaration).""" meta = STRATEGY_META.get(name, {"markets": ["us"]}) return list(meta.get("markets") or ["us"]) def _make_args(market: str, name: str, capital: float) -> argparse.Namespace: """Build the argparse namespace cmd_auto expects.""" return argparse.Namespace( market=market, strategy=name, capital=capital, tx_cost=0.001, fixed_fee=0.0, # cmd_auto falls back to MARKET_FEES + tier integer_shares=True, # match the existing live state convention ) def main() -> int: parser = argparse.ArgumentParser(description="Daily live-trading update") parser.add_argument("--market", default="all", help="Which market(s): 'us', 'cn', 'all' (default: all)") parser.add_argument("--capital", type=float, default=10_000, help="Initial capital for any newly-initialized strategy (default: 10000)") parser.add_argument("--only", nargs="+", default=None, help="If set, only run these strategy names") parser.add_argument("--skip", nargs="+", default=None, help="Strategy names to skip (e.g. broken ones)") parser.add_argument("--dry-run", action="store_true", help="Print what would be run without executing") args = parser.parse_args() markets = ["us", "cn"] if args.market == "all" else [args.market] only = set(args.only) if args.only else None skip = set(args.skip) if args.skip else set() plan: list[tuple[str, str]] = [] for name in STRATEGY_REGISTRY: if only and name not in only: continue if name in skip: continue for mk in _strategy_markets(name): if mk in markets: plan.append((mk, name)) print(f"[run_daily] {datetime.now().isoformat(timespec='seconds')} " f"will process {len(plan)} (market, strategy) pairs") if args.dry_run: for mk, name in plan: print(f" [DRY] {mk:<3} {name}") return 0 failures = [] t0 = time.time() for mk, name in plan: ns = _make_args(mk, name, args.capital) try: cmd_auto(ns) except SystemExit: raise except Exception as e: # noqa: BLE001 failures.append((mk, name, repr(e))) print(f"[run_daily] FAILED {mk} {name}: {e}", file=sys.stderr) elapsed = time.time() - t0 print(f"[run_daily] done in {elapsed:.1f}s — {len(plan) - len(failures)} ok, {len(failures)} failed") for mk, name, err in failures: print(f" fail {mk} {name}: {err}", file=sys.stderr) return 0 if not failures else 1 if __name__ == "__main__": sys.exit(main())