#!/usr/bin/env python3
"""
=============================================================================
analyze_execution.py — Analyse des logs d'exécution
=============================================================================

UTILISATION
-----------
    python3 analyze_execution.py --logs ./exec_logs/executions_202605.jsonl
    python3 analyze_execution.py --logs ./exec_logs/ --output ./diagnosis

Le script attend au moins quelques jours de données (50+ ordres) pour
produire un diagnostic significatif.

CE QUE LE SCRIPT RÉPOND
-----------------------
1. Combien tu paies vraiment de slippage par trade (médiane et P95)
2. Quel type d'ordres ton bot passe (MARKET vs LIMIT)
3. Quelles paires ont le plus mauvais spread/slippage
4. Combien de PnL tu perds en friction par mois
5. Si tu trades mieux en supprimant les paires les plus toxiques
=============================================================================
"""

import argparse
import json
import sys
from collections import defaultdict
from pathlib import Path

try:
    import numpy as np
    import pandas as pd
except ImportError as e:
    print(f"ERREUR : {e}. Installer : pip install pandas numpy")
    sys.exit(1)


def load_logs(log_path: Path) -> pd.DataFrame:
    """Charge un fichier JSONL ou un dossier contenant des JSONL."""
    if log_path.is_dir():
        files = sorted(log_path.glob("*.jsonl"))
    else:
        files = [log_path]
    if not files:
        raise FileNotFoundError(f"Aucun fichier .jsonl dans {log_path}")

    rows = []
    for f in files:
        with open(f) as fh:
            for line in fh:
                line = line.strip()
                if not line:
                    continue
                try:
                    rows.append(json.loads(line))
                except json.JSONDecodeError:
                    continue

    df = pd.DataFrame(rows)
    if df.empty:
        raise ValueError("Aucun record valide trouvé.")
    return df


def pair_buys_and_sells(df: pd.DataFrame) -> pd.DataFrame:
    """Apparie chaque BUY à son SELL via buy_log_id."""
    buys = df[df["side"] == "BUY"].set_index("log_id")
    sells = df[df["side"] == "SELL"].copy()
    sells = sells[sells["buy_log_id"].notna()]

    if buys.empty or sells.empty:
        return pd.DataFrame()

    paired = []
    for _, sell in sells.iterrows():
        buy_id = sell["buy_log_id"]
        if buy_id in buys.index:
            buy = buys.loc[buy_id]
            # Calcul du PnL réel (basé sur les vrais prix d'exécution)
            real_pnl_pct = (sell["avg_fill_price"] - buy["avg_fill_price"]) / buy["avg_fill_price"] * 100
            # PnL théorique (ce que le bot a calculé avec les prix mid)
            theoretical_pnl_pct = (sell["theoretical_price"] - buy["theoretical_price"]) / buy["theoretical_price"] * 100
            # Friction = différence entre PnL théorique et PnL réel
            friction_pct = theoretical_pnl_pct - real_pnl_pct

            paired.append({
                "symbol": buy["symbol"],
                "pattern": buy.get("signal_pattern", ""),
                "exit_reason": sell.get("exit_reason", ""),
                "buy_theoretical": buy["theoretical_price"],
                "buy_actual": buy["avg_fill_price"],
                "buy_slippage_pct": buy["slippage_pct"],
                "sell_theoretical": sell["theoretical_price"],
                "sell_actual": sell["avg_fill_price"],
                "sell_slippage_pct": sell["slippage_pct"],
                "real_pnl_pct": real_pnl_pct,
                "theoretical_pnl_pct": theoretical_pnl_pct,
                "friction_pct": friction_pct,
                "buy_spread_pct": buy.get("spread_pct"),
                "sell_spread_pct": sell.get("spread_pct"),
                "vol_24h_usdt": buy.get("volume_24h_usdt"),
                "order_type": buy.get("order_type"),
            })

    return pd.DataFrame(paired)


