# ==============================================
# ON-DEMAND FAST PREDICTOR (SPX trend -> XSP ideas)
# ==============================================
from datetime import datetime
import os
import pandas as pd
import numpy as np
import scipy.stats as si
import pytz

# -----------------------
# CONFIG GLOBAL
# -----------------------
SMA_WINDOW = 5
MINUTES_TENDENCIA = 30

# Ajustes de ideas (equivalente a adjust 0,1,2,3)
ADJUST_LIST = [0, 1, 2, 3]
DISTANCE = 1  # tú estás usando 1 en tus llamadas

# -----------------------
# UTILS CSV
# -----------------------
def save_to_csv(data, output_csv):
    """Guarda los datos en un archivo CSV de manera acumulativa."""
    df = pd.DataFrame([data])
    if not os.path.exists(output_csv):
        df.to_csv(output_csv, index=False)
    else:
        df.to_csv(output_csv, mode='a', header=False, index=False)

# -----------------------
# PREP FEATURES
# -----------------------
def prepare_features(df):
    """
    Preparación de datos (igual a la tuya).
    IMPORTANTE: asume que existe gex_per_strike, underlying_price, strike, etc.
    """
    df['gex_change'] = df.groupby('strike')['gex_per_strike'].diff()
    df['gex_change_smooth'] = df['gex_change'].rolling(window=SMA_WINDOW).mean()
    df['price_sma'] = df['underlying_price'].rolling(window=SMA_WINDOW).mean()
    df.dropna(inplace=True)
    return df

# -----------------------
# TENDENCIA (SPX)
# -----------------------
def calculate_tendencia_30min(df, window_minutes=30, k=0.5):
    if df.empty:
        return "NO DATA", 0.0

    last_timestamp = df.index.max()
    start_time = last_timestamp - pd.Timedelta(minutes=window_minutes)
    df_30 = df.loc[df.index >= start_time].copy()

    if len(df_30) < 2:
        return "NO DATA", 0.0

    first_price = df_30['underlying_price'].iloc[0]
    last_price = df_30['underlying_price'].iloc[-1]
    priceTrend = (last_price - first_price) / first_price if first_price != 0 else 0

    sum_OI_call = df_30['open_interest_call'].sum()
    sum_OI_put  = df_30['open_interest_put'].sum()
    total_OI = abs(sum_OI_call) + abs(sum_OI_put)
    OI_diff = (sum_OI_call - sum_OI_put) / total_OI if total_OI != 0 else 0

    sum_vol_call = df_30['volume_call'].sum()
    sum_vol_put  = df_30['volume_put'].sum()
    total_vol = abs(sum_vol_call) + abs(sum_vol_put)
    Vol_diff = (sum_vol_call - sum_vol_put) / total_vol if total_vol != 0 else 0

    df_30['call_gex'] = df_30['gamma_call'] * df_30['open_interest_call'] * 100
    df_30['put_gex']  = df_30['gamma_put']  * df_30['open_interest_put']  * 100
    sum_call_gex = df_30['call_gex'].sum()
    sum_put_gex  = df_30['put_gex'].sum()
    total_gex = abs(sum_call_gex) + abs(sum_put_gex)
    GEX_diff = (sum_call_gex - sum_put_gex) / total_gex if total_gex != 0 else 0

    w_price = 0.25
    w_oi    = 0.25
    w_vol   = 0.25
    w_gex   = 0.25

    raw_score = (
        w_price * priceTrend +
        w_oi * OI_diff +
        w_vol * Vol_diff +
        w_gex * GEX_diff
    )

    actual_window = (df_30.index[-1] - df_30.index[0]).total_seconds() / 60.0
    damping_factor = actual_window / window_minutes if actual_window < window_minutes else 1.0
    adjusted_score = raw_score * damping_factor

    offset = -k * OI_diff
    final_score = adjusted_score + offset

    if final_score > 0:
        return "UP", final_score
    else:
        return "DOWN", final_score

# -----------------------
# PREDICTORS (SPX)
# -----------------------
def predict_close_oi_gex(df):
    df["oi_gex_weight"] = df["open_interest_call"] + df["open_interest_put"] + df["gex_per_strike"]
    total_ = df["oi_gex_weight"].sum()
    if total_ == 0:
        return 0
    predicted_close = (df["strike"] * df["oi_gex_weight"]).sum() / total_
    return round(predicted_close, 2)

