diff --git a/trader.py b/trader.py index d5f5df4..511f229 100644 --- a/trader.py +++ b/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__":