import matplotlib.ticker as mticker
import matplotlib.pyplot as plt
import pandas as pd
import pyarrow.parquet as pq
import os
import time
from datetime import datetime
import matplotlib
matplotlib.use("Agg")

PATH_UBUNTU = "/var/www/html/flask_project/"

# ========================= COLORES CONSOLA =========================

RED = "\033[91m"
GREEN = "\033[92m"
RESET = "\033[0m"


def colorize(value):
    if isinstance(value, (int, float)):
        color = GREEN if value >= 0 else RED
        return f"{color}{value}{RESET}"
    return value


# ========================= CONFIGURACION GENERAL =========================

DESPLAZAMIENTOS = {
    "cons": 0,
    "inte": -5,
    "agre": -10,
    "ultr": -15
}

desde = "2025-05-01"
hasta = "2026-04-17"
symbol = "XSP"
riesgo = "inte"   # cons | inte | agre | ultr

# ---- Elegir estrategia: "Vertical" o "IronCondor" ----
estrategia = "IronCondor"

CREDITO_TARGET = 0     # dolares
COMISION_POR_PATA = 0.51  # dolares por pata (solo entrada, se deja expirar)

ARCHIVO_TOP_HORARIOS = f"backtestingSemanal/top3/top3_horarios_{symbol}_{estrategia}_{riesgo}.csv"

# ========================= MULTI-HORA =========================
HORAS_FIJAS = ["1005", "1010", "1015", "1020", "1025", "1030", "1035", "1040", "1045", "1050", "1055", "1100", "1105", "1110", "1115", "1120", "1125", "1130", "1135", "1140", "1145", "1150", "1155", "1200", "1205", "1210", "1215", "1220", "1225", "1230", "1235", "1240", "1245", "1250", "1255", "1300", "1305", "1310", "1315", "1320", "1325", "1330", "1335", "1340", "1345", "1350", "1355", "1400"]
# HORAS_FIJAS = ["1200", "1205", "1210", "1215", "1220", "1225", "1230", "1235", "1240", "1245", "1250", "1255"]
# HORAS_FIJAS = None
# ==============================================================

if estrategia not in ("Vertical", "IronCondor"):
    raise ValueError(f"Estrategia no valida: '{estrategia}'. Usar 'Vertical' o 'IronCondor'.")

factor = 5 if symbol in ["SPY", "QQQ", "XSP"] else 1
desplazamiento = DESPLAZAMIENTOS.get(riesgo, 0) / factor
comision_x_trade = COMISION_POR_PATA * (2 if estrategia == "Vertical" else 4)

if HORAS_FIJAS:
    horas_a_procesar = HORAS_FIJAS
    print(f"\nModo MULTI-HORA: {len(horas_a_procesar)} horarios a procesar -> {horas_a_procesar}")
else:
    horas_a_procesar = [None]
    print("\nModo DINAMICA (Top 1 semana siguiente)")

print("\n" + "="*70)
print(f"BTM {estrategia.upper()} MULTI-HORA BACKTEST (FAST)".center(70))
print("="*70)
print(f"{'Fecha Desde:':25} {desde}")
print(f"{'Fecha Hasta:':25} {hasta}")
print(f"{'Simbolo:':25} {symbol}")
print(f"{'Estrategia:':25} {estrategia}")
print(f"{'Riesgo:':25} {riesgo}")
print(f"{'Credito Target ($):':25} {CREDITO_TARGET}")
print(f"{'Comision x Pata ($):':25} {COMISION_POR_PATA}")
print(f"{'Comision x Trade ($):':25} {comision_x_trade}")
print("="*70 + "\n")


# ========================= CARGA TOP HORARIOS =========================

def cargar_ventanas_top_horarios(archivo_top, rank_objetivo=1):
    df_top = pd.read_csv(archivo_top)
    df_top = df_top[df_top['rank'] == rank_objetivo].copy()
    df_top['end_date'] = pd.to_datetime(df_top['end_date'])
    df_top['hora_str'] = df_top['hora'].apply(lambda h: str(int(h)).zfill(4))
    df_top = df_top.sort_values('end_date').reset_index(drop=True)

    ventanas = []
    for i, row in df_top.iterrows():
        apply_from = row['end_date'] + pd.Timedelta(days=1)
        apply_until = df_top.loc[i + 1, 'end_date'] if i + 1 < len(df_top) else pd.Timestamp("2099-12-31")
        ventanas.append((apply_from, apply_until, row['hora_str']))

    return ventanas


