"""
QuantOracle Strategy Optimizer
===============================
Walk-forward parameter optimization for a momentum/mean-reversion strategy.
Tests multiple parameter combinations across a backtest, then validates
the best configuration with full risk analysis and Monte Carlo.

This is the realistic heavy-usage pattern:
  - 1,200-1,500+ API calls per run
  - A single optimization session costs ~$6-8 at paid rates
  - Exceeds free tier (1,000/day) in ~15 minutes
  - The alternative: an LLM computing this in-context would cost
    $12-60 in tokens (Sonnet to Opus), take 4x longer, and get
    ~15-30% of the complex math wrong

Usage:
    pip install requests
    python strategy_optimizer.py

    # With API key (recommended -- this WILL exceed free tier):
    QUANTORACLE_API_KEY=your_key python strategy_optimizer.py

API: https://api.quantoracle.dev
GitHub: https://github.com/QuantOracledev/quantoracle
"""

import requests
import random
import time
import os
from itertools import product as cartesian

# ── Config ────────────────────────────────────────────────
API = os.environ.get("QUANTORACLE_URL", "https://api.quantoracle.dev")
KEY = os.environ.get("QUANTORACLE_API_KEY", "")
HDR = {"Content-Type": "application/json"}
if KEY:
    HDR["X-Api-Key"] = KEY

calls = 0
cost = 0.0

COST_MAP = {
    "indicators/technical": 0.005, "indicators/regime": 0.005,
    "indicators/crossover": 0.005, "indicators/bollinger-bands": 0.002,
    "stats/realized-volatility": 0.005, "stats/sharpe-ratio": 0.002,
    "stats/hurst-exponent": 0.008, "stats/cointegration": 0.008,
    "risk/portfolio": 0.008, "risk/kelly": 0.005,
    "risk/var-parametric": 0.008, "risk/drawdown": 0.005,
    "risk/position-size": 0.005,
    "portfolio/optimize": 0.015, "simulate/montecarlo": 0.015,
    "options/price": 0.005, "options/implied-vol": 0.005,
    "tvm/cagr": 0.002,
}


def qo(ep, params):
    global calls, cost
    calls += 1
    cost += COST_MAP.get(ep, 0.005)
    try:
        r = requests.post(f"{API}/v1/{ep}", json=params, headers=HDR, timeout=30)
        return r.json() if r.status_code == 200 else None
    except:
        return None


def synth_prices(start, n, mu, sigma, seed):
    random.seed(seed)
    prices = [start]
    for _ in range(n):
        prices.append(prices[-1] * (1 + random.gauss(mu, sigma)))
    return [round(p, 2) for p in prices]


def synth_returns(n, mu, sigma, seed):
    random.seed(seed)
    return [random.gauss(mu, sigma) for _ in range(n)]


# ══════════════════════════════════════════════════════════
# UNIVERSE -- 15 assets with different return/vol profiles
# ══════════════════════════════════════════════════════════

UNIVERSE = {
    "SPY":  (0.0004, 0.012), "QQQ":  (0.0005, 0.015),
    "IWM":  (0.0003, 0.016), "DIA":  (0.0003, 0.011),
    "SOXL": (0.0008, 0.045), "TECL": (0.0006, 0.035),
    "TQQQ": (0.0007, 0.040), "BITX": (0.001,  0.055),
    "ETHU": (0.0009, 0.06),  "TLT":  (0.0001, 0.01),
    "GLD":  (0.0002, 0.008), "SLV":  (0.0003, 0.018),
    "XLE":  (0.0003, 0.018), "XLF":  (0.0002, 0.014),
    "ARKK": (0.0004, 0.03),
}

DAYS = 252  # 1 year of trading days


# ══════════════════════════════════════════════════════════
# PHASE 1: PARAMETER SWEEP
# Test every combination of lookback x rebalance x threshold
#
# Parameters:
#   lookback:   [5, 10, 14, 20, 30]    (5 values)
#   rebalance:  [1, 3, 5, 10]          (4 values)
#   rsi_buy:    [25, 30, 35]           (3 values)
#   rsi_sell:   [65, 70, 75]           (3 values)
#
# Total combinations: 5 x 4 x 3 x 3 = 180
# Each combo runs ~6 technical indicator calls (assets x rebalance points)
# Total: ~1,080 calls just for the sweep
# ══════════════════════════════════════════════════════════

