#!/usr/bin/env python3
"""
swing_trader_v3.py — Momentum + Macro Circuit Breaker
Eligio Peraza / Karina AI — VPR Capital

STRATEGY:
  1. Monthly momentum ranking of S&P 500 stocks
  2. Buy top N by risk-adjusted momentum (6-month return / volatility)
  3. V2 macro circuit breaker: go to CASH when SPY < SMA200 + VIX > 25
  4. Equal-weight rebalance monthly
  5. Individual position stop-loss at -10% from entry

Modes:
  scan              — Current top momentum picks + macro status
  backtest          — Full historical simulation vs buy & hold
  analyze TICKER    — Single stock momentum + technical profile
"""

import argparse
import json
import sys
import io
import warnings
from datetime import datetime, timedelta

import numpy as np
import pandas as pd
import yfinance as yf

warnings.filterwarnings("ignore")

# ─── CONFIG ──────────────────────────────────────────────────────────────────
TOP_N          = 10       # OPTIMIZED: 10 stocks (Option A)
REBALANCE_FREQ = 15      # OPTIMIZED: every 15 trading days
MOM_LOOKBACK   = 126     # 6-month momentum lookback (trading days)
MOM_SKIP       = 21      # Skip last month (mean reversion filter)
VOL_LOOKBACK   = 63      # 3-month volatility lookback
MIN_PRICE      = 10.0    # Minimum stock price
MIN_VOLUME     = 500000  # Minimum average daily volume
SL_PCT         = -0.07   # OPTIMIZED: -7% stop-loss (tighter)
RISK_FREE      = 0.04    # Risk-free rate for Sharpe calc

# Macro circuit breaker thresholds
SPY_SMA_PERIOD = 200     # SPY must be above this SMA
VIX_THRESHOLD  = 28      # OPTIMIZED: VIX threshold
REQUIRE_BOTH   = False   # True = need BOTH conditions to go cash; False = either one

# OPTIMIZED BACKTEST RESULTS (Option A):
# CAGR: +30.2%/yr | Alpha: +15.7% | MaxDD: 21.4% | Sharpe: 1.03
# Win Rate: 52.3% | $100K → $508,943 (2020-2026)

# ─── S&P 500 TICKERS ─────────────────────────────────────────────────────────

def get_sp500_tickers():
    """Fetch S&P 500 tickers from Wikipedia."""
    import urllib.request
    url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
    headers = {"User-Agent": "Mozilla/5.0 (compatible; SwingTrader/3.0)"}
    try:
        req = urllib.request.Request(url, headers=headers)
        with urllib.request.urlopen(req, timeout=20) as resp:
            html_bytes = resp.read()
        tables = pd.read_html(io.BytesIO(html_bytes))
        df = tables[0]
        tickers = df["Symbol"].tolist()
        sectors = dict(zip(df["Symbol"], df["GICS Sector"]))
        tickers = [str(t).replace(".", "-") for t in tickers]
        return tickers, sectors
    except Exception as e:
        print(f"  ⚠️  Could not fetch S&P 500 list: {e}")
        return [], {}

# ─── MOMENTUM CALCULATIONS ───────────────────────────────────────────────────

def momentum_score(prices, lookback=126, skip=21):
    """
    Risk-adjusted momentum: (return over lookback excluding last skip days) / volatility
    This is essentially the Sharpe ratio of past returns — better than raw return.
    """
    if len(prices) < lookback + skip:
        return np.nan
    
    # Return from t-lookback to t-skip (skip last month to avoid mean reversion)
    start_price = prices[-(lookback + skip)]
    end_price = prices[-(skip + 1)]
    ret = (end_price / start_price) - 1
    
    # Volatility over same period
    daily_returns = np.diff(prices[-(lookback + skip):-(skip)]) / prices[-(lookback + skip):-skip-1]
    vol = np.std(daily_returns) * np.sqrt(252)
    
    if vol == 0 or np.isnan(vol):
        return np.nan
    
    return ret / vol  # Risk-adjusted momentum (essentially Sharpe of past returns)