def obtener_hora_para_fecha(fecha_dt, ventanas):
    for apply_from, apply_until, hora_str in ventanas:
        if apply_from <= fecha_dt <= apply_until:
            return hora_str
    return None


# ========================= CARGA CHAIN (PARQUET) =========================

def _load_chain(symbol, fecha_str, chain_cache=None):
    """
    Carga parquet del chain con índice [timestamp, strike].
    Si se pasa chain_cache (dict), reutiliza entradas ya cargadas
    en lugar de leer el disco de nuevo — crítico en modo MULTI-HORA.
    """
    if chain_cache is not None and fecha_str in chain_cache:
        return chain_cache[fecha_str]

    folder = os.path.join(PATH_UBUNTU, "chains")
    prefix = "$" if symbol in ["SPX", "RUT", "XSP"] else ""
    pq_path = os.path.join(folder, f"optionChain_{prefix}{symbol}_{fecha_str}.parquet")

    if not os.path.exists(pq_path):
        df = pd.DataFrame()
    else:
        df = pq.read_table(pq_path).to_pandas()
        df["avg_call"] = (df["bid_call"] + df["ask_call"]) / 2
        df["avg_put"] = (df["bid_put"] + df["ask_put"]) / 2
        df["timestamp"] = pd.to_datetime(df["timestamp"])

    if chain_cache is not None:
        chain_cache[fecha_str] = df

    return df


def obtener_ultimo_precio(df_chain):
    """Precio de cierre = último underlying_price del día."""
    if df_chain.empty:
        return None
    return df_chain.sort_values("timestamp")["underlying_price"].iloc[-1]


# ========================= VERTICAL VECTORIZADO =========================

def buscar_entrada_vertical_v(df_chain, strike1, strike2, option_type, entry_time, credito_target):
    try:
        col = f"avg_{option_type.lower()}"
        entry_time = entry_time.replace(second=30, microsecond=0)

        df = df_chain[df_chain["timestamp"] >= entry_time]
        df = df[df["strike"].isin([strike1, strike2])][["timestamp", "strike", col, "underlying_price"]]

        if df.empty:
            return None, None

        pivot = df.pivot_table(index="timestamp", columns="strike", values=col, aggfunc="first")

        if strike1 not in pivot.columns or strike2 not in pivot.columns:
            return None, None

        credits = (pivot[strike1] - pivot[strike2]) * 100

        if credito_target == 0:
            primer_ts = credits.index[0]
            credit_val = credits.iloc[0] / 100
            if credit_val > abs(strike1 - strike2):
                return None, None
            return credit_val, primer_ts

        cumple = (credits >= credito_target).fillna(False)

        if cumple.iloc[0]:
            primer_ts = cumple.index[0]
            credit_val = credits.iloc[0] / 100
            if credit_val > abs(strike1 - strike2):
                return None, None
            return credit_val, primer_ts

        confirmado = cumple.astype(int).rolling(3).sum() >= 3
        if not confirmado.any():
            return None, None

        primer_ts = confirmado[confirmado].index[0]
        return credito_target / 100, primer_ts

    except Exception as e:
        print(f"\n[ERROR] buscar_entrada_vertical_v: {e}")
        return None, None


def calcular_pnl_vertical(credit_entry, close_price, strike1, strike2, option_type):
    spread = abs(strike1 - strike2)
    if option_type == "CALL":
        strike_venta = min(strike1, strike2)
        strike_compra = max(strike1, strike2)
        if close_price > strike_compra:
            pnl = (credit_entry - spread) * 100
        elif close_price < strike_venta:
            pnl = credit_entry * 100
        else:
            pnl = credit_entry * 100 - (close_price - strike_venta) * 100
    else:
        strike_venta = max(strike1, strike2)
        strike_compra = min(strike1, strike2)
        if close_price < strike_compra:
            pnl = (credit_entry - spread) * 100
        elif close_price > strike_venta:
            pnl = credit_entry * 100
        else:
            pnl = credit_entry * 100 - (strike_venta - close_price) * 100
    return round(pnl, 2)


# ========================= IRON CONDOR VECTORIZADO =========================

