"""
Agente Carver de Leonex.

Incorpora el marco de Robert Carver ("Systematic Trading" / "Advanced Futures
Trading Strategies"). Tres ideas centrales, distintas de como funciona el
Strategy Lab basado en trades discretos:

  1. FORECASTS CONTINUOS, no senales on/off. Cada regla produce un numero
     con signo y magnitud: +20 = subida fuerte esperada, -20 = bajada fuerte,
     0 = neutral. Importa la FUERZA de la senal, no solo "entra / no entra".
  2. COMBINACION DE FORECASTS. Varias reglas (EWMAC a distintas velocidades
     + breakout) se promedian en un forecast combinado por activo.
  3. VOLATILITY TARGETING. El tamano de la posicion se escala por el forecast
     Y por la volatilidad del activo: a igual forecast, un activo volatil pesa
     menos. Cada posicion aporta un riesgo comparable.

ALCANCE: es una CAPA PARALELA de analisis. No reemplaza el motor de trades
discretos del Strategy Lab (eso seria reescribir medio sistema). Responde a
una pregunta concreta: ¿que tendria, hoy y con cuanto peso, un sistema de
seguimiento de tendencia estilo Carver?

Salida: dashboard/data/carver_report.json

Uso:
    python agents/agente_carver.py
    python agents/agente_carver.py --limit-tickers 20
    python agents/agente_carver.py --db /ruta/a/Leonex.sqlite
"""

from __future__ import annotations

import argparse
import json
import logging
import sys
from datetime import datetime
try:
    from datetime import UTC
except ImportError:                       # Python < 3.11
    from datetime import timezone
    UTC = timezone.utc
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parent))
import agente_strategy_lab as lab  # noqa: E402

REPORT_OUT = lab.DASHBOARD_DATA_DIR / "carver_report.json"

# Pares EWMAC (rapida, lenta) — velocidades pensadas para barras DIARIAS.
EWMAC_SPEEDS = [(16, 64), (32, 128), (64, 256)]
BREAKOUT_WINDOW = 80
FORECAST_CAP = 20.0          # forecast acotado a +-20 (escala de Carver)
TARGET_AVG_FORECAST = 10.0   # se escala cada regla para que |media| ~ 10
VOL_SPAN = 35                # span EWMA para la volatilidad
MIN_BARS = 300               # hace falta historia para EWMAC 64/256
TARGET_PORTFOLIO_VOL = 0.20  # vol anualizada objetivo de la cartera


def daily_vol(close: pd.Series) -> pd.Series:
    """Volatilidad diaria de retornos (EWMA, estilo Carver)."""
    return close.pct_change().ewm(span=VOL_SPAN, min_periods=10).std()


def _scale_and_cap(raw: pd.Series) -> pd.Series:
    """Escala una serie cruda para que su |media| ~ TARGET_AVG_FORECAST y la
    recorta a +-FORECAST_CAP. Es el 'forecast scalar' de Carver, calibrado
    empiricamente sobre la propia historia del activo."""
    m = float(raw.abs().mean())
    if not np.isfinite(m) or m <= 1e-12:
        return raw * 0.0
    return (raw * (TARGET_AVG_FORECAST / m)).clip(-FORECAST_CAP, FORECAST_CAP)


def ewmac_forecast(close: pd.Series, price_vol: pd.Series,
                   fast: int, slow: int) -> pd.Series:
    """EWMAC: cruce de medias exponenciales normalizado por volatilidad.
    Es la regla de seguimiento de tendencia caballo de batalla de Carver."""
    raw = lab._ema(close, fast) - lab._ema(close, slow)
    norm = raw / price_vol.replace(0.0, np.nan)
    return _scale_and_cap(norm)


def breakout_forecast(close: pd.Series, window: int) -> pd.Series:
    """Breakout: donde esta el precio dentro de su rango de N barras.
    +0.5 (techo) -> forecast alcista fuerte; -0.5 (suelo) -> bajista."""
    hi = close.rolling(window, min_periods=window).max()
    lo = close.rolling(window, min_periods=window).min()
    mid = (hi + lo) / 2.0
    rng = (hi - lo).replace(0.0, np.nan)
    return _scale_and_cap((close - mid) / rng)