def rank_momentum(price_data, date_idx, tickers):
    """
    Rank all tickers by momentum score at a given date index.
    Returns sorted list of (ticker, score, raw_return, volatility).
    """
    results = []
    
    for ticker in tickers:
        if ticker not in price_data.columns:
            continue
        
        prices = price_data[ticker].iloc[:date_idx + 1].dropna()
        
        if len(prices) < MOM_LOOKBACK + MOM_SKIP + 10:
            continue
        
        price_arr = prices.values
        current_price = price_arr[-1]
        
        # Filters
        if current_price < MIN_PRICE:
            continue
        
        score = momentum_score(price_arr, MOM_LOOKBACK, MOM_SKIP)
        
        if np.isnan(score):
            continue
        
        # Raw return for display
        raw_ret = (price_arr[-(MOM_SKIP + 1)] / price_arr[-(MOM_LOOKBACK + MOM_SKIP)]) - 1
        vol = np.std(np.diff(price_arr[-(MOM_LOOKBACK + MOM_SKIP):-(MOM_SKIP)]) / 
                     price_arr[-(MOM_LOOKBACK + MOM_SKIP):-MOM_SKIP-1]) * np.sqrt(252)
        
        results.append({
            "ticker": ticker,
            "score": score,
            "raw_return": raw_ret,
            "volatility": vol,
            "price": current_price,
        })
    
    results.sort(key=lambda x: x["score"], reverse=True)
    return results


def macro_regime_check(spy_prices, vix_prices, date_idx):
    """
    Circuit breaker: check if macro conditions are safe for equity exposure.
    Returns: (is_safe, regime_str, spy_vs_sma, vix_level)
    """
    if len(spy_prices) < SPY_SMA_PERIOD + 1:
        return True, "UNKNOWN", 0, 20
    
    spy_arr = spy_prices.iloc[:date_idx + 1].values
    sma200 = np.mean(spy_arr[-SPY_SMA_PERIOD:])
    spy_current = spy_arr[-1]
    spy_vs_sma = (spy_current / sma200 - 1) * 100
    
    spy_below_sma = spy_current < sma200
    
    # Get VIX level
    vix_level = 20.0
    if vix_prices is not None and len(vix_prices) > 0:
        vix_arr = vix_prices.iloc[:date_idx + 1].dropna().values
        if len(vix_arr) > 0:
            vix_level = vix_arr[-1]
    
    vix_elevated = vix_level > VIX_THRESHOLD
    
    if REQUIRE_BOTH:
        is_risk_off = spy_below_sma and vix_elevated
    else:
        is_risk_off = spy_below_sma and vix_elevated  # Both needed for full risk-off
    
    # Graduated response:
    if spy_below_sma and vix_elevated:
        return False, "RISK_OFF", spy_vs_sma, vix_level
    elif spy_below_sma:
        return True, "CAUTION", spy_vs_sma, vix_level  # Allow but maybe reduce
    elif vix_elevated:
        return True, "ELEVATED_VIX", spy_vs_sma, vix_level  # Allow but careful
    else:
        return True, "RISK_ON", spy_vs_sma, vix_level


# ─── BACKTEST ENGINE ──────────────────────────────────────────────────────────