def buscar_entrada_ic_v(df_chain, cs1, cs2, ps1, ps2, entry_time, credito_target):
    try:
        entry_time = entry_time.replace(second=30, microsecond=0)
        all_strikes = [cs1, cs2, ps1, ps2]

        df = df_chain[df_chain["timestamp"] >= entry_time]
        df = df[df["strike"].isin(all_strikes)][["timestamp", "strike", "avg_call", "avg_put"]]

        if df.empty:
            return None, None

        pivot_call = df.pivot_table(index="timestamp", columns="strike", values="avg_call", aggfunc="first")
        pivot_put = df.pivot_table(index="timestamp", columns="strike", values="avg_put",  aggfunc="first")

        if not all(s in pivot_call.columns for s in [cs1, cs2]):
            return None, None
        if not all(s in pivot_put.columns for s in [ps1, ps2]):
            return None, None

        idx = pivot_call.index.intersection(pivot_put.index)
        pivot_call = pivot_call.loc[idx]
        pivot_put = pivot_put.loc[idx]

        credit_call = pivot_call[cs1] - pivot_call[cs2]
        credit_put = pivot_put[ps1] - pivot_put[ps2]
        credits_total = (credit_call + credit_put) * 100

        put_spread = ps1 - ps2
        call_spread = cs2 - cs1
        max_credit = min(put_spread, call_spread)

        if credito_target == 0:
            primer_ts = credits_total.index[0]
            credit_val = (credit_call.iloc[0] + credit_put.iloc[0])
            if credit_val > max_credit:
                return None, None
            return credit_val, primer_ts

        cumple = (credits_total >= credito_target).fillna(False)

        if cumple.iloc[0]:
            primer_ts = cumple.index[0]
            credit_val = credit_call.iloc[0] + credit_put.iloc[0]
            if credit_val > max_credit:
                return None, None
            return credit_val, primer_ts

        confirmado = cumple.astype(int).rolling(3).sum() >= 3
        if not confirmado.any():
            return None, None

        primer_ts = confirmado[confirmado].index[0]
        credit_val = credito_target / 100
        return credit_val, primer_ts

    except Exception as e:
        print(f"\n[ERROR] buscar_entrada_ic_v: {e}")
        return None, None


def calcular_pnl_ic(credit_entry, close_price, cs1, cs2, ps1, ps2):
    call_spread = abs(cs2 - cs1)
    put_spread = abs(ps1 - ps2)
    pnl_call = 0 if close_price <= cs1 else (-call_spread if close_price >= cs2 else -(close_price - cs1))
    pnl_put = 0 if close_price >= ps1 else (-put_spread if close_price <= ps2 else -(ps1 - close_price))
    return round((credit_entry + pnl_call + pnl_put) * 100, 2)


# ========================= CARGA DE STRIKES =========================

def cargar_strikes(hora, symbol, estrategia, desplazamiento):
    path_makekos = "/var/www/html/backtestingmarket/predictor_data/makekos/"
    archivo_strikes = f"{path_makekos}{symbol}/{symbol}_{estrategia}_strikes_{hora}.csv"

    try:
        df_s = pd.read_csv(archivo_strikes)
        df_s['Day'] = pd.to_datetime(df_s['Day'])

        if estrategia == "Vertical":
            df_s['Strike1_Desplazado'] = df_s.apply(
                lambda r: r['Strike1'] + desplazamiento if r['Option_Type'] == 'CALL'
                else r['Strike1'] - desplazamiento, axis=1).astype(int)
            df_s['Strike2_Desplazado'] = df_s.apply(
                lambda r: r['Strike2'] + desplazamiento if r['Option_Type'] == 'CALL'
                else r['Strike2'] - desplazamiento, axis=1).astype(int)
        else:
            df_s['CS1'] = (df_s['Call_Strike1'] + desplazamiento).astype(int)
            df_s['CS2'] = (df_s['Call_Strike2'] + desplazamiento).astype(int)
            df_s['PS1'] = (df_s['Put_Strike1'] - desplazamiento).astype(int)
            df_s['PS2'] = (df_s['Put_Strike2'] - desplazamiento).astype(int)

        print(f"  OK Strikes cargados para hora {hora}")
        return df_s

    except FileNotFoundError:
        print(f"WARN Archivo no encontrado para hora {hora}: {archivo_strikes}")
        return None