def run_backtest(assets, prices_cache, returns_cache,
                 lookback, rebalance_freq, rsi_buy, rsi_sell):
    """Run a single backtest with given parameters. Returns portfolio returns."""
    portfolio_returns = []
    tickers = list(assets)

    for day in range(lookback + 1, DAYS, rebalance_freq):
        # Score each asset based on technical signals
        scores = {}
        for ticker in tickers:
            window = prices_cache[ticker][max(0, day - lookback):day + 1]
            if len(window) < 5:
                continue

            tech = qo("indicators/technical", {"prices": window, "period": min(lookback, len(window) - 1)})
            if not tech:
                continue

            # Momentum score: bullish trend + RSI in buy zone
            score = 0
            if tech["trend"] == "BULLISH":
                score += 1
            if tech["rsi"] < rsi_buy:      # oversold = buy signal
                score += 2
            elif tech["rsi"] > rsi_sell:    # overbought = avoid
                score -= 1
            scores[ticker] = score

        # Select top 5 assets by score
        ranked = sorted(scores.items(), key=lambda x: -x[1])
        selected = [t for t, s in ranked[:5] if s > 0]

        if not selected:
            selected = ["GLD"]  # defensive default

        # Equal-weight the selected assets for this period
        for d in range(day, min(day + rebalance_freq, DAYS)):
            day_ret = sum(returns_cache[t][d] for t in selected if d < len(returns_cache[t])) / len(selected)
            portfolio_returns.append(day_ret)

    return portfolio_returns


def parameter_sweep():
    """Sweep all parameter combinations and rank by Sharpe."""
    print("\n" + "=" * 60)
    print("PHASE 1: PARAMETER SWEEP")
    print("=" * 60)

    # Pre-generate all price/return data (no API calls)
    prices_cache = {}
    returns_cache = {}
    for ticker, (mu, sigma) in UNIVERSE.items():
        prices_cache[ticker] = synth_prices(100, DAYS, mu, sigma, seed=hash(ticker) % 10000)
        returns_cache[ticker] = synth_returns(DAYS, mu, sigma, seed=hash(ticker + "r") % 10000)

    # Define parameter grid
    lookbacks = [5, 10, 14, 20, 30]
    rebalance_freqs = [1, 3, 5, 10]
    rsi_buys = [25, 30, 35]
    rsi_sells = [65, 70, 75]

    combos = list(cartesian(lookbacks, rebalance_freqs, rsi_buys, rsi_sells))
    total = len(combos)

    print(f"  Testing {total} parameter combinations across {len(UNIVERSE)} assets")
    print(f"  Estimated API calls: ~{total * 6}")
    print(f"  Estimated cost: ~${total * 6 * 0.005:.2f} USDC")
    print()

    results = []
    best_sharpe = -999
    start = time.time()

    for i, (lb, rf, rb, rs) in enumerate(combos):
        returns = run_backtest(
            list(UNIVERSE.keys())[:8],  # Use top 8 assets for speed
            prices_cache, returns_cache,
            lookback=lb, rebalance_freq=rf, rsi_buy=rb, rsi_sell=rs,
        )

        if len(returns) < 20:
            continue

        # Quick Sharpe calculation (1 call)
        sharpe_result = qo("stats/sharpe-ratio", {"returns": returns})
        sharpe = sharpe_result.get("sharpe_ratio", 0) if sharpe_result else 0

        results.append({
            "lookback": lb, "rebalance": rf,
            "rsi_buy": rb, "rsi_sell": rs,
            "sharpe": sharpe, "returns": returns,
            "n_days": len(returns),
        })

        if sharpe > best_sharpe:
            best_sharpe = sharpe
            print(f"  [{i+1:3d}/{total}]  NEW BEST  "
                  f"lookback={lb:2d} rebal={rf:2d} rsi={rb}/{rs}  "
                  f"Sharpe={sharpe:+.2f}  "
                  f"({calls} calls, ${cost:.2f})")
        elif (i + 1) % 20 == 0:
            elapsed = time.time() - start
            rate = calls / elapsed if elapsed > 0 else 0
            print(f"  [{i+1:3d}/{total}]  "
                  f"Best Sharpe={best_sharpe:+.2f}  "
                  f"({calls} calls, {rate:.0f} calls/sec)")

    # Sort by Sharpe
    results.sort(key=lambda x: -x["sharpe"])

    print(f"\n  -- Top 5 Parameter Sets --")
    for i, r in enumerate(results[:5]):
        print(f"  {i+1}. Sharpe={r['sharpe']:+.3f}  "
              f"lookback={r['lookback']:2d}  rebal={r['rebalance']:2d}  "
              f"RSI={r['rsi_buy']}/{r['rsi_sell']}")

    return results


