#!/usr/bin/env python3
"""
swing_trader.py — EWT + DeMark Swing Trading Agent
Eligio Peraza / Karina AI — VPR Capital

Modes:
  scan              — Scan watchlist, print only ACTIONABLE setups with entry/exit
  analyze TICKER    — Deep single-asset analysis
  backtest TICKER [YEARS] — Historical stress test with accuracy metrics

Data: Yahoo Finance (yfinance)
Timeframes: Weekly (trend), Daily (signal), 4H (execution)
"""

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

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

warnings.filterwarnings("ignore")

# ─── CONFIG ──────────────────────────────────────────────────────────────────
WATCHLIST = ["SPY", "QQQ", "IWB", "IWM", "BTC-USD", "ETH-USD", "EURUSD=X", "GBPUSD=X", "JPY=X"]
RISK_PCT   = 0.015   # 1.5% account risk per trade
RR_MIN     = 3.0     # minimum Reward:Risk ratio
FIB_EXT    = 1.618   # Fibonacci extension for TP2
PROB_THRESHOLD = 60  # minimum probability % to flag as actionable

# ─── DATA ────────────────────────────────────────────────────────────────────

def fetch(ticker, period="1y", interval="1d"):
    df = yf.download(ticker, period=period, interval=interval, progress=False, auto_adjust=True)
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
    df.dropna(inplace=True)
    return df

def fetch_weekly(t): return fetch(t, "2y", "1wk")
def fetch_daily(t):  return fetch(t, "1y", "1d")
def fetch_4h(t):
    df = fetch(t, "60d", "1h")
    if df.empty: return df
    # Resample 1h → 4h
    df = df.resample("4h").agg({"Open":"first","High":"max","Low":"min","Close":"last","Volume":"sum"}).dropna()
    return df

# ─── INDICATORS ───────────────────────────────────────────────────────────────

def atr(df, period=14):
    try:
        h, l, c = df["High"], df["Low"], df["Close"]
        tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1)
        val = tr.rolling(period).mean().iloc[-1]
        if pd.isna(val):
            # fallback: simple range average
            val = (df["High"] - df["Low"]).tail(period).mean()
        return float(val)
    except Exception:
        return float((df["High"] - df["Low"]).mean())

def sma(df, n):
    return df["Close"].rolling(n).mean()

def td_sequential(df):
    """
    Calculate TD Sequential buy/sell setup counts.
    Returns: (buy_count, sell_count, completed_9_buy, completed_9_sell,
              completed_13_buy, completed_13_sell, recycled)
    """
    close = df["Close"].values
    n = len(close)
    buy_count = 0
    sell_count = 0
    completed_9_buy  = False
    completed_9_sell = False

    # Setup phase: compare close to close 4 bars ago
    buy_counts  = []
    sell_counts = []
    bc = sc = 0
    for i in range(4, n):
        if close[i] < close[i-4]:
            bc += 1; sc = 0
        elif close[i] > close[i-4]:
            sc += 1; bc = 0
        else:
            bc = max(0, bc); sc = max(0, sc)
        buy_counts.append(bc)
        sell_counts.append(sc)

    buy_count  = buy_counts[-1]  if buy_counts  else 0
    sell_count = sell_counts[-1] if sell_counts else 0

    # Was a 9 recently completed (within last 5 bars)?
    recent_buy  = buy_counts[-5:]  if len(buy_counts) >= 5 else buy_counts
    recent_sell = sell_counts[-5:] if len(sell_counts) >= 5 else sell_counts
    completed_9_buy  = any(v >= 9 for v in recent_buy)
    completed_9_sell = any(v >= 9 for v in recent_sell)

    # Countdown (simplified 13-count check)
    completed_13_buy  = any(v >= 13 for v in buy_counts[-15:])
    completed_13_sell = any(v >= 13 for v in sell_counts[-15:])

    # Recycled: count went past 9 without a price flip
    recycled = buy_count > 13 or sell_count > 13

    return {
        "buy_count": int(buy_count),
        "sell_count": int(sell_count),
        "completed_9_buy":   completed_9_buy,
        "completed_9_sell":  completed_9_sell,
        "completed_13_buy":  completed_13_buy,
        "completed_13_sell": completed_13_sell,
        "recycled": recycled,
    }

def price_flip(df, bars=2):
    """Check if price direction flipped in the last N bars."""
    c = df["Close"].values[-bars-1:]
    if len(c) < bars+1: return False
    dirs = [1 if c[i] > c[i-1] else -1 for i in range(1, len(c))]
    return dirs[0] != dirs[-1]

