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:
185
trader.py
185
trader.py
@@ -593,6 +593,130 @@ def cmd_status(args):
|
|||||||
print()
|
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):
|
def cmd_log(args):
|
||||||
"""Print the daily log: each day's holdings, cash, operations."""
|
"""Print the daily log: each day's holdings, cash, operations."""
|
||||||
market = args.market
|
market = args.market
|
||||||
@@ -844,17 +968,21 @@ def cmd_monitor(args):
|
|||||||
1. MORNING (~9:45 AM): Download open prices, run strategy, print orders
|
1. MORNING (~9:45 AM): Download open prices, run strategy, print orders
|
||||||
2. EVENING (~4:35 PM): Download close prices, execute trades, record
|
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:
|
Usage:
|
||||||
tmux new -s quant
|
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
|
# Ctrl-B D to detach
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
import time as _time
|
import time as _time
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
|
||||||
market = args.market
|
market = args.market
|
||||||
|
strategies = args.strategy # list of strategy names
|
||||||
|
|
||||||
# Market schedule configuration — two phases per day
|
# Market schedule configuration — two phases per day
|
||||||
MARKET_CONFIG = {
|
MARKET_CONFIG = {
|
||||||
@@ -894,10 +1022,12 @@ def cmd_monitor(args):
|
|||||||
|
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f" MONITOR MODE — {config['label']}")
|
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" Morning (open → signals): {morn_h:02d}:{morn_m:02d} {tz}")
|
||||||
print(f" Evening (close → execute): {eve_h:02d}:{eve_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"{'='*60}")
|
||||||
print(f"[monitor] Started at {datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
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")
|
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')}")
|
f"{now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
||||||
print(f"[monitor] {'='*55}")
|
print(f"[monitor] {'='*55}")
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
if phase == "morning":
|
if phase == "morning":
|
||||||
cmd_morning(args)
|
cmd_morning(sub_args)
|
||||||
else:
|
else:
|
||||||
# Check if morning was run today; if not, use auto (combined)
|
# Check if morning was run today; if not, use auto (combined)
|
||||||
state = load_state(market, args.strategy)
|
state = load_state(market, strat_name)
|
||||||
today_str = now.strftime("%Y-%m-%d")
|
today_str = now.strftime("%Y-%m-%d")
|
||||||
if state and state.get("last_morning") == today_str and state.get("pending_trades"):
|
if state and state.get("last_morning") == today_str and state.get("pending_trades"):
|
||||||
cmd_evening(args)
|
cmd_evening(sub_args)
|
||||||
else:
|
else:
|
||||||
print(f"[monitor] Morning was not run today — using auto (combined) mode")
|
print(f"[monitor] Morning not run for {strat_name} — using auto mode")
|
||||||
cmd_auto(args)
|
cmd_auto(sub_args)
|
||||||
print(f"[monitor] {phase.upper()} completed successfully.")
|
print(f"[monitor] {strat_name} {phase} OK")
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[monitor] ERROR during {phase}: {e}")
|
print(f"[monitor] ERROR in {strat_name} {phase}: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
print(f"[monitor] Will continue to next scheduled event.")
|
print(f"[monitor] Continuing with next strategy...")
|
||||||
|
|
||||||
|
print(f"\n[monitor] All {len(strategies)} strategies processed.")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
@@ -1137,10 +1275,21 @@ 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)
|
# Monitor (long-running daemon for tmux) — supports multiple strategies
|
||||||
p_monitor = sub.add_parser("monitor",
|
p_monitor = sub.add_parser("monitor",
|
||||||
help="Long-running daemon: auto-runs daily after market close (for tmux)")
|
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)
|
# 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")
|
||||||
@@ -1166,6 +1315,12 @@ def main():
|
|||||||
p_log.add_argument("--start", default=None, help="Start date filter (YYYY-MM-DD)")
|
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)")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.command == "morning":
|
if args.command == "morning":
|
||||||
@@ -1182,6 +1337,8 @@ def main():
|
|||||||
cmd_simulate(args)
|
cmd_simulate(args)
|
||||||
elif args.command == "log":
|
elif args.command == "log":
|
||||||
cmd_log(args)
|
cmd_log(args)
|
||||||
|
elif args.command == "compare":
|
||||||
|
cmd_compare(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user