# ══════════════════════════════════════════════════════════
# PHASE 2: DEEP ANALYSIS OF TOP 3 CONFIGURATIONS
# Full risk suite on each winning parameter set
# Expected calls: ~60-80
# ══════════════════════════════════════════════════════════

def deep_analysis(top_results, portfolio_value=80000):
    """Run comprehensive risk analysis on the top parameter configurations."""
    print("\n" + "=" * 60)
    print("PHASE 2: DEEP ANALYSIS (Top 3 configs)")
    print("=" * 60)

    for i, config in enumerate(top_results[:3]):
        returns = config["returns"]
        print(f"\n  -- Config #{i+1}: lookback={config['lookback']} "
              f"rebal={config['rebalance']} RSI={config['rsi_buy']}/{config['rsi_sell']} --")

        # Full 22-metric risk suite (1 call)
        risk = qo("risk/portfolio", {"returns": returns, "risk_free_rate": 0.045})
        if risk:
            print(f"    Annualized return: {risk['returns']['annualized']*100:+.1f}%")
            print(f"    Volatility:        {risk['returns']['vol']*100:.1f}%")
            print(f"    Sharpe:            {risk['risk']['sharpe']:.3f}")
            print(f"    Sortino:           {risk['risk']['sortino']:.3f}")
            print(f"    Max drawdown:      {risk['risk']['max_drawdown']*100:.1f}%")
            print(f"    Win rate:          {risk['returns']['win_rate']*100:.0f}%")

        # VaR (1 call)
        var = qo("risk/var-parametric", {
            "returns": returns,
            "portfolio_value": portfolio_value,
            "confidence_levels": [0.95, 0.99],
        })
        if var:
            v95 = var["var_results"]["95"]
            print(f"    Daily VaR 95%:     ${v95['var_dollar']:,.0f}")
            print(f"    Daily CVaR 95%:    ${v95['cvar_dollar']:,.0f}")

        # Kelly leverage (1 call -- continuous mode needs >=10 returns)
        kelly = qo("risk/kelly", {"mode": "continuous", "returns": returns})
        if kelly:
            print(f"    Kelly leverage:    {kelly['full_kelly_leverage']:.2f}x")
            print(f"    Half Kelly:        {kelly['half_kelly']:.2f}x")

        # Drawdown analysis (1 call)
        equity = [portfolio_value]
        for r in returns:
            equity.append(equity[-1] * (1 + r))
        dd = qo("risk/drawdown", {"equity_curve": equity})
        if dd:
            print(f"    Underwater:        {dd['underwater_pct']:.0f}% of days")

        # Hurst -- is this strategy trending or mean-reverting? (1 call)
        hurst = qo("stats/hurst-exponent", {"series": equity[-100:]})
        if hurst:
            h = hurst.get("hurst_exponent", 0.5)
            label = hurst.get("interpretation", "RANDOM")
            print(f"    Hurst exponent:    {h:.3f} ({label.lower()})")

        # Monte Carlo forward projection (1 call)
        ann_ret = risk["returns"]["annualized"] if risk else 0.1
        ann_vol = risk["returns"]["vol"] if risk else 0.2
        mc = qo("simulate/montecarlo", {
            "initial_value": portfolio_value,
            "annual_return": min(ann_ret, 1.0),  # cap at 100% for realism
            "annual_vol": min(ann_vol, 1.0),
            "years": 2,
            "simulations": 1000,
        })
        if mc:
            t = mc["terminal"]
            print(f"    2yr Monte Carlo:")
            print(f"      5th %ile:        ${t['p5']:>10,.0f}")
            print(f"      Median:          ${t['median']:>10,.0f}")
            print(f"      95th %ile:       ${t['p95']:>10,.0f}")
            print(f"      Prob of loss:    {mc['prob_loss']*100:.0f}%")

        # Portfolio optimization with this config's assets (1 call)
        asset_returns = {}
        for ticker in list(UNIVERSE.keys())[:5]:
            asset_returns[ticker] = synth_returns(120, *UNIVERSE[ticker], seed=hash(ticker + "opt") % 10000)

        opt = qo("portfolio/optimize", {
            "returns": asset_returns,
            "mode": "max_sharpe",
            "risk_free_rate": 0.045,
        })
        if opt:
            print(f"    Optimal weights (max Sharpe):")
            for ticker, weight in sorted(opt["weights"].items(), key=lambda x: -x[1])[:5]:
                if weight > 0.01:
                    print(f"      {ticker:5s} {weight*100:5.1f}%")