def trend_phase(df_weekly, df_daily):
    """
    Simplified EWT phase detection.
    Returns: (weekly_phase, daily_phase, bias)
    """
    wma20 = sma(df_weekly, 20).iloc[-1]
    wma50 = sma(df_weekly, 50).iloc[-1]
    dma20 = sma(df_daily, 20).iloc[-1]
    dma50 = sma(df_daily, 50).iloc[-1]
    dma20_prev = sma(df_daily, 20).iloc[-5]
    w_close = df_weekly["Close"].iloc[-1]
    d_close = df_daily["Close"].iloc[-1]

    # Weekly: position relative to MAs
    if w_close > wma20 > wma50:
        w_phase = "impulse_up"
    elif w_close < wma20 < wma50:
        w_phase = "impulse_down"
    elif wma20 > wma50 and w_close < wma20:
        w_phase = "correction_in_uptrend"
    else:
        w_phase = "transition"

    # Daily slope
    d_slope = (dma20 - dma20_prev) / dma20_prev * 100 if dma20_prev else 0

    if d_close > dma20 > dma50:
        d_phase = "uptrend"
    elif d_close < dma20 < dma50:
        d_phase = "downtrend"
    elif d_slope > 0.1:
        d_phase = "accumulation"
    elif d_slope < -0.1:
        d_phase = "distribution"
    else:
        d_phase = "sideways"

    # Bias
    if w_phase in ("impulse_up", "correction_in_uptrend") and d_phase in ("uptrend", "accumulation"):
        bias = "BULLISH"
    elif w_phase == "impulse_down" and d_phase in ("downtrend", "distribution"):
        bias = "BEARISH"
    else:
        bias = "NEUTRAL"

    return w_phase, d_phase, bias

def fibonacci_levels(df, lookback=60):
    """
    Identify swing high/low in lookback window and compute Fibonacci levels.
    Returns: (swing_low, swing_high, fib_50, fib_618, fib_1618_ext)
    """
    subset = df.tail(lookback)
    swing_low  = subset["Low"].min()
    swing_high = subset["High"].max()
    rng = swing_high - swing_low
    if rng == 0:
        return swing_low, swing_high, swing_low, swing_low, swing_high
    fib_50  = swing_high - 0.500 * rng
    fib_618 = swing_high - 0.618 * rng
    fib_ext = swing_low  + FIB_EXT * rng   # 1.618 extension from low
    return swing_low, swing_high, fib_50, fib_618, fib_ext

def support_resistance(df, lookback=30):
    """Simple pivot-based S/R."""
    subset = df.tail(lookback)
    resistance = subset["High"].rolling(5, center=True).max().dropna()
    support    = subset["Low"].rolling(5, center=True).min().dropna()
    key_resistance = resistance.nlargest(3).mean()
    key_support    = support.nsmallest(3).mean()
    return key_support, key_resistance

# ─── SIGNAL LOGIC ─────────────────────────────────────────────────────────────

