auto mode for continuous running
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
.claude
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
135
trader.py
135
trader.py
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user