# ══════════════════════════════════════════════════════════
# PHASE 3: OPTIONS OVERLAY SCAN
# Price covered calls across the top holdings
# Expected calls: ~100-150
# ══════════════════════════════════════════════════════════

def options_scan():
    """Scan covered call opportunities across multiple assets, strikes, and expiries."""
    print("\n" + "=" * 60)
    print("PHASE 3: OPTIONS OVERLAY SCAN")
    print("=" * 60)

    assets = {
        "SPY": 555, "QQQ": 480, "SOXL": 45, "TECL": 85,
        "BITX": 52, "ARKK": 62,
    }
    expiries = [0.025, 0.083, 0.17, 0.25]  # 1wk, 1mo, 2mo, 3mo
    otm_pcts = [1.02, 1.05, 1.08, 1.10, 1.15]  # 2% to 15% OTM

    print(f"  Scanning {len(assets)} assets x {len(expiries)} expiries x "
          f"{len(otm_pcts)} strikes = {len(assets) * len(expiries) * len(otm_pcts)} combinations")

    best_yield = 0
    best_trade = ""

    for ticker, spot in assets.items():
        print(f"\n  {ticker} @ ${spot}")
        ticker_best = 0

        for expiry in expiries:
            exp_label = f"{int(expiry*365)}d"

            for otm in otm_pcts:
                strike = round(spot * otm, 0)

                # Price the call (1 call)
                result = qo("options/price", {
                    "S": spot, "K": strike, "T": expiry,
                    "r": 0.045, "sigma": 0.3, "type": "call",
                })

                if not result:
                    continue

                premium = result.get("price", 0)
                if not premium or premium < 0.01:
                    continue

                # Annualized yield
                ann_yield = (premium / spot) * (365 / (expiry * 365)) * 100

                if ann_yield > best_yield:
                    best_yield = ann_yield
                    best_trade = f"{ticker} {exp_label} ${strike}C @ ${premium:.2f}"

                if ann_yield > ticker_best:
                    ticker_best = ann_yield

                # Get IV for this strike (1 call)
                qo("options/implied-vol", {
                    "S": spot, "K": strike, "T": expiry,
                    "r": 0.045, "market_price": premium, "type": "call",
                })

        print(f"    Best yield found: {ticker_best:.1f}%/yr")

    print(f"\n  -- Best Overall Trade --")
    print(f"  {best_trade}")
    print(f"  Annualized yield: {best_yield:.1f}%")


# ══════════════════════════════════════════════════════════
# PHASE 4: CROSS-ASSET CORRELATION & PAIRS
# Full correlation matrix + cointegration scan
# Expected calls: ~40-60
# ══════════════════════════════════════════════════════════

def correlation_analysis():
    """Build correlation matrix and scan for pairs trading opportunities."""
    print("\n" + "=" * 60)
    print("PHASE 4: CORRELATION & PAIRS ANALYSIS")
    print("=" * 60)

    tickers = list(UNIVERSE.keys())[:10]

    # Cointegration scan -- every pair (1 call per pair)
    # 10 choose 2 = 45 pairs
    pairs_found = []
    tested = 0

    print(f"  Testing {len(tickers) * (len(tickers)-1) // 2} pairs for cointegration...")

    for i in range(len(tickers)):
        for j in range(i + 1, len(tickers)):
            t1, t2 = tickers[i], tickers[j]
            p1 = synth_prices(100, 120, *UNIVERSE[t1], seed=hash(t1 + "corr") % 10000)
            p2 = synth_prices(100, 120, *UNIVERSE[t2], seed=hash(t2 + "corr") % 10000)

            coint = qo("stats/cointegration", {
                "series_x": p1[-100:], "series_y": p2[-100:],
            })
            tested += 1

            if coint and coint.get("cointegrated"):
                hl = coint.get("half_life", 0)
                hr = coint.get("hedge_ratio", 0)
                pairs_found.append((t1, t2, hl, hr))
                print(f"    + {t1}/{t2}  half-life={hl:.0f}d  hedge={hr:.3f}")

    print(f"\n  {tested} pairs tested, {len(pairs_found)} cointegrated")

    # For cointegrated pairs, check Hurst exponent of the spread
    for t1, t2, hl, hr in pairs_found[:5]:
        p1 = synth_prices(100, 120, *UNIVERSE[t1], seed=hash(t1 + "corr") % 10000)
        p2 = synth_prices(100, 120, *UNIVERSE[t2], seed=hash(t2 + "corr") % 10000)
        spread = [a - hr * b for a, b in zip(p1[-100:], p2[-100:])]

        hurst = qo("stats/hurst-exponent", {"series": spread})
        if hurst:
            h = hurst.get("hurst_exponent", 0.5)
            label = hurst.get("interpretation", "RANDOM")
            print(f"    {t1}/{t2} spread Hurst={h:.3f} -> {label.lower()}")

    return pairs_found