# ========================= BARRA DE PROGRESO =========================

def imprimir_progreso(idx, total, tiempo_inicio, fecha_str):
    elapsed = time.time() - tiempo_inicio
    avg_seg = elapsed / idx
    eta_seg = avg_seg * (total - idx)
    pct = idx / total * 100
    bloques = int(pct / 5)
    barra = "#" * bloques + "-" * (20 - bloques)
    print(
        f"\r  [{barra}] {pct:5.1f}%  {idx}/{total}  "
        f"Elapsed: {time.strftime('%H:%M:%S', time.gmtime(elapsed))}  "
        f"ETA: {time.strftime('%H:%M:%S', time.gmtime(eta_seg))}  "
        f"Fecha: {fecha_str}   ",
        end="", flush=True
    )


# ========================= BACKTEST PRINCIPAL =========================

def backtest_unificado(ventanas, desde, hasta, symbol, estrategia, desplazamiento,
                       comision_x_trade, hora_fija=None, chain_cache=None):
    """
    chain_cache: dict compartido entre llamadas para evitar recargar
    los mismos parquets en cada hora del modo MULTI-HORA.
    Pasar siempre el mismo dict desde el loop principal.
    """
    resultados = []

    horas_unicas = [hora_fija] if hora_fija else list(set(v[2] for v in ventanas))
    modo_hora = hora_fija if hora_fija else "DINAMICA"

    strikes_por_hora = {}
    for hora in horas_unicas:
        strikes_por_hora[hora] = cargar_strikes(hora, symbol, estrategia, desplazamiento)

    desde_dt = pd.to_datetime(desde)
    hasta_dt = pd.to_datetime(hasta)

    todas_fechas = set()
    for df_s in strikes_por_hora.values():
        if df_s is not None:
            fechas = df_s[(df_s['Day'] >= desde_dt) & (df_s['Day'] <= hasta_dt)]['Day']
            todas_fechas.update(fechas.dt.date.tolist())

    total_fechas = len(todas_fechas)
    print(f"\n  Total fechas de trading encontradas: {total_fechas}")

    cached = sum(1 for f in todas_fechas if f.strftime("%Y-%m-%d") in (chain_cache or {}))
    if cached:
        print(f"  [{cached}/{total_fechas} fechas ya en cache — sin lectura de disco]")

    print("  Procesando...\n")

    tiempo_inicio = time.time()

    for idx, fecha_date in enumerate(sorted(todas_fechas), start=1):
        fecha_dt = pd.Timestamp(fecha_date)
        fecha_str = fecha_date.strftime("%Y-%m-%d")

        imprimir_progreso(idx, total_fechas, tiempo_inicio, fecha_str)

        # _load_chain usa chain_cache internamente: hit = no I/O, no alloc
        df_chain = _load_chain(symbol, fecha_str, chain_cache=chain_cache)

        close_price = obtener_ultimo_precio(df_chain)

        if hora_fija:
            hora = hora_fija
        else:
            hora = obtener_hora_para_fecha(fecha_dt, ventanas)
            if hora is None:
                print(f"\n  WARN Sin hora Top 1 para {fecha_str}, saltando...")
                continue

        df_s = strikes_por_hora.get(hora)
        if df_s is None:
            continue

        fila = df_s[df_s['Day'].dt.date == fecha_date]
        if fila.empty:
            continue

        r = fila.iloc[0]
        hora_fmt = f"{hora[:2]}:{hora[2:]}"
        entry_time = pd.to_datetime(f"{fecha_str} {hora_fmt}")

        if estrategia == "Vertical":
            option_type = r['Option_Type']
            strike1 = r['Strike1_Desplazado']
            strike2 = r['Strike2_Desplazado']

            credit_entry, entry_ts = buscar_entrada_vertical_v(
                df_chain, strike1, strike2, option_type, entry_time, CREDITO_TARGET
            )

            base = {
                "Hora": modo_hora, "Day": fecha_str, "Time": hora,
                "Strike1": strike1, "Strike2": strike2,
                "Option": option_type, "ClosePrice": close_price,
            }

            if credit_entry is None:
                resultados.append({**base, "EntryTime": None, "EntryCredit": None,
                                   "Exit": "NO_ENTRY", "Comision": 0.0, "P/L": 0.0})
                continue

            pnl_bruto = calcular_pnl_vertical(credit_entry, close_price, strike1, strike2, option_type)
            pnl_neto = round(pnl_bruto - comision_x_trade, 2)
            resultados.append({**base,
                               "EntryTime": entry_ts,
                               "EntryCredit": round(credit_entry * 100, 2),
                               "Exit": "CLOSE", "Comision": comision_x_trade, "P/L": pnl_neto})

        else:  # IronCondor
            cs1, cs2 = r['CS1'], r['CS2']
            ps1, ps2 = r['PS1'], r['PS2']

            if ps1 > cs1:
                continue

            credit_entry, entry_ts = buscar_entrada_ic_v(
                df_chain, cs1, cs2, ps1, ps2, entry_time, CREDITO_TARGET
            )

            base = {
                "Hora": modo_hora, "Day": fecha_str, "Time": hora,
                "Call_Strike1": cs1, "Call_Strike2": cs2,
                "Put_Strike1": ps1,  "Put_Strike2": ps2,
                "ClosePrice": close_price,
            }

            if credit_entry is None:
                resultados.append({**base, "EntryTime": None, "EntryCredit": None,
                                   "Exit": "NO_ENTRY", "Comision": 0.0, "P/L": 0.0})
                continue

            pnl_bruto = calcular_pnl_ic(credit_entry, close_price, cs1, cs2, ps1, ps2)
            pnl_neto = round(pnl_bruto - comision_x_trade, 2)
            resultados.append({**base,
                               "EntryTime": entry_ts,
                               "EntryCredit": round(credit_entry * 100, 2),
                               "Exit": "CLOSE", "Comision": comision_x_trade, "P/L": pnl_neto})

    # NO hacer chain_cache.clear() aquí — el cache es propiedad del llamador
    tiempo_total = time.time() - tiempo_inicio
    print()
    print(f"\n  Backtest completado en {time.strftime('%H:%M:%S', time.gmtime(tiempo_total))}\n")

    if not resultados:
        return pd.DataFrame()

    return pd.DataFrame(resultados)