def predict_close_delta_neutral(df):
    df["delta_total"] = df["delta_call"] + df["delta_put"]
    df["delta_abs"] = df["delta_total"].abs()
    total_ = df["delta_abs"].sum()
    if total_ == 0:
        return 0
    predicted_close = (df["strike"] * df["delta_abs"]).sum() / total_
    return round(predicted_close, 2)

def predict_close_volume(df):
    df["total_volume"] = df["volume_call"] + df["volume_put"]
    total_ = df["total_volume"].sum()
    if total_ == 0:
        return 0
    predicted_close = (df["strike"] * df["total_volume"]).sum() / total_
    return round(predicted_close, 2)

def calculate_market_gravity(df):
    df["F_put"]  = df["open_interest_put"]  * df["gamma_put"]  * df["delta_put"]  * df["volume_put"]
    df["F_call"] = df["open_interest_call"] * df["gamma_call"] * df["delta_call"] * df["volume_call"]
    df["F_total"] = df["F_call"] - df["F_put"]
    denom = df["F_total"].abs().sum()
    if denom == 0:
        return 0
    P_gravity = (df["strike"] * df["F_total"]).sum() / denom
    return round(P_gravity, 2)

def calculate_gex(df):
    # si timestamp es índice, lo reseteamos
    if 'timestamp' not in df.columns and df.index.name == 'timestamp':
        df = df.reset_index()

    if 'timestamp' not in df.columns:
        raise ValueError("❌ ERROR: La columna 'timestamp' no está en el DataFrame.")

    df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce")
    latest_timestamp = df["timestamp"].max()
    df_latest = df[df["timestamp"] == latest_timestamp].copy()
    if df_latest.empty:
        return 0.0, "N/A", None

    spot_price = df_latest["underlying_price"].iloc[0]
    df_latest = df_latest[(df_latest["strike"] >= spot_price - 20) & (df_latest["strike"] <= spot_price + 20)].copy()

    df_latest["gex_call"] = df_latest["gamma_call"] * df_latest["open_interest_call"] * df_latest["underlying_price"]
    df_latest["gex_put"]  = df_latest["gamma_put"]  * df_latest["open_interest_put"]  * df_latest["underlying_price"]
    df_latest["gex_total"] = df_latest["gex_call"] + df_latest["gex_put"]

    df_sorted = df_latest.sort_values(by="strike")
    df_sorted["gex_cumsum"] = df_sorted["gex_total"].cumsum()
    df_sorted["gex_smooth"] = df_sorted["gex_cumsum"].rolling(window=3, center=True, min_periods=1).mean()

    gamma_flip_strike = None
    prev_gex = None
    for _, row in df_sorted.iterrows():
        current_gex = row["gex_smooth"]
        if prev_gex is not None and prev_gex * current_gex < 0:
            gamma_flip_strike = row["strike"]
            break
        prev_gex = current_gex

    if gamma_flip_strike is None:
        min_gex_index = df_sorted["gex_smooth"].abs().idxmin()
        gamma_flip_strike = df_sorted.loc[min_gex_index, "strike"]

    gex_total = df_latest["gex_total"].sum()
    gamma_state = "Positiva" if gex_total > 0 else "Negativa"

    return round(gex_total, 2), gamma_state, gamma_flip_strike

def predict_closing_prices(df):
    gex_value, gamma_state, gamma_flip_strike = calculate_gex(df)
    return {
        "OI_GEX": predict_close_oi_gex(df),
        "Delta Neutro": predict_close_delta_neutral(df),
        "Volumen": predict_close_volume(df),
        "Punto de Gravedad": calculate_market_gravity(df),
        "GEX": gex_value,
        "Gamma State": gamma_state,
        "Gamma Flip Level": gamma_flip_strike
    }

# -----------------------
# EXPECTED MOVE
# -----------------------
def black_scholes(S, K, T, r, sigma, option_type="call"):
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if option_type == "call":
        price = S * si.norm.cdf(d1) - K * np.exp(-r * T) * si.norm.cdf(d2)
    else:
        price = K * np.exp(-r * T) * si.norm.cdf(-d2) - S * si.norm.cdf(-d1)
    return price