def asset_forecast(df: pd.DataFrame) -> dict | None:
    """Calcula los forecasts de cada regla, el combinado y la volatilidad
    para un activo. Devuelve los valores en la ULTIMA barra."""
    close = pd.to_numeric(df["close"], errors="coerce").dropna()
    if len(close) < MIN_BARS:
        return None
    dvol = daily_vol(close)
    price_vol = close * dvol                       # volatilidad en precio

    rules: dict[str, pd.Series] = {}
    for fast, slow in EWMAC_SPEEDS:
        rules[f"ewmac_{fast}_{slow}"] = ewmac_forecast(close, price_vol,
                                                       fast, slow)
    rules[f"breakout_{BREAKOUT_WINDOW}"] = breakout_forecast(
        close, BREAKOUT_WINDOW)

    # Forecast combinado: media de las reglas, re-escalada (el promedio
    # reduce el rango — Carver lo corrige con el forecast diversification
    # multiplier) y recortada.
    combined = _scale_and_cap(pd.DataFrame(rules).mean(axis=1))

    def _last(s: pd.Series) -> float:
        v = s.iloc[-1]
        return float(v) if np.isfinite(v) else 0.0

    av = dvol.iloc[-1]
    ann_vol = float(av * np.sqrt(252)) if np.isfinite(av) else 0.0
    return {
        "rules": {k: round(_last(v), 2) for k, v in rules.items()},
        "combined": round(_last(combined), 2),
        "ann_vol_pct": round(ann_vol * 100.0, 2),
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Agente Carver de Leonex")
    parser.add_argument("--limit-tickers", type=int, default=0,
                        help="Limita el universo (smoke test).")
    parser.add_argument("--db", type=str, default=str(lab.DB_PATH))
    args = parser.parse_args()

    lab.LOGS_DIR.mkdir(parents=True, exist_ok=True)
    lab.DASHBOARD_DATA_DIR.mkdir(parents=True, exist_ok=True)
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
        handlers=[
            logging.FileHandler(lab.LOGS_DIR / "agente_carver.log",
                                encoding="utf-8"),
            logging.StreamHandler(),
        ],
    )
    log = logging.getLogger("agente_carver")
    db_path = Path(args.db)

    universe = lab.load_universe(db_path)
    if args.limit_tickers and len(universe) > args.limit_tickers:
        universe = universe[: args.limit_tickers]
    log.info("Carver: %d activos | reglas EWMAC %s + breakout %d",
             len(universe), EWMAC_SPEEDS, BREAKOUT_WINDOW)

    rows: list[dict] = []
    for ticker in universe:
        try:
            df = lab.load_prices(ticker, "1d", db_path)
        except Exception:
            continue
        if df is None or df.empty:
            continue
        fc = asset_forecast(df)
        if fc is None:
            continue
        rows.append({"ticker": ticker, **fc})

    # Volatility targeting: la senal de tamano es forecast / volatilidad.
    # A igual forecast, el activo mas volatil recibe menos peso.
    for r in rows:
        v = r["ann_vol_pct"] / 100.0
        r["_signal"] = (r["combined"] / v) if v > 1e-6 else 0.0
    gross = sum(abs(r["_signal"]) for r in rows) or 1.0
    for r in rows:
        # Peso con signo, normalizado a que la suma de |pesos| sea 100%.
        r["weight_pct"] = round(r.pop("_signal") / gross * 100.0, 2)
        r["direction"] = ("LONG" if r["combined"] > 0
                          else "SHORT" if r["combined"] < 0 else "FLAT")

    rows.sort(key=lambda r: -abs(r["combined"]))
    n_long = sum(1 for r in rows if r["combined"] > 0)
    n_short = sum(1 for r in rows if r["combined"] < 0)
    n_strong = sum(1 for r in rows if abs(r["combined"]) >= 10.0)
    avg_abs = round(float(np.mean([abs(r["combined"]) for r in rows])), 2) \
        if rows else 0.0

    summary = (
        f"Marco Carver sobre {len(rows)} activos: {len(EWMAC_SPEEDS)} reglas "
        f"EWMAC + breakout, combinadas en un forecast continuo (-20..+20). "
        f"{n_long} en LARGO, {n_short} en CORTO, {n_strong} con conviccion "
        f"fuerte (|forecast|>=10); forecast medio absoluto {avg_abs}. El peso "
        f"sugerido escala con el forecast Y con la inversa de la volatilidad "
        f"(volatility targeting): a igual senal, el activo volatil pesa menos. "
        f"Es una capa de analisis paralela — no ejecuta ni reemplaza al "
        f"Strategy Lab."
    )
    log.info(summary)

    payload = {
        "generated_at": datetime.now(UTC).isoformat(),
        "method": "Carver continuous forecasts (EWMAC + breakout) + "
                  "volatility targeting",
        "ewmac_speeds": [f"{f}/{s}" for f, s in EWMAC_SPEEDS],
        "breakout_window": BREAKOUT_WINDOW,
        "forecast_cap": FORECAST_CAP,
        "target_portfolio_vol_pct": TARGET_PORTFOLIO_VOL * 100.0,
        "n_assets": len(rows),
        "n_long": n_long,
        "n_short": n_short,
        "n_strong": n_strong,
        "avg_abs_forecast": avg_abs,
        "assets": rows,
        "summary_note": summary,
    }
    REPORT_OUT.write_text(
        json.dumps(payload, indent=2, ensure_ascii=False, default=str),
        encoding="utf-8")
    log.info("Reporte exportado -> %s", REPORT_OUT)

    sep = "=" * 80
    print(f"\n{sep}")
    print("Leonex -- Carver (forecasts continuos + volatility targeting)")
    print(sep)
    print(summary)
    for r in rows[:12]:
        print(f"  {r['ticker']:<10} {r['direction']:<6} "
              f"forecast={r['combined']:+6.1f}  vol={r['ann_vol_pct']:>6.1f}%  "
              f"peso={r['weight_pct']:+6.2f}%")
    print(sep)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