# ========================= GRAFICO MENSUAL =========================

MESES_ES = {
    1: "Ene", 2: "Feb", 3: "Mar", 4: "Abr",
    5: "May", 6: "Jun", 7: "Jul", 8: "Ago",
    9: "Sep", 10: "Oct", 11: "Nov", 12: "Dic"
}


def generar_grafico_mensual(df, symbol, estrategia, riesgo, modo_hora,
                            credito_target, comision_x_trade, output_file, desde, hasta):
    df_trades = df[df['Exit'] == 'CLOSE'].copy()
    df_trades['Day'] = pd.to_datetime(df_trades['Day'])
    df_trades['Mes'] = df_trades['Day'].dt.to_period('M')

    monthly = df_trades.groupby('Mes').agg(
        PnL_Total=('P/L', 'sum'),
        Num_Trades=('P/L', 'count')
    ).reset_index()
    monthly['Mes_Label'] = monthly['Mes'].apply(
        lambda p: f"{MESES_ES[p.month]} {p.year}"
    )
    monthly['PnL_Total'] = monthly['PnL_Total'].round(2)

    colores = ['#2ecc71' if v >= 0 else '#e74c3c' for v in monthly['PnL_Total']]

    fig, ax1 = plt.subplots(figsize=(max(10, len(monthly) * 1.1), 6))
    fig.patch.set_facecolor('#1a1a2e')
    ax1.set_facecolor('#16213e')

    bars = ax1.bar(monthly['Mes_Label'], monthly['PnL_Total'],
                   color=colores, width=0.6, zorder=3, edgecolor='#0f3460', linewidth=0.8)
    ax1.axhline(0, color='#aaaaaa', linewidth=0.8, linestyle='--', zorder=2)

    for bar, pnl, n_trades in zip(bars, monthly['PnL_Total'], monthly['Num_Trades']):
        altura = bar.get_height()
        rango = monthly['PnL_Total'].abs().max() or 1
        offset_pnl = rango * 0.03
        offset_trades = rango * 0.10
        if altura >= 0:
            y_pnl, y_trades, va = altura + offset_pnl, altura + offset_trades, 'bottom'
        else:
            y_pnl, y_trades, va = altura - offset_pnl, altura - offset_trades, 'top'
        ax1.text(bar.get_x() + bar.get_width() / 2, y_pnl, f"${pnl:,.0f}",
                 ha='center', va=va, fontsize=8.5, fontweight='bold', color='white', zorder=5)
        ax1.text(bar.get_x() + bar.get_width() / 2, y_trades, f"{n_trades} trades",
                 ha='center', va=va, fontsize=7.5, color='#cccccc', zorder=5)

    ax1.set_xlabel("Mes", color='#cccccc', fontsize=10)
    ax1.set_ylabel("P/L Neto ($)", color='#cccccc', fontsize=10)
    ax1.tick_params(axis='x', colors='#cccccc', rotation=35, labelsize=8.5)
    ax1.tick_params(axis='y', colors='#cccccc', labelsize=9)
    ax1.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, _: f"${x:,.0f}"))
    for spine in ax1.spines.values():
        spine.set_edgecolor('#0f3460')
    ax1.grid(axis='y', color='#0f3460', linewidth=0.6, zorder=1)
    ymin, ymax = ax1.get_ylim()
    ax1.set_ylim(ymin * 1.22, ymax * 1.22)

    profit_total = monthly['PnL_Total'].sum()
    total_trades = monthly['Num_Trades'].sum()
    titulo = (
        f"{symbol}  |  {estrategia}  |  {riesgo.upper()}  |  Hora: {modo_hora}  |  {desde} → {hasta}\n"
        f"Rendimiento Mensual (neto comisiones)     "
        f"Profit Total: ${profit_total:,.2f}   "
        f"Trades: {total_trades}   "
        f"CT: ${credito_target}   "
        f"Com/trade: ${comision_x_trade:.2f}"
    )
    ax1.set_title(titulo, color='white', fontsize=10, pad=14)
    plt.tight_layout()

    img_file = output_file.replace(".csv", "_rendimiento_mensual.jpg")
    plt.savefig(img_file, dpi=150, bbox_inches='tight',
                facecolor=fig.get_facecolor(), format='jpg')
    plt.close()
    print(f"  Grafico guardado en: {img_file}\n")