def calculate_expected_move(df):
    if 'timestamp' not in df.columns and df.index.name == 'timestamp':
        df = df.reset_index()
    if 'timestamp' not in df.columns:
        return None

    df['timestamp'] = pd.to_datetime(df['timestamp'], errors='coerce')
    df = df.dropna(subset=['timestamp'])
    if df.empty:
        return None

    latest_timestamp = df['timestamp'].max()
    df_latest = df[df['timestamp'] == latest_timestamp]
    if df_latest.empty:
        return None

    underlying_price = df_latest['underlying_price'].iloc[0]
    atm_strike = df_latest.iloc[(df_latest['strike'] - underlying_price).abs().argsort()[:1]]
    if atm_strike.empty:
        return None

    bid_call = atm_strike['bid_call'].values[0]
    ask_call = atm_strike['ask_call'].values[0]
    bid_put = atm_strike['bid_put'].values[0]
    ask_put = atm_strike['ask_put'].values[0]

    call_price = (bid_call + ask_call) / 2
    put_price = (bid_put + ask_put) / 2
    straddle_price = call_price + put_price

    T = 1 / 252
    r = 0.05

    if 'iv_call' in df_latest.columns and 'iv_put' in df_latest.columns:
        iv_atm = (df_latest['iv_call'].values[0] + df_latest['iv_put'].values[0]) / 2
    else:
        iv_atm = straddle_price / (underlying_price * np.sqrt(T))

    iv_atm = max(0.05, min(iv_atm, 0.5))
    expected_move = straddle_price * (1 + iv_atm)
    return round(expected_move, 2)

# -----------------------
# DOMINANT STRIKES
# -----------------------
def get_dominant_strikes(df, underlying_price):
    latest_timestamp = df.index.max()
    df_latest = df.loc[latest_timestamp]

    df_latest.loc[:, 'total_open_interest'] = df_latest['open_interest_call'] + df_latest['open_interest_put']
    df_latest.loc[:, 'total_volume'] = df_latest['volume_call'] + df_latest['volume_put']

    top_oi_strikes = df_latest[['strike', 'total_open_interest']].nlargest(2, 'total_open_interest')
    top_gex_strikes = df_latest[['strike', 'gex_per_strike']].nlargest(2, 'gex_per_strike')
    top_volume_strikes = df_latest[['strike', 'total_volume']].nlargest(2, 'total_volume')

    return {'oi': top_oi_strikes, 'gex': top_gex_strikes, 'volume': top_volume_strikes}

# -----------------------
# IDEAS (XSP) usando tendencia SPX
# -----------------------
def get_vertical_idea(df, tendencia, symbol_name, distance=5, adjust=0):
    if tendencia is None:
        return "NO DATA"
    tendencia = str(tendencia).lower()

    if df.empty:
        return "NO DATA"

    last_ts = df.index.max()
    expiry_str = last_ts.strftime("%d %b %y")
    df_last = df.loc[[last_ts]].copy()

    if tendencia == "up":
        df_last['put_diff'] = (df_last['delta_put'] + 0.20).abs()
        df_sorted = df_last.sort_values('put_diff')
        if df_sorted.empty:
            return "NO DATA"

        short_strike = round(df_sorted.iloc[0]['strike'] + adjust)
        long_strike = short_strike - distance

        df_short = df_last[df_last['strike'] == short_strike]
        df_long  = df_last[df_last['strike'] == long_strike]
        if df_short.empty or df_long.empty:
            return "NO DATA"

        short_mid = (df_short.iloc[0]['bid_put'] + df_short.iloc[0]['ask_put']) / 2
        long_mid  = (df_long.iloc[0]['bid_put']  + df_long.iloc[0]['ask_put'])  / 2
        net_credit = round(short_mid - long_mid, 2)

        return (f"SELL -1 Vertical {symbol_name.lstrip('$')} {expiry_str} "
                f"{int(short_strike)}/{int(long_strike)} PUT @{net_credit} LMT")

    elif tendencia == "down":
        df_last['call_diff'] = (df_last['delta_call'] - 0.20).abs()
        df_sorted = df_last.sort_values('call_diff')
        if df_sorted.empty:
            return "NO DATA"

        short_strike = round(df_sorted.iloc[0]['strike'] - adjust)
        long_strike  = short_strike + distance

        df_short = df_last[df_last['strike'] == short_strike]
        df_long  = df_last[df_last['strike'] == long_strike]
        if df_short.empty or df_long.empty:
            return "NO DATA"

        short_mid = (df_short.iloc[0]['bid_call'] + df_short.iloc[0]['ask_call']) / 2
        long_mid  = (df_long.iloc[0]['bid_call']  + df_long.iloc[0]['ask_call'])  / 2
        net_credit = round(short_mid - long_mid, 2)

        return (f"SELL -1 Vertical {symbol_name.lstrip('$')} {expiry_str} "
                f"{int(short_strike)}/{int(long_strike)} CALL @{net_credit} LMT")

    return "NO DATA"

