import matplotlib.ticker as mticker
import matplotlib.pyplot as plt
import pandas as pd
import pyarrow.parquet as pq
import os
import glob
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"
YELLOW = "\033[93m"
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 = "SPX"
riesgo = "ultr"   # 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)

# ---- Generar JPGs de rendimiento mensual por hora ----
GENERAR_JPG = False   # True = genera | False = saltea (mas rapido)

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) v2".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


# ========================= INCREMENTAL: BUSCAR CACHE =========================

def buscar_csv_previo(directorio, estrategia, symbol, horas_a_procesar, desde, riesgo, credito_target):
    """
    Busca el CSV de la corrida anterior para esta combo.
    El nombre fijo por combo es: P&L_{estrategia}_{symbol}_MULTI_{horas_label}_{desde_str}_*_{riesgo}_CT_{ct}.csv
    El * representa el hasta anterior (que no conocemos, puede haber cambiado).
    Retorna el path del archivo más reciente que matchee, o None si no existe.
    """
    horas_label = f"{horas_a_procesar[0]}_{horas_a_procesar[-1]}_n{len(horas_a_procesar)}" if HORAS_FIJAS else "DINAMICA"
    desde_str = desde.replace('-', '')
    patron = os.path.join(
        directorio,
        f"P&L_{estrategia}_{symbol}_MULTI_{horas_label}_{desde_str}_*_{riesgo}_CT_{credito_target}.csv"
    )
    candidatos = glob.glob(patron)
    if not candidatos:
        return None
    # Si hay varios (no debería), tomar el más reciente por fecha de modificación
    return max(candidatos, key=os.path.getmtime)


def cargar_trades_previos(csv_path):
    """
    Carga las filas de trades válidas del CSV previo, descartando el bloque resumen.
    Una fila es trade válida si la columna 'Day' contiene una fecha parseable.
    Retorna un DataFrame limpio con solo las filas de trades.
    """
    df_raw = pd.read_csv(csv_path, dtype=str)

    # Filtrar: solo filas donde 'Day' es una fecha válida (formato YYYY-MM-DD)
    def es_fecha_valida(val):
        try:
            pd.to_datetime(val)
            return True
        except:
            return False

    mask = df_raw['Day'].apply(es_fecha_valida)
    df_trades = df_raw[mask].copy()

    # Restaurar tipos correctos
    df_trades['Day'] = pd.to_datetime(df_trades['Day']).dt.strftime('%Y-%m-%d')
    for col in ['Call_Strike1', 'Call_Strike2', 'Put_Strike1', 'Put_Strike2']:
        if col in df_trades.columns:
            df_trades[col] = pd.to_numeric(df_trades[col], errors='coerce')
    for col in ['Strike1', 'Strike2']:
        if col in df_trades.columns:
            df_trades[col] = pd.to_numeric(df_trades[col], errors='coerce')
    df_trades['ClosePrice'] = pd.to_numeric(df_trades['ClosePrice'], errors='coerce')
    df_trades['EntryCredit'] = pd.to_numeric(df_trades['EntryCredit'], errors='coerce')
    df_trades['Comision'] = pd.to_numeric(df_trades['Comision'], errors='coerce')
    df_trades['P/L'] = pd.to_numeric(df_trades['P/L'], errors='coerce')
    df_trades['Hora'] = df_trades['Hora'].astype(str)
    df_trades['Time'] = df_trades['Time'].astype(str)

    return df_trades.reset_index(drop=True)


def detectar_fecha_inicio_incremental(df_trades_previos, hasta_nuevo):
    """
    Dado el DataFrame de trades previos, detecta la última fecha procesada
    y devuelve el día siguiente como nueva fecha de inicio para el backtest.
    Si no hay trades previos, retorna None (correr todo desde `desde`).
    """
    if df_trades_previos is None or df_trades_previos.empty:
        return None

    ultima_fecha = pd.to_datetime(df_trades_previos['Day']).max()
    nueva_fecha_inicio = ultima_fecha + pd.Timedelta(days=1)
    nueva_fecha_inicio_str = nueva_fecha_inicio.strftime('%Y-%m-%d')

    hasta_dt = pd.to_datetime(hasta_nuevo)
    if nueva_fecha_inicio > hasta_dt:
        return "NADA_NUEVO"

    return nueva_fecha_inicio_str