# ══════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════

def main():
    t0 = time.time()

    print("+" + "=" * 58 + "+")
    print("|  QuantOracle Strategy Optimizer                        |")
    print("|  Parameter Sweep + Deep Analysis + Options + Pairs     |")
    print("+" + "=" * 58 + "+")
    print(f"  API:    {API}")
    print(f"  Auth:   {'API Key' if KEY else 'Free Tier'}")

    # Check health
    try:
        r = requests.get(f"{API}/health", timeout=5)
        h = r.json()
        print(f"  Status: {h['status']} | {h['tools']} tools")
    except:
        print("  ! Cannot reach API")
        return

    # Check quota
    try:
        r = requests.get(f"{API}/usage", timeout=5)
        u = r.json()
        remaining = u["remaining"]
        print(f"  Quota:  {remaining}/{u['daily_limit']} calls remaining")
        if remaining < 1200 and not KEY:
            print()
            print("  ! WARNING: This optimizer needs ~1,200-1,500 calls.")
            print(f"  ! You have {remaining} free calls remaining today.")
            print("  ! Set QUANTORACLE_API_KEY or enable x402 payments")
            print("  ! to avoid hitting the rate limit mid-optimization.")
            print()
            resp = input("  Continue anyway? [y/N] ")
            if resp.lower() != "y":
                return
    except:
        pass

    # Phase 1: Parameter sweep (~1,000-1,200 calls)
    results = parameter_sweep()

    # Phase 2: Deep analysis of top configs (~60-80 calls)
    if results:
        deep_analysis(results)

    # Phase 3: Options overlay scan (~100-150 calls)
    options_scan()

    # Phase 4: Correlation & pairs (~50-70 calls)
    correlation_analysis()

    # Final summary
    elapsed = time.time() - t0

    print("\n" + "=" * 60)
    print("OPTIMIZATION COMPLETE")
    print("=" * 60)
    print(f"  Total API calls:    {calls:,}")
    print(f"  Estimated cost:     ${cost:.2f} USDC")
    print(f"  Time elapsed:       {elapsed:.0f}s ({elapsed/60:.1f} min)")
    print(f"  Throughput:         {calls/elapsed:.0f} calls/sec")
    if calls > 0:
        print(f"  Avg cost/call:      ${cost/calls:.4f}")
    print()

    if calls > 1000:
        llm_low = calls * 0.01   # Sonnet/GPT-4o: ~$0.01/call
        llm_high = calls * 0.05  # Opus: ~$0.05/call
        llm_time = calls * 3     # ~3s per LLM call (sequential)
        print(f"  This session made {calls:,} API calls.")
        print(f"  Free tier (1,000/day) would have been exhausted at call #1,000.")
        print(f"  With x402 payment, total cost: ${cost:.2f} USDC.")
        print(f"  With an LLM computing this in-context:")
        print(f"    Est. token cost:  ${llm_low:.2f} - ${llm_high:.2f} (Sonnet to Opus)")
        print(f"    Est. error rate:  ~15-30% of calculations incorrect")
        print(f"    Est. time:        {llm_time:.0f}s ({llm_time/60:.0f} min) sequential")
        print(f"  QuantOracle: {llm_time/elapsed:.0f}x faster, 100% accurate, "
              f"{llm_low/cost:.0f}-{llm_high/cost:.0f}x cheaper.")
    print()


if __name__ == "__main__":
    main()