def get_ironCondor_idea(df, tendencia, symbol_name, distance=5, adjust=0):
    if tendencia is None:
        return "NO DATA"
    tendencia = str(tendencia).lower()

    if df.empty:
        return "NO DATA"

    last_ts = df.index.max()
    expiry_str = last_ts.strftime("%d %b %y")

    if tendencia == "up":
        call_target = 0.10
        put_target = -0.12
    elif tendencia == "down":
        call_target = 0.12
        put_target = -0.10
    else:
        return "NO DATA"

    df_last = df.loc[[last_ts]].copy()
    df_last['call_diff'] = (df_last['delta_call'] - call_target).abs()
    df_last['put_diff']  = (df_last['delta_put']  - put_target).abs()

    df_call_sorted = df_last.sort_values('call_diff')
    df_put_sorted  = df_last.sort_values('put_diff')
    if df_call_sorted.empty or df_put_sorted.empty:
        return "NO DATA"

    short_call_strike = round(df_call_sorted.iloc[0]['strike'] - adjust, 0)
    long_call_strike  = short_call_strike + distance
    short_put_strike  = round(df_put_sorted.iloc[0]['strike'] + adjust, 0)
    long_put_strike   = short_put_strike - distance

    df_short_call = df_last[df_last['strike'] == short_call_strike]
    df_long_call  = df_last[df_last['strike'] == long_call_strike]
    df_short_put  = df_last[df_last['strike'] == short_put_strike]
    df_long_put   = df_last[df_last['strike'] == long_put_strike]
    if df_short_call.empty or df_long_call.empty or df_short_put.empty or df_long_put.empty:
        return "NO DATA"

    short_call_mid = (df_short_call.iloc[0]['bid_call'] + df_short_call.iloc[0]['ask_call']) / 2
    long_call_mid  = (df_long_call.iloc[0]['bid_call']  + df_long_call.iloc[0]['ask_call'])  / 2
    short_put_mid  = (df_short_put.iloc[0]['bid_put']   + df_short_put.iloc[0]['ask_put'])   / 2
    long_put_mid   = (df_long_put.iloc[0]['bid_put']    + df_long_put.iloc[0]['ask_put'])    / 2

    net_credit = round((short_call_mid - long_call_mid) + (short_put_mid - long_put_mid), 2)

    return (f"SELL -1 IRON CONDOR {symbol_name.lstrip('$')} {expiry_str} "
            f"[CALLS {int(short_call_strike)}/{int(long_call_strike)}] + "
            f"[PUTS {int(short_put_strike)}/{int(long_put_strike)}] @{net_credit} LMT")

# -----------------------
# SHORT TERM TREND (del output SPX)
# -----------------------
def calculate_short_term_trend(
    output_csv_path,
    columns=["prediccion_OI_GEX", "prediccion_Delta_Neutro", "prediccion_Volumen", "punto_de_gravedad"],
    minutes=60,
    threshold=0.95,
    debug=False
):
    try:
        df = pd.read_csv(output_csv_path, parse_dates=["timestamp"])
    except FileNotFoundError:
        return ("NO DATA", 0, 0)

    if df.empty:
        return ("NO DATA", 0, 0)

    df.sort_values(by="timestamp", inplace=True)
    df.set_index("timestamp", inplace=True)

    last_timestamp = df.index.max()
    start_time = last_timestamp - pd.Timedelta(minutes=minutes)
    df_window = df.loc[df.index >= start_time].copy()

    if len(df_window) < 2:
        return ("NO DATA", 0, 0)

    total_pos = 0
    total_neg = 0

    for col in columns:
        if col not in df_window.columns:
            continue
        series = df_window[col].dropna()
        if len(series) < 2:
            continue
        diffs = series.diff().dropna()
        total_pos += (diffs > 0).sum()
        total_neg += (diffs < 0).sum()

    total_changes = total_pos + total_neg
    if total_changes == 0:
        return ("FLAT", total_pos, total_neg)

    pos_ratio = total_pos / total_changes
    neg_ratio = total_neg / total_changes

    if pos_ratio > threshold:
        return ("UP", total_pos, total_neg)
    elif neg_ratio > threshold:
        return ("DOWN", total_pos, total_neg)
    else:
        return ("FLAT", total_pos, total_neg)



