auto mode for continuous running
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.claude
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
135
trader.py
135
trader.py
@@ -4,16 +4,25 @@ Daily trading simulation system.
|
||||
Subcommands:
|
||||
morning — Download today's open prices, run strategy, output trade orders
|
||||
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
|
||||
simulate — Replay a date range day-by-day (for forward testing)
|
||||
|
||||
Usage:
|
||||
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 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 simulate --market us --strategy recovery_mom_top10 \\
|
||||
--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.
|
||||
"""
|
||||
|
||||
@@ -648,6 +657,125 @@ def cmd_simulate(args):
|
||||
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):
|
||||
"""
|
||||
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)")
|
||||
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)
|
||||
p_status = sub.add_parser("status", help="Show current portfolio")
|
||||
p_status.add_argument("--market", choices=UNIVERSES.keys(), default="us")
|
||||
@@ -806,6 +939,8 @@ def main():
|
||||
cmd_evening(args)
|
||||
elif args.command == "auto":
|
||||
cmd_auto(args)
|
||||
elif args.command == "monitor":
|
||||
cmd_monitor(args)
|
||||
elif args.command == "status":
|
||||
cmd_status(args)
|
||||
elif args.command == "simulate":
|
||||
|
||||
Reference in New Issue
Block a user