# ========================= EJECUCION MULTI-HORA =========================

os.makedirs("resultados", exist_ok=True)

if not HORAS_FIJAS:
    ventanas = cargar_ventanas_top_horarios(ARCHIVO_TOP_HORARIOS, rank_objetivo=1)
    print("Ventanas de aplicacion (ultimas 5):")
    for v in ventanas[-5:]:
        print(f"  Desde {v[0].date()} hasta {v[1].date()} -> hora {v[2]}")
    print()
else:
    ventanas = []

todos_los_df = []
resumen_horas = []

# ── Cache global: se crea UNA vez y se reutiliza en todas las horas ──────────
# Los 265 parquets se leen una sola vez del disco (~primer hora).
# Las 47 horas restantes trabajan directamente sobre el dict en RAM.
# Esto evita los ~10 GB de allocations repetidas que matan el proceso.
chain_cache_global = {}
print(f"Cache global inicializado. Los parquets se cargaran una sola vez.\n")

for i, hora_actual in enumerate(horas_a_procesar, start=1):
    modo_hora = hora_actual if hora_actual else "DINAMICA"
    print(f"\n{'='*70}")
    print(f"  [{i}/{len(horas_a_procesar)}]  Procesando hora: {modo_hora}  "
          f"(cache: {len(chain_cache_global)} fechas)")
    print(f"{'='*70}")

    df_hora = backtest_unificado(
        ventanas, desde, hasta, symbol, estrategia, desplazamiento,
        comision_x_trade, hora_fija=hora_actual,
        chain_cache=chain_cache_global   # ← mismo dict para todas las horas
    )

    if df_hora.empty:
        print(f"  WARN Sin datos para hora {modo_hora}, saltando...\n")
        resumen_horas.append({
            "Hora": modo_hora, "Profit_Total": 0, "Comisiones": 0,
            "Wins": 0, "Losses": 0, "No_Trade_Days": 0, "Win_Rate_%": 0
        })
        continue

    profit_total = df_hora['P/L'].sum()
    comisiones_totales = df_hora['Comision'].sum()
    wins = (df_hora['P/L'] > 0).sum()
    losses = (df_hora['P/L'] < 0).sum()
    no_trades = (df_hora['Exit'] == "NO_ENTRY").sum()
    win_rate = (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0

    resumen_horas.append({
        "Hora":          modo_hora,
        "Profit_Total":  round(profit_total, 2),
        "Comisiones":    round(comisiones_totales, 2),
        "Wins":          int(wins),
        "Losses":        int(losses),
        "No_Trade_Days": int(no_trades),
        "Win_Rate_%":    round(win_rate, 2),
    })

    todos_los_df.append(df_hora)

    output_file = (
        f"resultadosNew/P&L_{estrategia}_{symbol}_HORA_{modo_hora}_"
        f"{desde.replace('-','')}_{hasta.replace('-','')}_{riesgo}_CT_{CREDITO_TARGET}.csv"
    )
    generar_grafico_mensual(
        df_hora, symbol, estrategia, riesgo, modo_hora,
        CREDITO_TARGET, comision_x_trade, output_file, desde, hasta
    )

# Liberar cache al terminar (opcional, el proceso termina igual)
chain_cache_global.clear()


# ========================= CSV UNIFICADO =========================

if todos_los_df:
    df_unificado = pd.concat(todos_los_df, ignore_index=True)

    filas_extra = []
    col0 = df_unificado.columns[0]
    col_last = df_unificado.columns[-1]

    filas_extra.append({col: "" for col in df_unificado.columns})
    header_row = {col: "" for col in df_unificado.columns}
    header_row[col0] = "===== RESUMEN POR HORA ====="
    filas_extra.append(header_row)

    for r in resumen_horas:
        for label, value in r.items():
            fila = {col: "" for col in df_unificado.columns}
            fila[col0] = label
            fila[col_last] = value
            filas_extra.append(fila)
        filas_extra.append({col: "" for col in df_unificado.columns})

    df_all_trades = df_unificado[df_unificado['Exit'] == 'CLOSE']
    global_profit = df_all_trades['P/L'].sum()
    global_wins = (df_all_trades['P/L'] > 0).sum()
    global_losses = (df_all_trades['P/L'] < 0).sum()
    global_wr = (global_wins / (global_wins + global_losses) * 100) if (global_wins + global_losses) > 0 else 0

    for label, value in [
        ("===== SUMMARY GLOBAL =====", ""),
        ("Estrategia",            estrategia),
        ("Riesgo",                riesgo),
        ("Horas",                 str(horas_a_procesar)),
        ("Profit Global (neto)",  round(global_profit, 2)),
        ("Wins Global",           int(global_wins)),
        ("Losses Global",         int(global_losses)),
        ("Win Rate Global (%)",   round(global_wr, 2)),
        ("Credito Target",        CREDITO_TARGET),
        ("Comision x Trade",      comision_x_trade),
        ("Generado",              datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
    ]:
        fila = {col: "" for col in df_unificado.columns}
        fila[col0] = label
        fila[col_last] = value
        filas_extra.append(fila)

    df_final = pd.concat([df_unificado, pd.DataFrame(filas_extra)], ignore_index=True)

    horas_label = f"{horas_a_procesar[0]}_{horas_a_procesar[-1]}_n{len(horas_a_procesar)}" if HORAS_FIJAS else "DINAMICA"
    output_unificado = (
        f"resultadosNew/P&L_{estrategia}_{symbol}_MULTI_{horas_label}_"
        f"{desde.replace('-','')}_{hasta.replace('-','')}_{riesgo}_CT_{CREDITO_TARGET}.csv"
    )

    df_final.to_csv(output_unificado, index=False)
    print(f"\nCSV unificado guardado en: {output_unificado}")


# ========================= TABLA COMPARATIVA FINAL =========================

print("\n" + "="*70)
print(f"  TABLA COMPARATIVA  —  {estrategia} | {symbol} | {riesgo.upper()}")
print("="*70)
print(f"  {'Hora':<10} {'Profit':>12} {'Wins':>6} {'Losses':>8} {'No Entry':>10} {'Win Rate':>10}")
print("  " + "-"*60)
for r in resumen_horas:
    print(
        f"  {r['Hora']:<10} ${r['Profit_Total']:>11,.2f} {r['Wins']:>6} "
        f"{r['Losses']:>8} {r['No_Trade_Days']:>10} {r['Win_Rate_%']:>9.2f}%"
    )
print("="*70 + "\n")