# -----------------------
# CORE: process snapshot pair
# -----------------------
def process_single_snapshot_pair(
    df_snapshot_spx,
    df_snapshot_xsp,
    real_last_timestamp,
    OUTPUT_CSV_SPX,
    OUTPUT_CSV_XSP,
    symbol_spx="$SPX",
    symbol_xsp="$XSP",
    DISTANCE=1,
    ADJUST_LIST=(0, 1, 2, 3)
):
    # =========================
    # 1) SPX => TENDENCIA / SCORE / PREDICCIONES
    # =========================
    df_spx = df_snapshot_spx.copy()
    df_spx.set_index("timestamp", inplace=True)

    if len(df_spx) < SMA_WINDOW:
        return

    df_spx_prepared = prepare_features(df_spx)

    tendencia_30min, score_30min = calculate_tendencia_30min(
        df_spx_prepared, window_minutes=MINUTES_TENDENCIA
    )

    predicted_closes = predict_closing_prices(df_spx_prepared)
    expected_move = calculate_expected_move(df_spx_prepared)

    latest_price_spx = df_spx_prepared["underlying_price"].iloc[-1]

    dominant_strikes = get_dominant_strikes(df_spx_prepared, latest_price_spx)
    OI_dominantes = dominant_strikes["oi"]["strike"].tolist()
    GEX_dominantes = dominant_strikes["gex"]["strike"].tolist()
    VOL_dominantes = dominant_strikes["volume"]["strike"].tolist()

    gamma_state = predicted_closes["Gamma State"]
    gex_value = predicted_closes["GEX"]
    gamma_flip_strike = predicted_closes["Gamma Flip Level"]

    short_trend, total_pos, total_neg = calculate_short_term_trend(
        output_csv_path=OUTPUT_CSV_SPX,
        columns=[
            "prediccion_OI_GEX",
            "prediccion_Delta_Neutro",
            "prediccion_Volumen",
            "punto_de_gravedad"
        ],
        minutes=60,
        threshold=0.95,
        debug=False
    )

    # =========================
    # 2) XSP => IDEAS (DIRIGIDAS POR SPX)
    # =========================
    df_xsp = df_snapshot_xsp.copy()
    df_xsp.set_index("timestamp", inplace=True)

    if len(df_xsp) < SMA_WINDOW:
        return

    df_xsp_prepared = prepare_features(df_xsp)
    df_latest_xsp = df_xsp_prepared.loc[
        [df_xsp_prepared.index.max()]
    ].copy()

    # --- Verticales XSP ---
    vertical_idea   = get_vertical_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[0])
    vertical_5      = get_vertical_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[1])
    vertical_10     = get_vertical_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[2])
    vertical_15     = get_vertical_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[3])

    # --- Iron Condors XSP ---
    ic_idea   = get_ironCondor_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[0])
    ic_5      = get_ironCondor_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[1])
    ic_10     = get_ironCondor_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[2])
    ic_15     = get_ironCondor_idea(df_latest_xsp, tendencia_30min, symbol_xsp, DISTANCE, ADJUST_LIST[3])

    # =========================
    # 3) OUTPUT SPX (SCHEMA BASE)
    # =========================
    data_spx = {
        "timestamp": real_last_timestamp.strftime("%Y-%m-%d %H:%M:%S"),
        "precio_actual": latest_price_spx,
        "tendencia_30min": tendencia_30min,
        "tendencia_corta": short_trend,
        "total_pos": total_pos,
        "total_neg": total_neg,
        "score_30min": score_30min,
        "strikes_OI": OI_dominantes,
        "strikes_GEX": GEX_dominantes,
        "strikes_Volumen": VOL_dominantes,
        "prediccion_OI_GEX": predicted_closes["OI_GEX"],
        "prediccion_Delta_Neutro": predicted_closes["Delta Neutro"],
        "prediccion_Volumen": predicted_closes["Volumen"],
        "punto_de_gravedad": predicted_closes["Punto de Gravedad"],
        "movimiento_esperado": expected_move,
        "Gamma_Exposure": gex_value,
        "Gamma_State": gamma_state,
        "Gamma_Flip_Level": gamma_flip_strike,
        "IDEA": None,
        "IDEA_adj5": None,
        "IDEA_adj10": None,
        "IDEA_adj15": None,
        "IDEA_IC": None,
        "IDEA_IC_adj5": None,
        "IDEA_IC_adj10": None,
        "IDEA_IC_adj15": None
    }

    # save_to_csv(data_spx, OUTPUT_CSV_SPX)

    # =========================
    # 4) OUTPUT XSP (MISMO SCHEMA – IDEAS XSP)
    # =========================
    data_xsp = {
        "timestamp": real_last_timestamp.strftime("%Y-%m-%d %H:%M:%S"),
        "precio_actual": round(latest_price_spx / 10, 2),  # luego puedes cambiarlo por underlying_price real XSP
        "tendencia_30min": tendencia_30min,
        "tendencia_corta": short_trend,
        "total_pos": total_pos,
        "total_neg": total_neg,
        "score_30min": score_30min,
        "strikes_OI": [int(round(float(s) / 10)) for s in OI_dominantes],
        "strikes_GEX": [int(round(float(s) / 10)) for s in GEX_dominantes],
        "strikes_Volumen": [int(round(float(s) / 10)) for s in VOL_dominantes],
        "prediccion_OI_GEX": round(predicted_closes["OI_GEX"]/10,2),
        "prediccion_Delta_Neutro": round(predicted_closes["Delta Neutro"]/10,2),
        "prediccion_Volumen": round(predicted_closes["Volumen"]/10,2),
        "punto_de_gravedad": round(predicted_closes["Punto de Gravedad"]/10,2),
        "movimiento_esperado": round(expected_move/10,2),
        "Gamma_Exposure": gex_value,
        "Gamma_State": gamma_state,
        "Gamma_Flip_Level": gamma_flip_strike,
        "IDEA": vertical_idea,
        "IDEA_adj5": vertical_5,
        "IDEA_adj10": vertical_10,
        "IDEA_adj15": vertical_15,
        "IDEA_IC": ic_idea,
        "IDEA_IC_adj5": ic_5,
        "IDEA_IC_adj10": ic_10,
        "IDEA_IC_adj15": ic_15
    }

    save_to_csv(data_xsp, OUTPUT_CSV_XSP)