def build_signal(ticker, df_weekly, df_daily, df_4h, account):
    """
    Apply all logic gates and return a trade idea dict (or None).
    """
    if df_daily.empty or df_weekly.empty:
        return None

    current_price = float(df_daily["Close"].iloc[-1])
    atr14 = atr(df_daily)

    # Gate 1 – Market Phase
    w_phase, d_phase, bias = trend_phase(df_weekly, df_daily)

    # Gate 2 – TD Sequential
    td_d  = td_sequential(df_daily)
    td_4h = td_sequential(df_4h) if not df_4h.empty else td_d

    # Gate 3 – Fibonacci & S/R
    swing_low, swing_high, fib_50, fib_618, fib_ext = fibonacci_levels(df_daily)
    key_support, key_resistance = support_resistance(df_daily)

    # Gate 4 – Price Flip
    flip = price_flip(df_daily)

    # Gate 5 – Volume Confirmation (above 20-day avg volume = institutional participation)
    has_volume = True  # default True for forex/crypto (no volume data)
    if "Volume" in df_daily.columns and df_daily["Volume"].sum() > 0:
        avg_vol = df_daily["Volume"].rolling(20).mean().iloc[-1]
        last_vol = df_daily["Volume"].iloc[-1]
        has_volume = last_vol >= avg_vol * 0.9  # within 10% of avg vol

    # ─ Determine direction ──────────────────────────────────────────────────
    direction = None
    entry_score = 0

    # LONG conditions (6 gates total)
    long_cond = [
        bias == "BULLISH",                                      # Gate 1: EWT weekly phase
        d_phase in ("accumulation", "uptrend", "sideways"),    # Gate 1b: daily phase
        td_d["completed_9_buy"] or td_4h["completed_9_buy"],  # Gate 2: TD-9 exhaustion
        current_price <= fib_618 * 1.02,                      # Gate 3: Fib retracement zone
        current_price >= key_support * 0.98,                  # Gate 3b: structural support
        flip or has_volume,                                    # Gate 4/5: flip OR volume
    ]
    short_cond = [
        bias == "BEARISH",                                      # Gate 1
        d_phase in ("distribution", "downtrend", "sideways"),  # Gate 1b
        td_d["completed_9_sell"] or td_4h["completed_9_sell"], # Gate 2
        current_price >= fib_50 * 0.98,                        # Gate 3
        current_price <= key_resistance * 1.02,                # Gate 3b
        flip or has_volume,                                    # Gate 4/5
    ]

    long_score  = sum(long_cond)
    short_score = sum(short_cond)

    # Weekly exhaustion override (Gate 1 reject rule)
    weekly_exhausted = w_phase == "impulse_up" and (td_sequential(df_weekly)["completed_13_buy"])

    # Anti-SHORT filter: block short signals when Weekly is in strong uptrend
    weekly_impulse_up = w_phase in ("impulse_up", "correction_in_uptrend")

    # MANDATORY hard gates (not scored — must ALL be true)
    fib_confluence_long  = current_price <= fib_618 * 1.02
    fib_confluence_short = current_price >= fib_50 * 0.98

    # Raised threshold: 4/6 gates required (was 3/5)
    if long_score >= 4 and not weekly_exhausted and fib_confluence_long and flip:
        direction   = "LONG"
        entry_score = long_score
    elif short_score >= 4 and not weekly_impulse_up and fib_confluence_short and flip:
        direction   = "SHORT"
        entry_score = short_score

    if direction is None:
        return {"ticker": ticker, "direction": "NEUTRAL", "actionable": False,
                "current_price": current_price, "bias": bias,
                "td_daily": td_d, "recycled": td_d["recycled"] or td_4h["recycled"],
                "w_phase": w_phase, "d_phase": d_phase}

    # ─ Entry / Exit levels ──────────────────────────────────────────────────
    if direction == "LONG":
        entry  = round(current_price, 4)
        sl     = round(min(swing_low, current_price - atr14 * 1.5), 4)  # structural low or 1.5 ATR below
        risk   = abs(entry - sl)
        tp1    = round(entry + risk * 3.0, 4)   # 3:1 R:R (realistic swing target)
        tp2    = round(entry + risk * 5.0, 4)   # 5:1 R:R (TD-13 zone)
    else:
        entry  = round(current_price, 4)
        sl     = round(max(swing_high, current_price + atr14 * 1.5), 4)
        risk   = abs(sl - entry)
        tp1    = round(entry - risk * 3.0, 4)   # 3:1 R:R
        tp2    = round(entry - risk * 5.0, 4)   # 5:1 R:R

    risk   = abs(entry - sl)
    reward = abs(tp1 - entry)
    rr     = round(reward / risk, 2) if risk > 0 else 0

    # Reject if R:R < minimum
    if rr < RR_MIN:
        return {"ticker": ticker, "direction": "NEUTRAL", "actionable": False,
                "current_price": current_price, "bias": bias,
                "td_daily": td_d, "recycled": td_d["recycled"],
                "rr_rejected": True, "rr": rr,
                "w_phase": w_phase, "d_phase": d_phase}

    # Position sizing (1.5% account risk)
    volatility_mult = 1.5 if "BTC" in ticker or "ETH" in ticker else (1.2 if "USD" in ticker else 1.0)
    risk_adj = risk * volatility_mult
    shares   = int((account * RISK_PCT) / risk_adj) if risk_adj else 0
    pos_size = round(shares * entry, 2)

    # Probability score (heuristic: 40 base + 10 per gate hit + bonuses)
    prob = 40 + entry_score * 10
    if flip:               prob += 5
    if not td_d["recycled"]: prob += 5
    prob = min(prob, 95)

    recycled = td_d["recycled"] or td_4h["recycled"]

    return {
        "ticker":       ticker,
        "direction":    direction,
        "actionable":   prob >= PROB_THRESHOLD,
        "current_price": current_price,
        "bias":         bias,
        "w_phase":      w_phase,
        "d_phase":      d_phase,
        "atr":          round(atr14, 4),
        "td_daily":     td_d,
        "td_4h":        td_4h,
        "price_flip":   flip,
        "key_support":  round(key_support, 4),
        "key_resistance": round(key_resistance, 4),
        "fib_50":       round(fib_50, 4),
        "fib_618":      round(fib_618, 4),
        "fib_ext":      round(fib_ext, 4),
        "entry":        entry,
        "stop_loss":    sl,
        "take_profit_1": tp1,
        "take_profit_2": tp2,
        "rr":           rr,
        "shares":       shares,
        "position_size": pos_size,
        "probability":  prob,
        "recycled":     recycled,
        "holding_days_target": "2-8 days",
    }

# ─── OUTPUT FORMATTING ────────────────────────────────────────────────────────

SEP = "═" * 62

def fmt_price(p, ticker):
    if "JPY" in ticker: return f"{p:.3f}"
    if "USD" in ticker and "BTC" not in ticker and "ETH" not in ticker: return f"{p:.4f}"
    return f"{p:.2f}"

def print_signal(sig, account):
    t = sig["ticker"]
    p = sig["current_price"]
    td = sig.get("td_daily", {})
    print(f"\n{SEP}")
    print(f"  {'📊 ANALYSIS':}  {t}")
    print(f"{SEP}")
    print(f"\n  ── CURRENT MARKET FACTS ─────────────────────────────")
    print(f"  Price:          {fmt_price(p, t)}")
    print(f"  ATR(14):        {sig.get('atr', 'N/A')}")
    print(f"  Overall Bias:   {sig.get('bias', 'N/A')}")
    print(f"  Weekly Trend:   {sig.get('w_phase', 'N/A')}")
    print(f"  Daily Trend:    {sig.get('d_phase', 'N/A')}")
    print(f"  Key Support:    {fmt_price(sig.get('key_support', 0), t)}")
    print(f"  Key Resistance: {fmt_price(sig.get('key_resistance', 0), t)}")
    print(f"  Fib 50%:        {fmt_price(sig.get('fib_50', 0), t)}")
    print(f"  Fib 61.8%:      {fmt_price(sig.get('fib_618', 0), t)}")
    print(f"  TD Daily:       Buy={td.get('buy_count',0)}  Sell={td.get('sell_count',0)}")
    print(f"  Completed 9:    Buy={td.get('completed_9_buy',False)}  Sell={td.get('completed_9_sell',False)}")
    print(f"  Price Flip:     {sig.get('price_flip', False)}")
    if sig.get("recycled"):
        print(f"  ⚠️  RECYCLED COUNT RISK — momentum may override signal")

    d = sig.get("direction", "NEUTRAL")
    print(f"\n  ── PROJECTED OUTCOME  [Possible Outcome — Not a Guarantee] ──")
    if d == "NEUTRAL":
        print(f"  No actionable setup at this time.")
        if sig.get("rr_rejected"):
            print(f"  ❌ R:R of {sig.get('rr', 0):.2f} is below minimum {RR_MIN:.1f}")
    else:
        arrow = "🟢 LONG" if d == "LONG" else "🔴 SHORT"
        print(f"  Signal:         {arrow}")
        print(f"  ────────────────────────────────────────────────────")
        print(f"  🎯 ENTRY:       {fmt_price(sig['entry'], t)}")
        print(f"  🛑 STOP LOSS:   {fmt_price(sig['stop_loss'], t)}  ← Wave invalidation")
        print(f"  💰 TAKE PROFIT 1 (1.618 Fib): {fmt_price(sig['take_profit_1'], t)}")
        print(f"  💰 TAKE PROFIT 2 (TD-13 zone): {fmt_price(sig['take_profit_2'], t)}")
        print(f"  ────────────────────────────────────────────────────")
        print(f"  R:R Ratio:      {sig['rr']:.2f}:1  ({'✅ PASS' if sig['rr'] >= RR_MIN else '❌ FAIL'})")
        print(f"  Position Size:  {sig['shares']} units  (${sig['position_size']:,.0f})")
        print(f"  Risk Amount:    ${account * RISK_PCT:,.0f}  (1.5% of ${account:,.0f})")
        print(f"  Holding Target: {sig['holding_days_target']}")
        print(f"  Probability:    {sig['probability']}%")
    print(f"{SEP}\n")

