auto mode for continuous running

This commit is contained in:
2026-04-05 00:50:26 +08:00
parent 42218741d4
commit 4709271504
3 changed files with 137 additions and 0 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.claude
# Python # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]

0
README.md Normal file
View File

135
trader.py
View File

@@ -4,16 +4,25 @@ Daily trading simulation system.
Subcommands: Subcommands:
morning — Download today's open prices, run strategy, output trade orders morning — Download today's open prices, run strategy, output trade orders
evening — Download close prices, record execution, update portfolio evening — Download close prices, record execution, update portfolio
auto — Single daily run (morning + evening in one step, for cron)
monitor — Long-running daemon that auto-runs daily after market close (for tmux)
status — Print current portfolio, P&L, and recent trades status — Print current portfolio, P&L, and recent trades
simulate — Replay a date range day-by-day (for forward testing) simulate — Replay a date range day-by-day (for forward testing)
Usage: Usage:
uv run python trader.py morning --market us --strategy recovery_mom_top10 uv run python trader.py morning --market us --strategy recovery_mom_top10
uv run python trader.py evening --market us --strategy recovery_mom_top10 uv run python trader.py evening --market us --strategy recovery_mom_top10
uv run python trader.py auto --market us --strategy recovery_mom_top10
uv run python trader.py monitor --market us --strategy recovery_mom_top10
uv run python trader.py status --market us --strategy recovery_mom_top10 uv run python trader.py status --market us --strategy recovery_mom_top10
uv run python trader.py simulate --market us --strategy recovery_mom_top10 \\ uv run python trader.py simulate --market us --strategy recovery_mom_top10 \\
--start 2026-01-01 --end 2026-04-04 --start 2026-01-01 --end 2026-04-04
Monitor mode (recommended for tmux):
tmux new -s quant
uv run python trader.py monitor --market us --strategy recovery_mom_top10
# Ctrl-B D to detach; tmux attach -t quant to reconnect
State is persisted in data/trader_{market}_{strategy}.json between runs. State is persisted in data/trader_{market}_{strategy}.json between runs.
""" """
@@ -648,6 +657,125 @@ def cmd_simulate(args):
print() print()
def cmd_monitor(args):
"""
Long-running monitor — runs in a tmux session, automatically executes daily.
Sleeps until market close + buffer, runs auto logic, then sleeps until
the next trading day. Handles weekends and holidays gracefully.
Usage:
tmux new -s quant
uv run python trader.py monitor --market us --strategy recovery_mom_top10
# Ctrl-B D to detach
"""
import time as _time
import zoneinfo
market = args.market
# Market schedule configuration
MARKET_CONFIG = {
"us": {
"tz": zoneinfo.ZoneInfo("America/New_York"),
"close_hour": 16, # 4:00 PM ET
"close_min": 0,
"buffer_min": 35, # Wait 35 min after close for data settlement
"label": "US (NYSE/NASDAQ)",
},
"cn": {
"tz": zoneinfo.ZoneInfo("Asia/Shanghai"),
"close_hour": 15, # 3:00 PM CST
"close_min": 0,
"buffer_min": 35,
"label": "CN (SSE/SZSE)",
},
}
config = MARKET_CONFIG.get(market)
if not config:
print(f"[monitor] Unknown market '{market}'. Supported: {list(MARKET_CONFIG.keys())}")
return
tz = config["tz"]
run_hour = config["close_hour"]
run_min = config["close_min"] + config["buffer_min"]
# Handle minute overflow
if run_min >= 60:
run_hour += run_min // 60
run_min = run_min % 60
print(f"\n{'='*60}")
print(f" MONITOR MODE — {config['label']}")
print(f" Strategy: {args.strategy} | Market: {market.upper()}")
print(f" Daily run at: {run_hour:02d}:{run_min:02d} {tz}")
print(f" Capital: ${args.capital:,.0f}")
print(f"{'='*60}")
print(f"[monitor] Started at {datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S %Z')}")
print(f"[monitor] Press Ctrl+C to stop\n")
def _next_run_time(now):
"""Compute the next execution datetime (skip weekends)."""
# Today's run time
target = now.replace(hour=run_hour, minute=run_min, second=0, microsecond=0)
if now >= target:
# Already past today's run time, schedule for tomorrow
target += timedelta(days=1)
# Skip weekends: Saturday=5, Sunday=6
while target.weekday() >= 5:
target += timedelta(days=1)
return target
while True:
now = datetime.now(tz)
next_run = _next_run_time(now)
wait_seconds = (next_run - now).total_seconds()
print(f"[monitor] {now.strftime('%Y-%m-%d %H:%M:%S')}"
f"Next run: {next_run.strftime('%Y-%m-%d %H:%M:%S %Z')} "
f"(in {wait_seconds/3600:.1f}h)")
# Sleep in chunks so Ctrl+C is responsive and we print heartbeats
while True:
now = datetime.now(tz)
remaining = (next_run - now).total_seconds()
if remaining <= 0:
break
# Sleep in 15-minute chunks, print heartbeat every hour
chunk = min(remaining, 900) # 15 min
_time.sleep(chunk)
now = datetime.now(tz)
remaining = (next_run - now).total_seconds()
if remaining > 60:
hours_left = remaining / 3600
if int(remaining) % 3600 < 900: # ~on the hour
print(f"[monitor] {now.strftime('%H:%M:%S')}"
f"waiting... ({hours_left:.1f}h until run)")
# Time to run!
now = datetime.now(tz)
print(f"\n[monitor] {'='*55}")
print(f"[monitor] Executing daily run at {now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
print(f"[monitor] {'='*55}")
try:
cmd_auto(args)
print(f"[monitor] Daily run completed successfully.")
except KeyboardInterrupt:
raise
except Exception as e:
print(f"[monitor] ERROR during daily run: {e}")
import traceback
traceback.print_exc()
print(f"[monitor] Will retry on next scheduled run.")
print()
def cmd_auto(args): def cmd_auto(args):
""" """
Automated daily run — single invocation handles both morning + evening. Automated daily run — single invocation handles both morning + evening.
@@ -783,6 +911,11 @@ def main():
help="Automated daily run: signal + execute in one step (for cron)") help="Automated daily run: signal + execute in one step (for cron)")
add_common(p_auto) add_common(p_auto)
# Monitor (long-running daemon for tmux)
p_monitor = sub.add_parser("monitor",
help="Long-running daemon: auto-runs daily after market close (for tmux)")
add_common(p_monitor)
# Status (strategy is a free-form string to allow sim_ prefixed names) # Status (strategy is a free-form string to allow sim_ prefixed names)
p_status = sub.add_parser("status", help="Show current portfolio") p_status = sub.add_parser("status", help="Show current portfolio")
p_status.add_argument("--market", choices=UNIVERSES.keys(), default="us") p_status.add_argument("--market", choices=UNIVERSES.keys(), default="us")
@@ -806,6 +939,8 @@ def main():
cmd_evening(args) cmd_evening(args)
elif args.command == "auto": elif args.command == "auto":
cmd_auto(args) cmd_auto(args)
elif args.command == "monitor":
cmd_monitor(args)
elif args.command == "status": elif args.command == "status":
cmd_status(args) cmd_status(args)
elif args.command == "simulate": elif args.command == "simulate":