# -----------------------
# FAST GENERATOR (2 inputs)
# -----------------------
def generate_prediction_fast_from_snapshots(
    INPUT_CSV_SPX,
    INPUT_CSV_XSP,
    OUTPUT_CSV_SPX,
    OUTPUT_CSV_XSP,
    symbol_spx="$SPX",
    symbol_xsp="$XSP"
):
    print(f"\n🚀 Cargando SPX: {INPUT_CSV_SPX}")
    print(f"🚀 Cargando XSP: {INPUT_CSV_XSP}")

    df_spx = pd.read_csv(INPUT_CSV_SPX, parse_dates=['timestamp'])
    df_xsp = pd.read_csv(INPUT_CSV_XSP, parse_dates=['timestamp'])

    if df_spx.empty or df_xsp.empty:
        print("⚠️ Uno de los archivos está vacío.")
        return

    df_spx.sort_values('timestamp', inplace=True)
    df_xsp.sort_values('timestamp', inplace=True)

    # 1) Filtrar segundos 10–50
    for df in (df_spx, df_xsp):
        df['second'] = df['timestamp'].dt.second
    df_spx = df_spx[(df_spx['second'] >= 10) & (df_spx['second'] <= 50)].copy()
    df_xsp = df_xsp[(df_xsp['second'] >= 10) & (df_xsp['second'] <= 50)].copy()
    df_spx.drop(columns=['second'], inplace=True)
    df_xsp.drop(columns=['second'], inplace=True)

    if df_spx.empty or df_xsp.empty:
        print("⚠️ No se encontraron snapshots en segundos 10–50.")
        return

    # 2) Desde 09:35 NY
    start_time = datetime.strptime("09:35:00", "%H:%M:%S").time()
    df_spx = df_spx[df_spx['timestamp'].dt.time >= start_time]
    df_xsp = df_xsp[df_xsp['timestamp'].dt.time >= start_time]
    if df_spx.empty or df_xsp.empty:
        print("⚠️ No hay datos después de 09:35.")
        return

    # 3) Un snapshot por minuto (último del minuto)
    for df in (df_spx, df_xsp):
        df["minute"] = df["timestamp"].dt.floor("min")

    df_spx_final = df_spx.sort_values("timestamp").groupby("minute").tail(1)
    df_xsp_final = df_xsp.sort_values("timestamp").groupby("minute").tail(1)

    # Usamos minutos de SPX como timeline base
    timestamps = df_spx_final['timestamp'].drop_duplicates().sort_values()
    print(f"✅ Minutos únicos encontrados (SPX): {len(timestamps)}")

    # limpiar outputs
    for path in (OUTPUT_CSV_SPX, OUTPUT_CSV_XSP):
        if os.path.exists(path):
            os.remove(path)

    # 4) Loop minuto a minuto
    for ts in timestamps:
        try:
            snap_spx = df_spx[df_spx['timestamp'] <= ts].copy()
            snap_xsp = df_xsp[df_xsp['timestamp'] <= ts].copy()

            if snap_spx.empty or snap_xsp.empty:
                continue

            process_single_snapshot_pair(
                snap_spx,
                snap_xsp,
                ts,
                OUTPUT_CSV_SPX,
                OUTPUT_CSV_XSP,
                symbol_spx=symbol_spx,
                symbol_xsp=symbol_xsp
            )

        except Exception as e:
            print(f"❌ Error en {ts}: {e}")

