Multi-strategy monitor and compare command

- Monitor accepts multiple --strategy args, runs all at each phase
- Each strategy maintains its own independent state file
- Add 'compare' subcommand: side-by-side ranking table + equity curves
- Error in one strategy doesn't block others (isolated try/catch)

Usage:
  trader.py monitor --strategy recovery_mom_top10 momentum dual_momentum
  trader.py compare --strategy sim_recovery_mom_top10 sim_momentum

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 00:04:45 +08:00
parent 942164a38f
commit 7f618b9c31

207
trader.py
View File

@@ -593,6 +593,130 @@ def cmd_status(args):
print()
def cmd_compare(args):
"""Compare multiple strategies side-by-side."""
market = args.market
strategy_names = args.strategy
# Load all states
states = {}
for name in strategy_names:
state = load_state(market, name)
if state and state.get("daily_equity"):
states[name] = state
else:
print(f" Warning: no data for '{name}' — skipping")
if not states:
print("No strategies with data found.")
return
# Find common date range
all_dates = set()
for state in states.values():
all_dates.update(state["daily_equity"].keys())
all_dates = sorted(all_dates)
if not all_dates:
print("No equity data found.")
return
print(f"\n{'='*80}")
print(f" STRATEGY COMPARISON — {market.upper()}")
print(f" Period: {all_dates[0]} to {all_dates[-1]} ({len(all_dates)} days)")
print(f"{'='*80}")
# Summary table
results = []
for name, state in states.items():
eq = state["daily_equity"]
dates = sorted(eq.keys())
if len(dates) < 2:
continue
initial = state["initial_capital"]
final = eq[dates[-1]]
n_days = len(dates)
total_ret = (final / initial - 1) * 100
ann_ret = ((final / initial) ** (252 / n_days) - 1) * 100 if n_days > 0 else 0
# Max drawdown
values = [eq[d] for d in dates]
peak = values[0]
max_dd = 0
for v in values:
peak = max(peak, v)
dd = (v - peak) / peak
max_dd = min(max_dd, dd)
# Sharpe
daily_rets = []
for i in range(1, len(values)):
daily_rets.append(values[i] / values[i - 1] - 1)
daily_rets = np.array(daily_rets) if daily_rets else np.array([0])
sharpe = (daily_rets.mean() / daily_rets.std() * np.sqrt(252)) if daily_rets.std() > 0 else 0
n_trades = len(state.get("trade_log", []))
total_comm = sum(t.get("commission", 0) for t in state.get("trade_log", []))
results.append({
"name": name,
"initial": initial,
"final": final,
"total_ret": total_ret,
"ann_ret": ann_ret,
"sharpe": sharpe,
"max_dd": max_dd * 100,
"n_trades": n_trades,
"commission": total_comm,
"n_days": n_days,
"n_positions": len(state.get("holdings", {})),
"cash": state.get("cash", 0),
})
# Sort by total return descending
results.sort(key=lambda r: -r["total_ret"])
# Print ranking
print(f"\n {'#':<3} {'Strategy':<25} {'Return':>9} {'Ann.Ret':>9} "
f"{'Sharpe':>8} {'MaxDD':>8} {'Trades':>7} {'Final':>14}")
print(f" {''*87}")
for i, r in enumerate(results, 1):
print(f" {i:<3} {r['name']:<25} {r['total_ret']:>+8.1f}% {r['ann_ret']:>+8.1f}% "
f"{r['sharpe']:>8.2f} {r['max_dd']:>7.1f}% {r['n_trades']:>7} "
f"${r['final']:>12,.2f}")
# Equity curve comparison (last 20 dates)
recent_dates = all_dates[-20:]
print(f"\n Equity Curve (last {len(recent_dates)} days):")
header = f" {'Date':<12}"
for r in results:
header += f" {r['name'][:14]:>14}"
print(header)
print(f" {'' * (12 + 15 * len(results))}")
for d in recent_dates:
row = f" {d:<12}"
for r in results:
eq = states[r["name"]]["daily_equity"]
if d in eq:
row += f" ${eq[d]:>12,.2f}"
else:
row += f" {'':>14}"
print(row)
# Winner summary
if results:
best = results[0]
worst = results[-1]
print(f"\n Best: {best['name']} ({best['total_ret']:>+.1f}%)")
if len(results) > 1:
print(f" Worst: {worst['name']} ({worst['total_ret']:>+.1f}%)")
print(f" Spread: {best['total_ret'] - worst['total_ret']:.1f}pp")
print()
def cmd_log(args):
"""Print the daily log: each day's holdings, cash, operations."""
market = args.market
@@ -844,17 +968,21 @@ def cmd_monitor(args):
1. MORNING (~9:45 AM): Download open prices, run strategy, print orders
2. EVENING (~4:35 PM): Download close prices, execute trades, record
Sleeps between phases and between trading days. Skips weekends.
Supports multiple strategies — each gets its own state file and runs
independently at each phase.
Usage:
tmux new -s quant
uv run python trader.py monitor --market us --strategy recovery_mom_top10
uv run python trader.py monitor --market us \\
--strategy recovery_mom_top10 momentum dual_momentum
# Ctrl-B D to detach
"""
import copy
import time as _time
import zoneinfo
market = args.market
strategies = args.strategy # list of strategy names
# Market schedule configuration — two phases per day
MARKET_CONFIG = {
@@ -894,10 +1022,12 @@ def cmd_monitor(args):
print(f"\n{'='*60}")
print(f" MONITOR MODE — {config['label']}")
print(f" Strategy: {args.strategy} | Market: {market.upper()}")
print(f" Market: {market.upper()} | Capital: ${args.capital:,.0f}")
print(f" Strategies ({len(strategies)}):")
for s in strategies:
print(f"{s}")
print(f" Morning (open → signals): {morn_h:02d}:{morn_m:02d} {tz}")
print(f" Evening (close → execute): {eve_h:02d}:{eve_m: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")
@@ -969,26 +1099,34 @@ def cmd_monitor(args):
f"{now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
print(f"[monitor] {'='*55}")
try:
if phase == "morning":
cmd_morning(args)
else:
# Check if morning was run today; if not, use auto (combined)
state = load_state(market, args.strategy)
today_str = now.strftime("%Y-%m-%d")
if state and state.get("last_morning") == today_str and state.get("pending_trades"):
cmd_evening(args)
for strat_name in strategies:
# Create a single-strategy args copy for each sub-command
sub_args = copy.copy(args)
sub_args.strategy = strat_name
print(f"\n[monitor] --- {strat_name} ---")
try:
if phase == "morning":
cmd_morning(sub_args)
else:
print(f"[monitor] Morning was not run today — using auto (combined) mode")
cmd_auto(args)
print(f"[monitor] {phase.upper()} completed successfully.")
except KeyboardInterrupt:
raise
except Exception as e:
print(f"[monitor] ERROR during {phase}: {e}")
import traceback
traceback.print_exc()
print(f"[monitor] Will continue to next scheduled event.")
# Check if morning was run today; if not, use auto (combined)
state = load_state(market, strat_name)
today_str = now.strftime("%Y-%m-%d")
if state and state.get("last_morning") == today_str and state.get("pending_trades"):
cmd_evening(sub_args)
else:
print(f"[monitor] Morning not run for {strat_name} — using auto mode")
cmd_auto(sub_args)
print(f"[monitor] {strat_name} {phase} OK")
except KeyboardInterrupt:
raise
except Exception as e:
print(f"[monitor] ERROR in {strat_name} {phase}: {e}")
import traceback
traceback.print_exc()
print(f"[monitor] Continuing with next strategy...")
print(f"\n[monitor] All {len(strategies)} strategies processed.")
print()
@@ -1137,10 +1275,21 @@ def main():
help="Automated daily run: signal + execute in one step (for cron)")
add_common(p_auto)
# Monitor (long-running daemon for tmux)
# Monitor (long-running daemon for tmux) — supports multiple strategies
p_monitor = sub.add_parser("monitor",
help="Long-running daemon: auto-runs daily after market close (for tmux)")
add_common(p_monitor)
p_monitor.add_argument("--market", choices=UNIVERSES.keys(), default="us")
p_monitor.add_argument("--strategy", nargs="+",
choices=list(STRATEGY_REGISTRY.keys()),
default=["recovery_mom_top10"],
help="One or more strategies to run in parallel")
p_monitor.add_argument("--capital", type=float, default=100_000)
p_monitor.add_argument("--tx-cost", type=float, default=0.001,
help="Proportional transaction cost (default: 0.001 = 10bps)")
p_monitor.add_argument("--fixed-fee", type=float, default=0.0,
help="Fixed dollar fee per trade")
p_monitor.add_argument("--integer-shares", action="store_true", default=False,
help="Only trade whole shares (no fractional)")
# Status (strategy is a free-form string to allow sim_ prefixed names)
p_status = sub.add_parser("status", help="Show current portfolio")
@@ -1166,6 +1315,12 @@ def main():
p_log.add_argument("--start", default=None, help="Start date filter (YYYY-MM-DD)")
p_log.add_argument("--end", default=None, help="End date filter (YYYY-MM-DD)")
# Compare strategies
p_cmp = sub.add_parser("compare", help="Compare multiple strategies side-by-side")
p_cmp.add_argument("--market", choices=UNIVERSES.keys(), default="us")
p_cmp.add_argument("--strategy", nargs="+", required=True,
help="Strategy state names to compare (e.g. recovery_mom_top10 momentum)")
args = parser.parse_args()
if args.command == "morning":
@@ -1182,6 +1337,8 @@ def main():
cmd_simulate(args)
elif args.command == "log":
cmd_log(args)
elif args.command == "compare":
cmd_compare(args)
if __name__ == "__main__":