# ========================= 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)

        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})

    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("resultadosNew", 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 = []

# ========================= LOGICA INCREMENTAL =========================

DIRECTORIO_RESULTADOS = "/var/www/html/backtestingmarket/resultadosNew"

csv_previo = buscar_csv_previo(
    DIRECTORIO_RESULTADOS, estrategia, symbol,
    horas_a_procesar, desde, riesgo, CREDITO_TARGET
)

df_trades_previos = None
desde_calculo = desde  # por defecto correr todo desde `desde`

if csv_previo:
    print(f"{YELLOW}[INCREMENTAL] Archivo previo encontrado:{RESET}")
    print(f"  {csv_previo}")
    df_trades_previos = cargar_trades_previos(csv_previo)
    ultima_fecha = pd.to_datetime(df_trades_previos['Day']).max().strftime('%Y-%m-%d')
    print(f"  Trades cargados: {len(df_trades_previos):,} filas")
    print(f"  Ultima fecha en cache: {ultima_fecha}")

    nueva_fecha = detectar_fecha_inicio_incremental(df_trades_previos, hasta)

    if nueva_fecha == "NADA_NUEVO":
        print(f"\n{GREEN}[INCREMENTAL] El archivo ya cubre hasta {hasta}. Nada nuevo que procesar.{RESET}")
        print("  Si cambiaste el 'hasta' a una fecha futura, volvé a correr cuando haya datos.")
        exit(0)

    desde_calculo = nueva_fecha
    print(f"  {GREEN}Backtest incremental: solo se procesaran fechas desde {desde_calculo}{RESET}")
    print(f"  (Se evitan {len(df_trades_previos):,} filas ya calculadas)\n")
else:
    print(f"{YELLOW}[INCREMENTAL] No se encontro archivo previo para esta combo.{RESET}")
    print(f"  Se correra el backtest completo desde {desde} hasta {hasta}\n")

# ── Cache global de chains ────────────────────────────────────────────────────
chain_cache_global = {}
print(f"Cache global de chains inicializado.\n")

todos_los_df = []
resumen_horas = []

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}")

    # ── Correr backtest solo para fechas nuevas ──────────────────────────────
    df_nuevos = pd.DataFrame()
    if desde_calculo <= hasta:
        df_nuevos = backtest_unificado(
            ventanas, desde_calculo, hasta, symbol, estrategia, desplazamiento,
            comision_x_trade, hora_fija=hora_actual,
            chain_cache=chain_cache_global
        )

    # ── Combinar previos + nuevos para esta hora ─────────────────────────────
    if df_trades_previos is not None and not df_trades_previos.empty:
        df_hora_prev = df_trades_previos[
            df_trades_previos['Hora'].astype(str) == str(modo_hora)
        ].copy()
        # Filtrar por el `desde` configurado (por si cambió)
        df_hora_prev = df_hora_prev[
            pd.to_datetime(df_hora_prev['Day']) >= pd.to_datetime(desde)
        ]
    else:
        df_hora_prev = pd.DataFrame()

    if not df_nuevos.empty and not df_hora_prev.empty:
        df_hora = pd.concat([df_hora_prev, df_nuevos], ignore_index=True)
    elif not df_hora_prev.empty:
        df_hora = df_hora_prev
    elif not df_nuevos.empty:
        df_hora = df_nuevos
    else:
        df_hora = pd.DataFrame()

    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"
    )
    if GENERAR_JPG:
        generar_grafico_mensual(
            df_hora, symbol, estrategia, riesgo, modo_hora,
            CREDITO_TARGET, comision_x_trade, output_file, desde, hasta
        )
    else:
        print(f"  JPG salteado (GENERAR_JPG=False)\n")

chain_cache_global.clear()


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

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

    # ── Borrar CSV previo antes de guardar el nuevo ───────────────────────────
    if csv_previo and os.path.exists(csv_previo):
        os.remove(csv_previo)
        print(f"\n[INCREMENTAL] CSV previo eliminado: {os.path.basename(csv_previo)}")

    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"{DIRECTORIO_RESULTADOS}/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")