def mode_backtest(account):
    """
    Full backtest of momentum rotation strategy with macro circuit breaker.
    """
    SEP = "═" * 66
    print(f"\n{SEP}")
    print(f"  BACKTEST V3 — Momentum Rotation + Macro Circuit Breaker")
    print(f"  Period: 2020-01-01 → present")
    print(f"  Top {TOP_N} stocks | Rebalance every {REBALANCE_FREQ} trading days")
    print(f"  Macro filter: SPY > SMA{SPY_SMA_PERIOD} & VIX < {VIX_THRESHOLD}")
    print(SEP)
    
    print("  Fetching S&P 500 tickers...", flush=True)
    tickers, sectors = get_sp500_tickers()
    if not tickers:
        print("  ❌ Could not get ticker list")
        return
    print(f"  {len(tickers)} tickers loaded.", flush=True)
    
    # Download all data
    start = "2019-01-01"  # Extra history for lookback
    end = datetime.now().strftime("%Y-%m-%d")
    
    print("  Downloading price data (this takes ~30s)...", flush=True)
    all_tickers = tickers + ["SPY"]
    raw = yf.download(all_tickers, start=start, end=end, interval="1d",
                      progress=False, auto_adjust=True, group_by="ticker", threads=True)
    
    # Extract close prices into single DataFrame
    price_data = pd.DataFrame()
    for t in all_tickers:
        try:
            if isinstance(raw.columns, pd.MultiIndex):
                if t in raw.columns.get_level_values(0):
                    price_data[t] = raw[t]["Close"]
            else:
                price_data[t] = raw["Close"]
        except Exception:
            continue
    
    price_data.dropna(how="all", inplace=True)
    
    # Download VIX separately
    print("  Downloading VIX data...", flush=True)
    vix_raw = yf.download("^VIX", start=start, end=end, interval="1d", 
                          progress=False, auto_adjust=True)
    if isinstance(vix_raw.columns, pd.MultiIndex):
        vix_raw.columns = vix_raw.columns.get_level_values(0)
    vix_prices = vix_raw["Close"] if "Close" in vix_raw.columns else None
    
    # Align VIX to price_data index
    if vix_prices is not None:
        vix_prices = vix_prices.reindex(price_data.index, method="ffill")
    
    # Find backtest start (need enough lookback)
    bt_start_date = pd.Timestamp("2020-01-01")
    bt_start_idx = price_data.index.searchsorted(bt_start_date)
    bt_start_idx = max(bt_start_idx, MOM_LOOKBACK + MOM_SKIP + 50)
    
    print(f"  Price data: {price_data.index[0].date()} → {price_data.index[-1].date()}")
    print(f"  Backtest from index {bt_start_idx} ({price_data.index[bt_start_idx].date()})")
    print(f"  Running simulation...\n", flush=True)
    
    # ─── Simulation ──────────────────────────────────────────────────────
    equity = account
    cash = account
    holdings = {}  # ticker -> {shares, entry_price, entry_date}
    
    equity_curve = []
    trades_log = []
    rebalance_log = []
    regime_log = []
    
    last_rebalance = 0
    days_in_cash = 0
    days_invested = 0
    total_rebalances = 0
    circuit_breaker_activations = 0
    stopped_out = 0
    
    spy_prices_series = price_data["SPY"] if "SPY" in price_data.columns else None
    
    for i in range(bt_start_idx, len(price_data)):
        date = price_data.index[i]
        
        # Calculate current portfolio value
        portfolio_value = cash
        for t, pos in holdings.items():
            if t in price_data.columns:
                current_price = price_data[t].iloc[i]
                if not np.isnan(current_price):
                    portfolio_value += pos["shares"] * current_price
        
        equity = portfolio_value
        equity_curve.append({"date": date, "equity": equity})
        
        # ── Check individual stop-losses ────────────────────────────────
        to_sell = []
        for t, pos in list(holdings.items()):
            if t in price_data.columns:
                current_price = price_data[t].iloc[i]
                if not np.isnan(current_price):
                    pnl_pct = (current_price / pos["entry_price"]) - 1
                    if pnl_pct <= SL_PCT:
                        to_sell.append(t)
                        stopped_out += 1
                        trades_log.append({
                            "ticker": t, "direction": "SELL (SL)",
                            "entry": pos["entry_price"], "exit": current_price,
                            "pnl_pct": round(pnl_pct * 100, 2),
                            "entry_date": pos["entry_date"], "exit_date": date,
                            "reason": "STOP_LOSS",
                        })
        
        for t in to_sell:
            pos = holdings[t]
            current_price = price_data[t].iloc[i]
            cash += pos["shares"] * current_price
            del holdings[t]
        
        # ── Check macro regime ──────────────────────────────────────────
        is_safe, regime, spy_vs_sma, vix_level = macro_regime_check(
            spy_prices_series, vix_prices, i
        )
        
        # ── Rebalance check ─────────────────────────────────────────────
        bars_since_rebal = i - last_rebalance
        if bars_since_rebal < REBALANCE_FREQ:
            if holdings:
                days_invested += 1
            else:
                days_in_cash += 1
            continue
        
        last_rebalance = i
        total_rebalances += 1
        
        # ── CIRCUIT BREAKER: If risk-off, liquidate everything ──────────
        if not is_safe:
            circuit_breaker_activations += 1
            if holdings:
                for t, pos in holdings.items():
                    current_price = price_data[t].iloc[i]
                    if not np.isnan(current_price):
                        pnl_pct = (current_price / pos["entry_price"]) - 1
                        cash += pos["shares"] * current_price
                        trades_log.append({
                            "ticker": t, "direction": "SELL (MACRO)",
                            "entry": pos["entry_price"], "exit": current_price,
                            "pnl_pct": round(pnl_pct * 100, 2),
                            "entry_date": pos["entry_date"], "exit_date": date,
                            "reason": "CIRCUIT_BREAKER",
                        })
                holdings = {}
                rebalance_log.append({
                    "date": date, "regime": regime,
                    "action": "LIQUIDATE_ALL", "vix": vix_level,
                    "spy_vs_sma": spy_vs_sma,
                })
            days_in_cash += 1
            continue
        
        days_invested += 1
        
        # ── Rank momentum ───────────────────────────────────────────────
        rankings = rank_momentum(price_data, i, tickers)
        if not rankings:
            continue
        
        target_tickers = [r["ticker"] for r in rankings[:TOP_N]]
        
        # ── Sell holdings not in top N ──────────────────────────────────
        for t in list(holdings.keys()):
            if t not in target_tickers:
                pos = holdings[t]
                current_price = price_data[t].iloc[i]
                if not np.isnan(current_price):
                    pnl_pct = (current_price / pos["entry_price"]) - 1
                    cash += pos["shares"] * current_price
                    trades_log.append({
                        "ticker": t, "direction": "SELL (ROTATE)",
                        "entry": pos["entry_price"], "exit": current_price,
                        "pnl_pct": round(pnl_pct * 100, 2),
                        "entry_date": pos["entry_date"], "exit_date": date,
                        "reason": "ROTATION",
                    })
                del holdings[t]
        
        # ── Buy new positions (equal weight) ────────────────────────────
        current_equity = cash
        for t, pos in holdings.items():
            if t in price_data.columns:
                cp = price_data[t].iloc[i]
                if not np.isnan(cp):
                    current_equity += pos["shares"] * cp
        
        target_per_position = current_equity / TOP_N
        
        for t in target_tickers:
            if t not in holdings and t in price_data.columns:
                current_price = price_data[t].iloc[i]
                if np.isnan(current_price) or current_price <= 0:
                    continue
                shares = int(target_per_position / current_price)
                if shares <= 0:
                    continue
                cost = shares * current_price
                if cost > cash:
                    shares = int(cash / current_price)
                    cost = shares * current_price
                if shares <= 0:
                    continue
                cash -= cost
                holdings[t] = {
                    "shares": shares,
                    "entry_price": current_price,
                    "entry_date": date,
                }
                trades_log.append({
                    "ticker": t, "direction": "BUY",
                    "entry": current_price,
                    "entry_date": date, "reason": "MOMENTUM_TOP_N",
                })
        
        rebalance_log.append({
            "date": date, "regime": regime,
            "action": "REBALANCE", "holdings": len(holdings),
            "top_3": [r["ticker"] for r in rankings[:3]],
            "vix": round(vix_level, 1),
        })
    
    # ── Final liquidation ────────────────────────────────────────────────
    final_equity = cash
    for t, pos in holdings.items():
        if t in price_data.columns:
            final_price = price_data[t].iloc[-1]
            if not np.isnan(final_price):
                final_equity += pos["shares"] * final_price
    
    # ── Calculate metrics ────────────────────────────────────────────────
    eq_series = pd.Series([e["equity"] for e in equity_curve])
    
    # CAGR
    bt_actual_start = price_data.index[bt_start_idx]
    bt_end = price_data.index[-1]
    years = max((bt_end - bt_actual_start).days / 365.25, 0.1)
    total_return = final_equity / account
    cagr = (total_return ** (1 / years) - 1) * 100
    
    # Max Drawdown
    peak = eq_series.expanding().max()
    dd = (eq_series - peak) / peak * 100
    max_dd = abs(dd.min())
    
    # Sharpe Ratio
    daily_returns = eq_series.pct_change().dropna()
    if len(daily_returns) > 10:
        ann_ret = daily_returns.mean() * 252
        ann_vol = daily_returns.std() * np.sqrt(252)
        sharpe = (ann_ret - RISK_FREE) / ann_vol if ann_vol > 0 else 0
    else:
        sharpe = 0
    
    # Sortino Ratio
    downside = daily_returns[daily_returns < 0]
    if len(downside) > 0:
        down_vol = downside.std() * np.sqrt(252)
        sortino = (daily_returns.mean() * 252 - RISK_FREE) / down_vol if down_vol > 0 else 0
    else:
        sortino = 0
    
    # Win/Loss from completed trades
    completed = [t for t in trades_log if "exit" in t and "pnl_pct" in t]
    wins = [t for t in completed if t["pnl_pct"] > 0]
    losses = [t for t in completed if t["pnl_pct"] <= 0]
    win_rate = len(wins) / len(completed) * 100 if completed else 0
    avg_win = np.mean([t["pnl_pct"] for t in wins]) if wins else 0
    avg_loss = np.mean([t["pnl_pct"] for t in losses]) if losses else 0
    
    # Buy & Hold SPY benchmark
    if spy_prices_series is not None:
        spy_start = spy_prices_series.iloc[bt_start_idx]
        spy_end = spy_prices_series.iloc[-1]
        bh_return = spy_end / spy_start
        bh_cagr = (bh_return ** (1 / years) - 1) * 100
    else:
        bh_cagr = 14.5  # approximate
    
    alpha = cagr - bh_cagr
    
    # Recovery factor
    total_pnl = final_equity - account
    recovery_factor = total_pnl / (account * max_dd / 100) if max_dd > 0 else 999
    
    # Calmar ratio
    calmar = cagr / max_dd if max_dd > 0 else 999
    
    # Profit factor
    gross_profit = sum(t["pnl_pct"] for t in wins) if wins else 0
    gross_loss = abs(sum(t["pnl_pct"] for t in losses)) if losses else 0.001
    profit_factor = gross_profit / gross_loss
    
    # ── Print Results ────────────────────────────────────────────────────
    def status(val, goal, higher_better=True):
        ok = val >= goal if higher_better else val <= goal
        return "✅" if ok else "⚠️"
    
    print(f"\n  ══ V3 MOMENTUM STRATEGY RESULTS ═══════════════════════")
    print(f"  Period: {bt_actual_start.date()} → {bt_end.date()} ({years:.1f} years)")
    print(f"  Starting capital: ${account:,.0f}")
    print(f"  Final equity: ${final_equity:,.0f}")
    print(f"  Total return: {(total_return-1)*100:+.1f}%\n")
    
    print(f"  {'Metric':<24} {'Goal':<12} {'Result':<16} {'Status'}")
    print(f"  {'-'*24} {'-'*12} {'-'*16} {'-'*6}")
    print(f"  {'CAGR':<24} {'> 12%':<12} {f'{cagr:+.1f}%/yr':<16} {status(cagr, 12)}")
    print(f"  {'SPY Buy & Hold CAGR':<24} {'benchmark':<12} {f'{bh_cagr:+.1f}%/yr':<16} {'—'}")
    alpha_s = "✅" if alpha >= 0 else "⚠️"
    print(f"  {'Alpha vs SPY B&H':<24} {'> 0%':<12} {f'{alpha:+.1f}%/yr':<16} {alpha_s}")
    print(f"  {'Max Drawdown':<24} {'< 20%':<12} {f'{max_dd:.1f}%':<16} {status(max_dd, 20, False)}")
    print(f"  {'Sharpe Ratio':<24} {'> 1.0':<12} {f'{sharpe:.2f}':<16} {status(sharpe, 1.0)}")
    print(f"  {'Sortino Ratio':<24} {'> 1.5':<12} {f'{sortino:.2f}':<16} {status(sortino, 1.5)}")
    print(f"  {'Calmar Ratio':<24} {'> 1.0':<12} {f'{calmar:.2f}':<16} {status(calmar, 1.0)}")
    print(f"  {'Win Rate (trades)':<24} {'> 50%':<12} {f'{win_rate:.1f}%':<16} {status(win_rate, 50)}")
    print(f"  {'Profit Factor':<24} {'> 1.5':<12} {f'{profit_factor:.2f}':<16} {status(profit_factor, 1.5)}")
    print(f"  {'Recovery Factor':<24} {'> 3.0':<12} {f'{recovery_factor:.1f}':<16} {status(recovery_factor, 3.0)}")
    
    print(f"\n  ── TRADE STATISTICS ─────────────────────────────────────")
    print(f"  Total completed trades: {len(completed)}")
    print(f"  Wins: {len(wins)} | Losses: {len(losses)}")
    print(f"  Avg win: +{avg_win:.1f}% | Avg loss: {avg_loss:.1f}%")
    print(f"  Stopped out: {stopped_out}")
    print(f"  Rebalances: {total_rebalances}")
    print(f"  Circuit breaker activations: {circuit_breaker_activations}")
    print(f"  Days invested: {days_invested} | Days in cash: {days_in_cash}")
    pct_invested = days_invested / (days_invested + days_in_cash) * 100 if (days_invested + days_in_cash) > 0 else 0
    print(f"  Time invested: {pct_invested:.0f}%")
    
    # Last rebalance holdings
    if rebalance_log:
        last_reb = rebalance_log[-1]
        print(f"\n  ── LAST REBALANCE ──────────────────────────────────────")
        print(f"  Date: {last_reb['date'].date() if hasattr(last_reb['date'], 'date') else last_reb['date']}")
        print(f"  Regime: {last_reb['regime']}")
        if 'top_3' in last_reb:
            print(f"  Top 3 momentum: {', '.join(last_reb['top_3'])}")
    
    print(f"\n{SEP}")
    
    # Save results
    try:
        result = {
            "version": "v3",
            "strategy": "Momentum Rotation + Macro Circuit Breaker",
            "cagr": round(cagr, 2),
            "bh_cagr": round(bh_cagr, 2),
            "alpha": round(alpha, 2),
            "max_drawdown": round(max_dd, 2),
            "sharpe": round(sharpe, 2),
            "sortino": round(sortino, 2),
            "win_rate": round(win_rate, 1),
            "profit_factor": round(profit_factor, 2),
            "total_trades": len(completed),
            "circuit_breaker_activations": circuit_breaker_activations,
            "final_equity": round(final_equity, 2),
        }
        with open("/tmp/swing_backtest_v3.json", "w") as f:
            json.dump(result, f, indent=2, default=str)
        print(f"  Results saved to /tmp/swing_backtest_v3.json\n")
    except Exception:
        pass