# ─── MODES ────────────────────────────────────────────────────────────────────

def mode_scan(account):
    now = datetime.now()
    print(f"\n{SEP}")
    print(f"  SWING TRADER — SCAN MODE")
    print(f"  {now.strftime('%Y-%m-%d %H:%M')}  |  Account: ${account:,.0f}")
    print(SEP)
    actionable = []
    for t in WATCHLIST:
        sys.stdout.write(f"  Scanning {t}... ")
        sys.stdout.flush()
        try:
            sig = build_signal(t, fetch_weekly(t), fetch_daily(t), fetch_4h(t), account)
            if sig and sig["actionable"]:
                print(f"{'LONG ✅' if sig['direction']=='LONG' else 'SHORT 🔴'} — {sig['probability']}%")
                actionable.append(sig)
            else:
                bias = sig.get("bias", "N/A") if sig else "ERR"
                recycled_flag = "  [!RECYCLED]" if sig and sig.get("recycled") else ""
                print(f"neutral{recycled_flag}")
        except Exception as e:
            print(f"error: {e}")
    print(f"\n{SEP}")
    if not actionable:
        print("  No actionable setups found at this time.")
        print(SEP)
        return
    print(f"  {len(actionable)} ACTIONABLE SETUP(S) FOUND:")
    print(SEP)
    for sig in actionable:
        t = sig["ticker"]
        d = "🟢 LONG" if sig["direction"]=="LONG" else "🔴 SHORT"
        print(f"\n  {d}  {t}  @  {fmt_price(sig['entry'], t)}")
        print(f"    🎯 Entry:   {fmt_price(sig['entry'], t)}")
        print(f"    🛑 SL:      {fmt_price(sig['stop_loss'], t)}")
        print(f"    💰 TP1:     {fmt_price(sig['take_profit_1'], t)}")
        print(f"    💰 TP2:     {fmt_price(sig['take_profit_2'], t)}")
        print(f"    R:R: {sig['rr']:.1f}:1  |  Prob: {sig['probability']}%  |  Size: {sig['shares']} units")
        if sig.get("recycled"):
            print(f"    ⚠️  RECYCLED COUNT RISK")
    print(f"\n{SEP}")
    # Save JSON
    try:
        with open("/tmp/swing_analysis.json", "w") as f:
            json.dump({"scan_time": now.isoformat(), "results": actionable}, f, indent=2, default=str)
    except Exception:
        pass

def mode_analyze(ticker, account):
    ticker = ticker.upper()
    print(f"\n{SEP}")
    print(f"  SWING TRADER — ANALYZE: {ticker}")
    print(f"  {datetime.now().strftime('%Y-%m-%d %H:%M')}  |  Account: ${account:,.0f}")
    print(SEP)
    try:
        sig = build_signal(ticker, fetch_weekly(ticker), fetch_daily(ticker), fetch_4h(ticker), account)
        print_signal(sig, account)
        with open("/tmp/swing_analysis.json", "w") as f:
            json.dump(sig, f, indent=2, default=str)
        print("  Output saved to /tmp/swing_analysis.json")
    except Exception as e:
        print(f"  Error analyzing {ticker}: {e}")

# ─── BACKTESTING ──────────────────────────────────────────────────────────────

BACKTEST_START = "2020-01-01"  # fixed start date for all backtests

