The monitor start script used to embed the proxy URL with credentials, which kept scripts/ out of git. It now reads QUANT_MARKET_PROXY from the untracked repo-root .env. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
"""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())
|