def report(df_orders: pd.DataFrame, df_trades: pd.DataFrame, output_dir: Path) -> str:
    """Génère un rapport texte du diagnostic d'exécution."""
    output_dir.mkdir(parents=True, exist_ok=True)
    lines = []

    lines.append("=" * 78)
    lines.append("DIAGNOSTIC D'EXÉCUTION — RAPPORT")
    lines.append("=" * 78)
    lines.append(f"Ordres totaux loggés : {len(df_orders)}")
    lines.append(f"Trades complets (BUY+SELL appariés) : {len(df_trades)}")
    lines.append("")

    # ----- 1. Type d'ordres -----
    lines.append("-" * 78)
    lines.append("1. TYPE D'ORDRES UTILISÉS")
    lines.append("-" * 78)
    by_type = df_orders["order_type"].value_counts()
    for order_type, count in by_type.items():
        pct = count / len(df_orders) * 100
        lines.append(f"  {order_type:<12} : {count:>5} ordres ({pct:>5.1f}%)")
    if "MARKET" in by_type.index and by_type["MARKET"] / len(df_orders) > 0.5:
        lines.append("")
        lines.append("  ⚠️  MAJORITÉ D'ORDRES MARKET — tu paies le spread complet à chaque ordre.")
        lines.append("     Considérer de passer en LIMIT post-only quand possible.")

    # ----- 2. Slippage -----
    lines.append("")
    lines.append("-" * 78)
    lines.append("2. SLIPPAGE RÉEL OBSERVÉ")
    lines.append("-" * 78)
    sl = df_orders["slippage_pct"].dropna()
    if len(sl) > 0:
        lines.append(f"  Médiane     : {sl.median():+.3f}%")
        lines.append(f"  Moyenne     : {sl.mean():+.3f}%")
        lines.append(f"  P75         : {sl.quantile(0.75):+.3f}%")
        lines.append(f"  P95         : {sl.quantile(0.95):+.3f}%")
        lines.append(f"  Pire ordre  : {sl.max():+.3f}%")
        if abs(sl.median()) > 0.2:
            lines.append("")
            lines.append(f"  ⚠️  Slippage médian > 0.2% — significatif.")
            lines.append(f"     Sur un round-trip : ~{sl.median()*2:.2f}% de friction par trade.")

    # ----- 3. Spread bid/ask au moment du fill -----
    lines.append("")
    lines.append("-" * 78)
    lines.append("3. SPREAD BID/ASK AU MOMENT DES FILLS")
    lines.append("-" * 78)
    sp = df_orders["spread_pct"].dropna()
    if len(sp) > 0:
        lines.append(f"  Médiane     : {sp.median():.3f}%")
        lines.append(f"  P75         : {sp.quantile(0.75):.3f}%")
        lines.append(f"  P95         : {sp.quantile(0.95):.3f}%")
    else:
        lines.append("  (Données indisponibles — connecter un client Binance pour les capter)")

    # ----- 4. Friction et PnL réel vs théorique -----
    if not df_trades.empty:
        lines.append("")
        lines.append("-" * 78)
        lines.append("4. PNL RÉEL vs PNL THÉORIQUE (le coeur du diagnostic)")
        lines.append("-" * 78)
        lines.append(f"  PnL théorique moyen : {df_trades['theoretical_pnl_pct'].mean():+.3f}%")
        lines.append(f"  PnL réel moyen      : {df_trades['real_pnl_pct'].mean():+.3f}%")
        lines.append(f"  Friction moyenne    : {df_trades['friction_pct'].mean():+.3f}% par trade")
        lines.append(f"  Friction médiane    : {df_trades['friction_pct'].median():+.3f}% par trade")
        win_th = (df_trades["theoretical_pnl_pct"] > 0).mean() * 100
        win_real = (df_trades["real_pnl_pct"] > 0).mean() * 100
        lines.append(f"  Win rate théorique  : {win_th:.1f}%")
        lines.append(f"  Win rate réel       : {win_real:.1f}%")
        lines.append(f"  Δ Win rate (perdu en friction) : {win_real - win_th:+.1f} pts")

        total_friction = df_trades["friction_pct"].sum()
        lines.append("")
        lines.append(f"  → Friction cumulée : {total_friction:.1f}% de PnL perdu sur {len(df_trades)} trades")

        # ----- 5. Par paire -----
        lines.append("")
        lines.append("-" * 78)
        lines.append("5. ANALYSE PAR PAIRE (top 15 par volume tradé)")
        lines.append("-" * 78)
        by_pair = df_trades.groupby("symbol").agg(
            n=("real_pnl_pct", "count"),
            real_pnl_mean=("real_pnl_pct", "mean"),
            real_pnl_total=("real_pnl_pct", "sum"),
            wr=("real_pnl_pct", lambda x: (x > 0).mean() * 100),
            friction_mean=("friction_pct", "mean"),
            vol_24h=("vol_24h_usdt", "median"),
        ).sort_values("n", ascending=False).head(15)

        lines.append(f"  {'Paire':<15}{'N':>5}{'WR%':>7}{'PnL/tr':>9}{'Frict':>8}{'PnL tot':>9}{'Vol24h':>12}")
        lines.append("  " + "-" * 75)
        for sym, r in by_pair.iterrows():
            vol_str = f"{r['vol_24h']/1e6:.1f}M" if pd.notna(r["vol_24h"]) else "?"
            lines.append(
                f"  {sym:<15}{int(r['n']):>5}{r['wr']:>6.1f}%"
                f"{r['real_pnl_mean']:>+8.2f}%{r['friction_mean']:>+7.2f}%"
                f"{r['real_pnl_total']:>+8.1f}%{vol_str:>12}"
            )

        # ----- 6. Corrélation liquidité ↔ friction -----
        lines.append("")
        lines.append("-" * 78)
        lines.append("6. CORRÉLATION LIQUIDITÉ ↔ FRICTION")
        lines.append("-" * 78)
        sub = df_trades.dropna(subset=["vol_24h_usdt", "friction_pct"])
        if len(sub) > 30:
            try:
                corr = sub["vol_24h_usdt"].corr(sub["friction_pct"], method="spearman")
                lines.append(f"  Corrélation Spearman : {corr:+.3f}")
                if corr < -0.2:
                    lines.append("  ✓ Plus la liquidité monte, moins il y a de friction.")
                    lines.append("    → Filtrer les paires à faible volume devrait améliorer la perf.")
                elif corr > 0.2:
                    lines.append("  ⚠️  Corrélation positive inattendue à investiguer.")
                else:
                    lines.append("  ~ Pas de corrélation claire — la friction vient d'autres facteurs.")
            except Exception:
                pass

        # ----- 7. Simulation : que ferait un filtre liquidité ? -----
        lines.append("")
        lines.append("-" * 78)
        lines.append("7. SIMULATION : FILTRES PROGRESSIFS DE LIQUIDITÉ")
        lines.append("-" * 78)
        lines.append(f"  {'Seuil vol 24h':<18}{'N trades':>10}{'WR%':>8}{'PnL réel':>11}{'PnL/trade':>12}")
        lines.append("  " + "-" * 60)
        for thr_m in [0, 1, 5, 10, 20, 50]:
            thr = thr_m * 1_000_000
            sub = df_trades[df_trades["vol_24h_usdt"].fillna(0) >= thr]
            if len(sub) < 5:
                continue
            wr = (sub["real_pnl_pct"] > 0).mean() * 100
            pnl_total = sub["real_pnl_pct"].sum()
            pnl_mean = sub["real_pnl_pct"].mean()
            label = f"≥ {thr_m}M$" if thr_m > 0 else "Tous"
            lines.append(f"  {label:<18}{len(sub):>10}{wr:>7.1f}%{pnl_total:>+10.1f}%{pnl_mean:>+11.3f}%")

    # Sauvegarde
    text = "\n".join(lines)
    (output_dir / "execution_diagnosis.txt").write_text(text)
    if not df_trades.empty:
        df_trades.to_parquet(output_dir / "trades_paired.parquet")

    return text


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--logs", required=True, help="Fichier .jsonl ou dossier")
    parser.add_argument("--output", default="./exec_diagnosis", help="Dossier de sortie")
    args = parser.parse_args()

    print(f"Chargement depuis {args.logs}...")
    df_orders = load_logs(Path(args.logs))
    print(f"  {len(df_orders)} ordres chargés")

    print("Appariement BUY/SELL...")
    df_trades = pair_buys_and_sells(df_orders)
    print(f"  {len(df_trades)} trades appariés")

    print("Génération du rapport...")
    text = report(df_orders, df_trades, Path(args.output))
    print("\n" + text)


if __name__ == "__main__":
    main()
