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:
207
trader.py
207
trader.py
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user