def mode_backtest(ticker, years, account):
    ticker = ticker.upper()
    print(f"\n{SEP}")
    print(f"  BACKTEST — {ticker} (desde {BACKTEST_START})")
    print(f"  Strategy: EWT Phase + TD Sequential 9/13 + Fibonacci 1.618")
    print(SEP)

    # Fetch using fixed start date
    end_date = datetime.now().strftime("%Y-%m-%d")
    df_daily  = yf.download(ticker, start=BACKTEST_START, end=end_date, interval="1d", progress=False, auto_adjust=True)
    df_weekly = yf.download(ticker, start="2019-01-01",   end=end_date, interval="1wk", progress=False, auto_adjust=True)

    for df in [df_daily, df_weekly]:
        if isinstance(df.columns, pd.MultiIndex):
            df.columns = df.columns.get_level_values(0)
        df.dropna(inplace=True)

    if df_daily.empty:
        print(f"  No data for {ticker}")
        return

    print(f"  Data range: {df_daily.index[0].date()} → {df_daily.index[-1].date()}")
    print(f"  Total bars: {len(df_daily)}")
    print(f"  Scanning for setups...\n")

    trades = []
    in_position = False
    entry_trade = None
    MIN_BARS = 80  # need enough history for indicators

    for i in range(MIN_BARS, len(df_daily)):
        hist_daily  = df_daily.iloc[:i]
        hist_weekly = df_weekly[df_weekly.index <= hist_daily.index[-1]]

        if in_position and entry_trade:
            curr = hist_daily.iloc[-1]
            bars_held = i - entry_trade["bar_in"]

            # Check exit conditions
            hit_sl = curr["Low"]  <= entry_trade["stop_loss"]   if entry_trade["direction"] == "LONG" else curr["High"] >= entry_trade["stop_loss"]
            hit_tp = curr["High"] >= entry_trade["take_profit_1"] if entry_trade["direction"] == "LONG" else curr["Low"]  <= entry_trade["take_profit_1"]
            timeout = bars_held > 45  # ~9 weeks max

            if hit_sl or hit_tp or timeout:
                exit_price = entry_trade["stop_loss"] if hit_sl else (entry_trade["take_profit_1"] if hit_tp else float(curr["Close"]))
                pnl_pct = (exit_price - entry_trade["entry"]) / entry_trade["entry"]
                if entry_trade["direction"] == "SHORT":
                    pnl_pct = -pnl_pct
                entry_trade.update({
                    "exit_price": round(exit_price, 4),
                    "exit_date":  hist_daily.index[-1],
                    "bars_held":  bars_held,
                    "pnl_pct":    round(pnl_pct * 100, 2),
                    "win":        hit_tp,
                    "exit_reason": "TP" if hit_tp else ("SL" if hit_sl else "TIMEOUT"),
                    "swing_trade": bars_held <= 15,
                })
                trades.append(entry_trade)
                in_position = False
                entry_trade = None
            continue

        # Check for new entry signal
        if not in_position and len(hist_weekly) >= 20:
            try:
                sig = build_signal(ticker, hist_weekly, hist_daily, pd.DataFrame(), account)
                if sig and sig.get("actionable"):
                    entry_trade = {
                        "bar_in":       i,
                        "entry_date":   hist_daily.index[-1],
                        "entry":        sig["entry"],
                        "stop_loss":    sig["stop_loss"],
                        "take_profit_1": sig["take_profit_1"],
                        "direction":    sig["direction"],
                        "rr":           sig["rr"],
                        "probability":  sig["probability"],
                        "recycled":     sig.get("recycled", False),
                        "fib_confluence": hist_daily["Close"].iloc[-1] <= sig["fib_618"] * 1.02,
                        "with_weekly_trend": sig["bias"] != "NEUTRAL",
                        "td9_mature":   sig["td_daily"].get("completed_9_buy") or sig["td_daily"].get("completed_9_sell"),
                        "price_flip":   sig.get("price_flip", False),
                    }
                    in_position = True
            except Exception:
                continue

    if not trades:
        print("  No trades were executed in this period.")
        return

    # ─ Calculate Metrics ────────────────────────────────────────────────────
    wins      = [t for t in trades if t["win"]]
    losses    = [t for t in trades if not t["win"]]
    swing_trades = [t for t in trades if t.get("swing_trade", True)]

    win_rate  = len(wins) / len(trades) * 100 if trades 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
    gross_profit = sum(t["pnl_pct"] for t in wins)
    gross_loss   = abs(sum(t["pnl_pct"] for t in losses))
    profit_factor = gross_profit / gross_loss if gross_loss else 999.0

    # Average win duration
    avg_duration = np.mean([t["bars_held"] for t in wins]) if wins else 0

    # Drawdown
    equity = [100.0]
    for t in trades:
        equity.append(equity[-1] * (1 + t["pnl_pct"] / 100))
    peak = equity[0]
    max_dd = 0
    for e in equity:
        peak = max(peak, e)
        dd = (peak - e) / peak * 100
        max_dd = max(max_dd, dd)

    recovery_factor = (equity[-1] - equity[0]) / max_dd if max_dd else 999.0

    # Annualized return (CAGR)
    start_date = pd.Timestamp(BACKTEST_START)
    end_date_ts = df_daily.index[-1]
    years_elapsed = max((end_date_ts - start_date).days / 365.25, 0.1)
    total_return = (equity[-1] / 100.0)  # as multiplier
    annualized_return = (total_return ** (1 / years_elapsed) - 1) * 100  # as %
    annualized_dollar = account * (annualized_return / 100)

    # Buy-and-Hold benchmark (same period)
    bh_start_price = float(df_daily["Close"].iloc[0])
    bh_end_price   = float(df_daily["Close"].iloc[-1])
    bh_total       = (bh_end_price / bh_start_price)
    bh_cagr        = (bh_total ** (1 / years_elapsed) - 1) * 100
    bh_dollar      = account * (bh_cagr / 100)
    alpha          = annualized_return - bh_cagr  # strategy alpha vs. passive

    # Sharpe Ratio (simplified: annualized return / annualized volatility, risk-free = 4%)
    pnl_series = pd.Series([t["pnl_pct"] for t in trades])
    if len(pnl_series) > 1:
        vol = pnl_series.std() * np.sqrt(252 / max(np.mean([t["bars_held"] for t in trades]), 1))
        sharpe = (annualized_return - 4.0) / vol if vol > 0 else 0.0
    else:
        sharpe = 0.0

    # Qualitative breakdowns
    with_trend = [t for t in trades if t.get("with_weekly_trend")]
    fib_trades = [t for t in trades if t.get("fib_confluence")]
    mature_9   = [t for t in trades if t.get("td9_mature")]
    flip_trades = [t for t in trades if t.get("price_flip")]
    recycled_trades = [t for t in trades if t.get("recycled")]

    def win_pct(subset):
        w = [t for t in subset if t["win"]]
        return f"{len(w)/len(subset)*100:.0f}%" if subset else "N/A"

    # ─ Print Results ────────────────────────────────────────────────────────
    def status(val, goal_lo, goal_hi=None, higher_better=True, labels=("✅ Pasa", "⚠️  No pasa")):
        if goal_hi:
            ok = goal_lo <= val <= goal_hi
        else:
            ok = val >= goal_lo if higher_better else val <= goal_lo
        return labels[0] if ok else labels[1]

    print(f"\n  ══ STRATEGY ACCURACY METRICS ══════════════════════════")
    print(f"  {'Metric':<22} {'Goal':<14} {'Result':<14} {'Alpha'}")
    print(f"  {'-'*22} {'-'*14} {'-'*14} {'-'*6}")
    print(f"  {'Win Rate':<22} {'55–65%':<14} {f'{win_rate:.1f}%':<14} {status(win_rate, 55, 65)}")
    print(f"  {'Profit Factor':<22} {'> 2.0':<14} {f'{profit_factor:.2f}':<14} {status(profit_factor, 2.0)}")
    print(f"  {'Avg Win Duration':<22} {'5–8 Days':<14} {f'{avg_duration:.1f} days':<14} {status(avg_duration, 5, 8)}")
    print(f"  {'Max Drawdown':<22} {'< 10%':<14} {f'{max_dd:.1f}%':<14} {status(max_dd, 10, higher_better=False)}")
    print(f"  {'Recovery Factor':<22} {'> 3.0':<14} {f'{recovery_factor:.1f}':<14} {status(recovery_factor, 3.0)}")
    ann_str = f"{annualized_return:+.1f}%/yr  (${annualized_dollar:+,.0f})"
    print(f"  {'Rendimiento Anualiz.':<22} {'> 8%/yr':<14} {ann_str:<14} {status(annualized_return, 8)}")
    bh_str  = f"{bh_cagr:+.1f}%/yr  (${bh_dollar:+,.0f})"
    if alpha >= 0:
        alpha_icon = "✅ Supera B&H"
    elif alpha >= -3:
        alpha_icon = "🟡 Casi empata"
    else:
        alpha_icon = "⚠️  Underperforma"
    print(f"  {'Buy & Hold (CAGR)':<22} {'benchmark':<14} {bh_str:<14} {'—'}")
    print(f"  {'Alpha vs. B&H':<22} {'> 0%':<14} {f'{alpha:+.1f}%/yr':<28} {alpha_icon}")
    print(f"  {'Sharpe Ratio':<22} {'> 1.0':<14} {f'{sharpe:.2f}':<14} {status(sharpe, 1.0)}")
    print(f"\n  Total trades:   {len(trades)}  |  Wins: {len(wins)}  |  Losses: {len(losses)}")
    print(f"  Swing trades (≤15d): {len(swing_trades)} ({len(swing_trades)/len(trades)*100:.0f}%)")
    print(f"  Avg win: +{avg_win:.1f}%   Avg loss: {avg_loss:.1f}%\n")
    print(f"  ── QUALITATIVE BREAKDOWN ───────────────────────────────")
    print(f"  With Weekly Trend:    {len(with_trend)} trades → Win rate: {win_pct(with_trend)}")
    print(f"  Fib Confluence:       {len(fib_trades)} trades → Win rate: {win_pct(fib_trades)}")
    print(f"  Mature TD-9 Signal:   {len(mature_9)} trades → Win rate: {win_pct(mature_9)}")
    print(f"  Price Flip Confirm:   {len(flip_trades)} trades → Win rate: {win_pct(flip_trades)}")
    print(f"  Recycled Count Risk:  {len(recycled_trades)} trades → Win rate: {win_pct(recycled_trades)}")
    print(f"\n{SEP}")

    # Save results
    try:
        result = {
            "ticker": ticker, "years": years,
            "win_rate": win_rate, "profit_factor": profit_factor,
            "avg_duration_days": avg_duration, "max_drawdown": max_dd,
            "recovery_factor": recovery_factor, "total_trades": len(trades),
            "trades": trades
        }
        with open("/tmp/swing_backtest.json", "w") as f:
            json.dump(result, f, indent=2, default=str)
        print("  Results saved to /tmp/swing_backtest.json\n")
    except Exception:
        pass

