chore: track precompute and ops scripts; move monitor proxy into .env
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>
This commit is contained in:
116
scripts/run_daily.py
Normal file
116
scripts/run_daily.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user