# ==========================================
# EJECUCIÓN POR FECHAS (ON DEMAND)
# ==========================================
PATH_UBUNTU = "/var/www/html"

SYMBOL_SPX = "$SPX"   # fuente de tendencia/score
SYMBOL_XSP = "$XSP"   # fuente de IDEAS

# ⚙️ Define la lista de fechas
fechas = [
    "2025-02-03", "2025-02-04", "2025-02-05", "2025-02-06", "2025-02-07",
    "2025-02-10", "2025-02-11", "2025-02-12", "2025-02-13", "2025-02-14",
    "2025-02-18", "2025-02-19", "2025-02-20", "2025-02-21",
    "2025-02-24", "2025-02-25", "2025-02-26", "2025-02-27", "2025-02-28",
    "2025-03-03", "2025-03-04", "2025-03-05", "2025-03-06", "2025-03-07",
    "2025-03-10", "2025-03-11", "2025-03-12", "2025-03-13", "2025-03-14",
    "2025-03-17", "2025-03-18", "2025-03-19", "2025-03-20", "2025-03-21",
    "2025-03-24", "2025-03-25", "2025-03-26", "2025-03-27", "2025-03-28",
    "2025-03-31",
    "2025-04-01", "2025-04-02", "2025-04-03", "2025-04-04",
    "2025-04-07", "2025-04-08", "2025-04-09", "2025-04-10", "2025-04-11",
    "2025-04-14", "2025-04-15", "2025-04-16", "2025-04-17", "2025-04-18",
    "2025-04-21", "2025-04-22", "2025-04-23", "2025-04-24", "2025-04-25",
    "2025-04-28", "2025-04-29", "2025-04-30", "2025-05-01", "2025-05-02",
    "2025-05-05", "2025-05-06", "2025-05-07", "2025-05-08", "2025-05-09",
    "2025-05-12", "2025-05-13", "2025-05-14", "2025-05-15", "2025-05-16",
    "2025-05-19", "2025-05-20", "2025-05-21", "2025-05-22", "2025-05-23",
    "2025-05-26", "2025-05-27", "2025-05-28", "2025-05-29", "2025-05-30",

    "2025-06-02", "2025-06-03", "2025-06-04", "2025-06-05", "2025-06-06",
    "2025-06-09", "2025-06-10", "2025-06-11", "2025-06-12", "2025-06-13",
    "2025-06-16", "2025-06-17", "2025-06-18", "2025-06-19", "2025-06-20",
    "2025-06-23", "2025-06-24", "2025-06-25", "2025-06-26", "2025-06-27",
    "2025-06-30",

    "2025-07-01", "2025-07-02", "2025-07-03", "2025-07-04",
    "2025-07-07", "2025-07-08", "2025-07-09", "2025-07-10", "2025-07-11",
    "2025-07-14", "2025-07-15", "2025-07-16", "2025-07-17", "2025-07-18",
    "2025-07-21", "2025-07-22", "2025-07-23", "2025-07-24", "2025-07-25",
    "2025-07-28", "2025-07-29", "2025-07-30", "2025-07-31",

    "2025-08-01",
    "2025-08-04", "2025-08-05", "2025-08-06", "2025-08-07", "2025-08-08",
    "2025-08-11", "2025-08-12", "2025-08-13", "2025-08-14", "2025-08-15",
    "2025-08-18", "2025-08-19", "2025-08-20", "2025-08-21", "2025-08-22",
    "2025-08-25", "2025-08-26", "2025-08-27", "2025-08-28", "2025-08-29",

    "2025-09-01", "2025-09-02", "2025-09-03", "2025-09-04", "2025-09-05",
    "2025-09-08", "2025-09-09", "2025-09-10", "2025-09-11", "2025-09-12",
    "2025-09-15", "2025-09-16", "2025-09-17", "2025-09-18", "2025-09-19",
    "2025-09-22", "2025-09-23", "2025-09-24", "2025-09-25", "2025-09-26",
    "2025-09-29", "2025-09-30",

    "2025-10-01", "2025-10-02", "2025-10-03",
    "2025-10-06", "2025-10-07", "2025-10-08", "2025-10-09", "2025-10-10",
    "2025-10-13", "2025-10-14", "2025-10-15", "2025-10-16", "2025-10-17",
    "2025-10-20", "2025-10-21", "2025-10-22", "2025-10-23", "2025-10-24",
    "2025-10-27", "2025-10-28", "2025-10-29", "2025-10-30", "2025-10-31",

    "2025-11-03", "2025-11-04", "2025-11-05", "2025-11-06", "2025-11-07",
    "2025-11-10", "2025-11-11", "2025-11-12", "2025-11-13", "2025-11-14",
    "2025-11-17", "2025-11-18", "2025-11-19", "2025-11-20", "2025-11-21",
    "2025-11-24", "2025-11-25", "2025-11-26", "2025-11-27", "2025-11-28",

    "2025-12-01", "2025-12-02", "2025-12-03", "2025-12-04", "2025-12-05",
    "2025-12-08", "2025-12-09", "2025-12-10", "2025-12-11", "2025-12-12"
]
def procesar_fechas(fechas):
    for fecha in fechas:
        input_spx = f"{PATH_UBUNTU}/flask_project/chains/optionChain_{SYMBOL_SPX}_{fecha}.csv"
        input_xsp = f"{PATH_UBUNTU}/flask_project/chains/optionChain_{SYMBOL_XSP}_{fecha}.csv"

        output_spx = f"{PATH_UBUNTU}/backtestingmarket/predictor_data/data2/prediction_{SYMBOL_SPX}_{fecha}.csv"
        output_xsp = f"{PATH_UBUNTU}/backtestingmarket/predictor_data/data2/prediction_{SYMBOL_XSP}_{fecha}.csv"

        print(f"\n🔎 Procesando fecha: {fecha}")
        if not os.path.exists(input_spx):
            print(f"⚠️ Archivo SPX no encontrado: {input_spx} (se salta)")
            continue
        if not os.path.exists(input_xsp):
            print(f"⚠️ Archivo XSP no encontrado: {input_xsp} (se salta)")
            continue

        try:
            generate_prediction_fast_from_snapshots(
                input_spx, input_xsp,
                output_spx, output_xsp,
                symbol_spx=SYMBOL_SPX,
                symbol_xsp=SYMBOL_XSP
            )

            # verificación rápida
            if os.path.exists(output_xsp):
                df_resultado = pd.read_csv(output_xsp)
                if df_resultado.empty:
                    print(f"⚠️ El archivo {output_xsp} se creó pero está vacío.")
                else:
                    print(f"✅ XSP predictor creado con {len(df_resultado)} registros: {output_xsp}")
            else:
                print(f"❌ No se generó el archivo XSP: {output_xsp}")

        except Exception as e:
            print(f"❌ Error procesando {fecha}: {e}")

# RUN
procesar_fechas(fechas)