# ─── S&P 500 PRE-FILTER ───────────────────────────────────────────────────────

def get_sp500_tickers():
    """Fetch S&P 500 tickers from Wikipedia via requests-style URL or fallback CSV."""
    import io, urllib.request
    url = "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
    headers = {"User-Agent": "Mozilla/5.0 (compatible; SwingTrader/1.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))
        tickers = tables[0]["Symbol"].tolist()
        tickers = [str(t).replace(".", "-") for t in tickers]
        return tickers
    except Exception:
        pass
    # Fallback: download from GitHub (slickcharts mirror)
    try:
        csv_url = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/main/data/constituents.csv"
        req2 = urllib.request.Request(csv_url, headers=headers)
        with urllib.request.urlopen(req2, timeout=20) as resp2:
            df = pd.read_csv(io.BytesIO(resp2.read()))
        tickers = df["Symbol"].tolist()
        tickers = [str(t).replace(".", "-") for t in tickers]
        return tickers
    except Exception as e:
        print(f"  ⚠️  Could not fetch S&P 500 list: {e}")
        return []

def mode_sp500(top_n, account):
    """
    Stage 1: Quick pre-filter of all S&P 500 stocks.
    Uses batch download — fast. Filters by:
      1. Volume surge (last bar > 1.1x 20-day avg)
      2. Price near recent swing low / Fib support (within 5%)
      3. Short-term momentum shift (3-day return > -3% and < +1% — pullback zone)
      4. ATR volatility filter (not too quiet, not too wild)
    Outputs top candidates for Stage 2 full analysis.
    """
    print(f"\n{SEP}")
    print(f"  S&P 500 PREFILTER — Stage 1")
    print(f"  {datetime.now().strftime('%Y-%m-%d %H:%M')}  |  Target: top {top_n} candidates")
    print(SEP)

    print("  Fetching S&P 500 ticker list...", flush=True)
    tickers = get_sp500_tickers()
    if not tickers:
        return
    print(f"  {len(tickers)} tickers loaded. Downloading data...", flush=True)

    # Batch download — much faster than one-by-one
    raw = yf.download(tickers, period="3mo", interval="1d",
                      progress=False, auto_adjust=True, group_by="ticker",
                      threads=True)

    candidates = []
    skipped = 0

    for ticker in tickers:
        try:
            # Extract single ticker from multi-ticker download
            if isinstance(raw.columns, pd.MultiIndex):
                if ticker not in raw.columns.get_level_values(0):
                    skipped += 1; continue
                df = raw[ticker].dropna()
            else:
                df = raw.dropna()

            if len(df) < 25:
                skipped += 1; continue

            close  = df["Close"].values
            volume = df["Volume"].values if "Volume" in df.columns else None
            high   = df["High"].values
            low    = df["Low"].values

            # ── Filter 1: Volume surge ──────────────────────────────────────
            vol_score = 0
            if volume is not None and volume[-1] > 0:
                avg_vol = np.mean(volume[-20:])
                if avg_vol > 0:
                    vol_ratio = volume[-1] / avg_vol
                    if vol_ratio >= 1.1:
                        vol_score = min(vol_ratio - 1.0, 1.0)  # 0–1 score

            # ── Filter 2: Near swing low (pullback zone) ────────────────────
            recent_low  = np.min(low[-20:])
            recent_high = np.max(high[-20:])
            rng = recent_high - recent_low
            if rng == 0:
                skipped += 1; continue
            fib_618 = recent_high - 0.618 * rng
            price_now = close[-1]
            near_support = price_now <= fib_618 * 1.05  # within 5% of fib 61.8%
            pct_from_low = (price_now - recent_low) / recent_low * 100

            # ── Filter 3: Short-term momentum (pullback, not freefall) ──────
            ret_3d = (close[-1] / close[-4] - 1) * 100 if len(close) >= 4 else 0
            pullback_zone = -8.0 <= ret_3d <= 1.0  # mild pullback, not crash

            # ── Filter 4: ATR volatility range ──────────────────────────────
            tr = pd.DataFrame({
                "hl": pd.Series(high - low),
                "hc": pd.Series(np.abs(high[1:] - close[:-1]), index=range(1, len(close))),
                "lc": pd.Series(np.abs(low[1:] - close[:-1]),  index=range(1, len(close))),
            }).max(axis=1)
            atr14_val = tr.rolling(14).mean().iloc[-1]
            atr_pct   = atr14_val / price_now * 100
            vol_ok    = 0.5 <= atr_pct <= 6.0  # not too quiet, not too wild

            # ── Composite score ─────────────────────────────────────────────
            score = 0
            if near_support:  score += 3
            if pullback_zone: score += 2
            if vol_ok:        score += 1
            if vol_score > 0: score += 1

            if score >= 4:
                candidates.append({
                    "ticker":      ticker,
                    "price":       round(float(price_now), 2),
                    "fib618":      round(float(fib_618), 2),
                    "ret_3d":      round(float(ret_3d), 2),
                    "atr_pct":     round(float(atr_pct), 2),
                    "vol_ratio":   round(float(vol_score + 1.0), 2),
                    "pct_from_low": round(float(pct_from_low), 2),
                    "score":       score,
                })
        except Exception:
            skipped += 1
            continue

    # Sort by score desc, then by proximity to Fib support
    candidates.sort(key=lambda x: (-x["score"], x["pct_from_low"]))
    top = candidates[:top_n]

    print(f"\n  ── PRE-FILTER RESULTS ──────────────────────────────────")
    print(f"  Scanned: {len(tickers)} | Candidates: {len(candidates)} | Showing top {len(top)}")
    print(f"  Skipped (no data): {skipped}\n")

    if not top:
        print("  No candidates passed the pre-filter today.")
    else:
        print(f"  {'#':<3} {'Ticker':<8} {'Price':>8} {'Fib61.8':>9} {'3D Ret':>8} {'ATR%':>6} {'Vol':>5} {'Score':>6}")
        print(f"  {'-'*3} {'-'*8} {'-'*8} {'-'*9} {'-'*8} {'-'*6} {'-'*5} {'-'*6}")
        for i, c in enumerate(top, 1):
            print(f"  {i:<3} {c['ticker']:<8} {c['price']:>8.2f} {c['fib618']:>9.2f} "
                  f"{c['ret_3d']:>+7.1f}% {c['atr_pct']:>5.1f}% {c['vol_ratio']:>4.1f}x {c['score']:>6}")

    print(f"\n{SEP}")

    # Save candidates for Stage 2
    try:
        with open("/tmp/sp500_candidates.json", "w") as f:
            json.dump({"timestamp": datetime.now().isoformat(), "candidates": top}, f, indent=2)
        print(f"  Candidates saved to /tmp/sp500_candidates.json")
        print(f"  Run: python3 swing_trader.py sp500scan  to run Stage 2 full analysis\n")
    except Exception:
        pass


def mode_sp500scan(account):
    """Stage 2: Full EWT+DeMark analysis on Stage 1 candidates."""
    try:
        with open("/tmp/sp500_candidates.json") as f:
            data = json.load(f)
        candidates = [c["ticker"] for c in data["candidates"]]
        ts = data.get("timestamp", "unknown")
        print(f"\n{SEP}")
        print(f"  S&P 500 FULL SCAN — Stage 2")
        print(f"  Pre-filter from: {ts[:16]}")
        print(f"  Analyzing {len(candidates)} candidates...")
        print(SEP)
    except FileNotFoundError:
        print("  ⚠️  No candidates found. Run 'sp500' first.")
        return

    # Reuse existing scan logic on candidate list
    original_watchlist = WATCHLIST[:]
    import swing_trader as _self
    _self.WATCHLIST = candidates

    # Temporarily override watchlist and run scan
    actionable = []
    for ticker in candidates:
        try:
            df_w = fetch_weekly(ticker)
            df_d = fetch_daily(ticker)
            df_4 = fetch_4h(ticker)
            sig  = build_signal(ticker, df_w, df_d, df_4, account)
            if sig and sig.get("actionable"):
                actionable.append(sig)
                status_str = f"LONG ✅ — {sig['probability']}%" if sig["direction"] == "LONG" else f"SHORT 🔴 — {sig['probability']}%"
            else:
                status_str = "neutral"
            recycled_flag = " [!RECYCLED]" if sig and sig.get("recycled") else ""
            print(f"  Scanning {ticker}... {status_str}{recycled_flag}")
        except Exception:
            print(f"  Scanning {ticker}... error")

    print(f"\n{SEP}")
    if not actionable:
        print("  No actionable setups from S&P 500 candidates today.")
    else:
        print(f"  {len(actionable)} ACTIONABLE SETUP(S) FROM S&P 500:\n{SEP}\n")
        for sig in actionable:
            d = sig["direction"]
            icon = "🟢" if d == "LONG" else "🔴"
            print(f"  {icon} {d}  {sig['ticker']}  @  {sig['current_price']}")
            print(f"    🎯 Entry:   {sig['entry']}")
            print(f"    🛑 SL:      {sig['stop_loss']}")
            print(f"    💰 TP1:     {sig['take_profit_1']}")
            print(f"    💰 TP2:     {sig['take_profit_2']}")
            print(f"    R:R: {sig['rr']}:1  |  Prob: {sig['probability']}%  |  Size: {sig.get('position_size','?')} units\n")
    print(SEP)


# ─── MAIN ─────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Swing Trader — EWT + DeMark")
    sub = parser.add_subparsers(dest="mode", required=True)

    sp = sub.add_parser("scan")
    sp.add_argument("--account", type=float, default=100000.0)

    s5 = sub.add_parser("sp500")
    s5.add_argument("--top", type=int, default=30, help="Max candidates to output (default: 30)")
    s5.add_argument("--account", type=float, default=100000.0)

    s5s = sub.add_parser("sp500scan")
    s5s.add_argument("--account", type=float, default=100000.0)

    ap = sub.add_parser("analyze")
    ap.add_argument("ticker")
    ap.add_argument("--account", type=float, default=100000.0)

    bp = sub.add_parser("backtest")
    bp.add_argument("ticker")
    bp.add_argument("years", type=int, nargs="?", default=3)
    bp.add_argument("--account", type=float, default=100000.0)

    args = parser.parse_args()

    if args.mode == "scan":
        mode_scan(args.account)
    elif args.mode == "sp500":
        mode_sp500(args.top, args.account)
    elif args.mode == "sp500scan":
        mode_sp500scan(args.account)
    elif args.mode == "analyze":
        mode_analyze(args.ticker, args.account)
    elif args.mode == "backtest":
        mode_backtest(args.ticker, args.years, args.account)

if __name__ == "__main__":
    main()