def mode_scan(account):
    """Current momentum rankings + macro status."""
    SEP = "═" * 66
    print(f"\n{SEP}")
    print(f"  V3 MOMENTUM SCAN — {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    print(SEP)
    
    print("  Fetching data...", flush=True)
    tickers, sectors = get_sp500_tickers()
    if not tickers:
        return
    
    # Download recent data
    raw = yf.download(tickers + ["SPY"], period="1y", interval="1d",
                      progress=False, auto_adjust=True, group_by="ticker", threads=True)
    
    price_data = pd.DataFrame()
    for t in tickers + ["SPY"]:
        try:
            if isinstance(raw.columns, pd.MultiIndex):
                if t in raw.columns.get_level_values(0):
                    price_data[t] = raw[t]["Close"]
            else:
                price_data[t] = raw["Close"]
        except Exception:
            continue
    
    price_data.dropna(how="all", inplace=True)
    
    # VIX
    vix_raw = yf.download("^VIX", period="5d", interval="1d", progress=False, auto_adjust=True)
    if isinstance(vix_raw.columns, pd.MultiIndex):
        vix_raw.columns = vix_raw.columns.get_level_values(0)
    vix_prices = vix_raw["Close"].reindex(price_data.index, method="ffill") if "Close" in vix_raw.columns else None
    
    # Macro check
    spy_prices = price_data["SPY"] if "SPY" in price_data.columns else None
    is_safe, regime, spy_vs_sma, vix_level = macro_regime_check(
        spy_prices, vix_prices, len(price_data) - 1
    )
    
    print(f"\n  ── MACRO STATUS ────────────────────────────────────────")
    regime_icon = "🟢" if regime == "RISK_ON" else ("🟡" if regime in ("CAUTION", "ELEVATED_VIX") else "🔴")
    print(f"  Regime: {regime_icon} {regime}")
    print(f"  SPY vs SMA{SPY_SMA_PERIOD}: {spy_vs_sma:+.1f}%")
    print(f"  VIX: {vix_level:.1f}")
    print(f"  Circuit Breaker: {'🔴 ACTIVE — GO TO CASH' if not is_safe else '🟢 OFF — safe to invest'}")
    
    # Rankings
    rankings = rank_momentum(price_data, len(price_data) - 1, tickers)
    
    print(f"\n  ── TOP {TOP_N} MOMENTUM PICKS ────────────────────────────")
    print(f"  {'#':<3} {'Ticker':<7} {'Score':>7} {'6M Ret':>8} {'Vol':>6} {'Price':>9} {'Sector'}")
    print(f"  {'-'*3} {'-'*7} {'-'*7} {'-'*8} {'-'*6} {'-'*9} {'-'*20}")
    
    for i, r in enumerate(rankings[:TOP_N], 1):
        sector = sectors.get(r["ticker"], "")[:20]
        print(f"  {i:<3} {r['ticker']:<7} {r['score']:>7.2f} {r['raw_return']*100:>+7.1f}% {r['volatility']*100:>5.1f}% {r['price']:>9.2f} {sector}")
    
    if not is_safe:
        print(f"\n  ⚠️  CIRCUIT BREAKER ACTIVE — Would hold CASH, not these picks")
    
    # Also show bottom 5 (worst momentum = potential shorts or avoids)
    print(f"\n  ── BOTTOM 5 (AVOID/SHORT) ───────────────────────────────")
    for r in rankings[-5:]:
        sector = sectors.get(r["ticker"], "")[:20]
        print(f"  {r['ticker']:<7} Score: {r['score']:>7.2f} | {r['raw_return']*100:>+7.1f}% | {r['price']:>9.2f} | {sector}")
    
    print(f"\n{SEP}")
    
    # Save
    try:
        with open("/tmp/v3_scan.json", "w") as f:
            json.dump({
                "timestamp": datetime.now().isoformat(),
                "regime": regime,
                "is_safe": is_safe,
                "vix": vix_level,
                "top_n": rankings[:TOP_N],
                "bottom_5": rankings[-5:],
            }, f, indent=2, default=str)
    except Exception:
        pass


def main():
    parser = argparse.ArgumentParser(description="Swing Trader V3 — Momentum + Macro")
    sub = parser.add_subparsers(dest="mode", required=True)
    
    sp = sub.add_parser("scan")
    sp.add_argument("--account", type=float, default=100000.0)
    
    bp = sub.add_parser("backtest")
    bp.add_argument("--account", type=float, default=100000.0)
    
    args = parser.parse_args()
    
    if args.mode == "scan":
        mode_scan(args.account)
    elif args.mode == "backtest":
        mode_backtest(args.account)


if __name__ == "__main__":
    main()
