import requests
import json
from datetime import datetime
import os
import time
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import scipy.stats as si
import pytz


# ------------------------------------------------
# CONFIGURACIÓN GLOBAL
# ------------------------------------------------
# Lista de fechas a procesar
# fechas_a_procesar = [
#     "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"
# ]

# fechas_a_procesar = [
#     "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-21", "2025-04-22", "2025-04-23", "2025-04-24", "2025-04-25",
#     "2025-04-28", "2025-04-29", "2025-04-30"
# ]

fechas_a_procesar = [
    "2025-01-31"
    # "2025-05-05", "2025-05-06", "2025-05-07", "2025-05-08", "2025-05-09",
    # "2025-05-12", "2025-05-13", "2025-05-14",
]

# fechas_a_procesar = ["2025-05-15", "2025-05-16"]

symbol = "$SPX"
# fecha_actual = "2025-02-26"
PATH_UBUNTU = "/var/www/html"
TIME_INTERVAL = '1min'
SMA_WINDOW = 5
MINUTES_TENDENCIA = 30

# Rango para filtrar strikes cercanos al precio subyacente para "dominant strikes"
STRIKE_RANGE = 50

# ------------------------------------------------
# FUNCIONES AUXILIARES
# ------------------------------------------------


def get_ny_time():
    ny_tz = pytz.timezone('America/New_York')
    return datetime.now(ny_tz).strftime('%Y-%m-%d %H:%M:%S')


def is_market_open():
    """Verifica si el mercado está abierto entre 9:35 AM y 16:01 PM (hora NY)."""
    ny_time_str = get_ny_time()
    ny_time = datetime.strptime(ny_time_str, '%Y-%m-%d %H:%M:%S')
    market_open = ny_time.replace(hour=9, minute=35, second=0, microsecond=0)
    market_close = ny_time.replace(hour=16, minute=1, second=0, microsecond=0)
    return market_open <= ny_time <= market_close


def load_data(file_path):
    """Carga los datos del archivo CSV en un DataFrame."""
    df = pd.read_csv(file_path, parse_dates=['timestamp'])
    df.sort_values(by=['timestamp'], inplace=True)
    df.set_index('timestamp', inplace=True)
    return df


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)

# ------------------------------------------------
# PREPARACIÓN DE FEATURES (OPCIONAL)
# ------------------------------------------------


def prepare_features(df):
    """
    Ejemplo de preparación de datos:
    - Cálculo de gex_change y rolling media.
    - Ajusta o amplía según tu necesidad real.
    """
    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

# ------------------------------------------------
# STRIKES DOMINANTES (OI, GEX, VOL)
# ------------------------------------------------


def get_dominant_strikes(df, underlying_price):

    # Tomamos el snapshot completo más reciente
    latest_timestamp = df.index.max()
    df_latest = df.loc[latest_timestamp]

    # Cálculo de métricas (SIN filtrar por rango)
    df_latest['total_open_interest'] = df_latest['open_interest_call'] + df_latest['open_interest_put']
    df_latest['total_volume'] = df_latest['volume_call'] + df_latest['volume_put']

    # Top 2 strikes por OI
    top_oi_strikes = df_latest[['strike', 'total_open_interest']].nlargest(2, 'total_open_interest')
    # Top 2 strikes por GEX
    top_gex_strikes = df_latest[['strike', 'gex_per_strike']].nlargest(2, 'gex_per_strike')
    # Top 2 strikes por Volumen
    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
    }


# def get_dominant_strikes(df, underlying_price):
#     """
#     Determina los strikes con mayor Open Interest, GEX y Volumen
#     dentro de un rango +/- STRIKE_RANGE alrededor del precio subyacente.
#     Retorna un dict con dataframes que tienen los top 2 strikes para:
#        'oi', 'gex', y 'volume'
#     """
#     df_filtered = df[
#         (df['strike'] >= underlying_price - STRIKE_RANGE) &
#         (df['strike'] <= underlying_price + STRIKE_RANGE)
#     ].copy()

#     # Cálculo de métricas
#     df_filtered['total_open_interest'] = df_filtered['open_interest_call'] + df_filtered['open_interest_put']
#     df_filtered['total_volume'] = df_filtered['volume_call'] + df_filtered['volume_put']

#     # GEX por strike ya la tienes en df como 'gex_per_strike' (o la calculas si no existe)

#     # Tomamos sólo el último timestamp disponible en df_filtered
#     latest_timestamp = df_filtered.index.max()
#     df_latest = df_filtered.loc[latest_timestamp]

#     # Top 2 strikes por OI
#     top_oi_strikes = df_latest[['strike', 'total_open_interest']].nlargest(2, 'total_open_interest')
#     # Top 2 strikes por GEX
#     top_gex_strikes = df_latest[['strike', 'gex_per_strike']].nlargest(2, 'gex_per_strike')
#     # Top 2 strikes por Volumen
#     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
#     }

# ------------------------------------------------
# TENDENCIA AVANZADA + SCORE
# ------------------------------------------------


def calculate_tendencia_30min(df, window_minutes=30, k=0.5):
    """
    Determina una tendencia (UP / DOWN) y su score en base a
    precio, OI, volumen y GEX en los últimos window_minutes.

    Además, ajusta el score usando un offset dinámico derivado del imbalance de OI (NOII),
    con un factor k calibrado a partir de datos históricos.

    Si no hay suficientes datos (ej. primeros 30 min del mercado), retorna "NO DATA".

    Retorna:
    --------
    tendencia : str  ("UP", "DOWN", o "NO DATA")
    score     : float (valor del score, o 0.0 si no hay datos)
    """
    if df.empty:
        return "NO DATA", 0.0

    # 1) Filtrar ventana de los últimos window_minutes
    last_timestamp = df.index.max()
    start_time = last_timestamp - pd.Timedelta(minutes=window_minutes)
    df_30 = df.loc[df.index >= start_time].copy()

    # 2) Si no hay suficientes datos en la ventana, retorna "NO DATA"
    if len(df_30) < 2:
        return "NO DATA", 0.0

    # 3) Cálculo de priceTrend (variación relativa del precio)
    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

    # 4) Open Interest Diff (CALL - PUT) normalizado
    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

    # 5) Volumen Diff (CALL - PUT) normalizado
    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

    # 6) GEX Diff (CALL - PUT) normalizado
    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

    # 7) Calcular score ponderado sin offset
    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
    )

    # 8) Aplicar factor de amortiguamiento: si la ventana no está completa, se reduce el score
    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

    # 9) Calcular el offset dinámico basado en el imbalance de OI (NOII)
    # NOII ya es OI_diff; usamos k para calibrar
    offset = - k * OI_diff  # Si OI_diff es positivo (más calls), offset negativo; en días bajistas OI_diff podría ser negativo, ajustando según sea necesario.

    # Nota: Si históricamente en días bajistas OI_diff tiende a estar en un rango específico,
    # puedes calibrar k para que offset ≈ -0.1 cuando se requiera.
    final_score = adjusted_score + offset

    # 10) Determinar la tendencia final
    if final_score > 0:
        return "UP", final_score
    else:
        return "DOWN", final_score


# ------------------------------------------------
# PREDICCIÓN DE CIERRES (VARIOS MÉTODOS)
# ------------------------------------------------
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 predict_closing_prices(df):
    """Calcula varias predicciones de cierre, punto de gravedad y Gamma Exposure."""
    gex_value, gamma_state, gamma_flip_strike = calculate_gex(df)  # Solo pasamos 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
    }


def calculate_gex(df):
    """Calcula la Gamma Exposure total y el Gamma Flip Level con detección precisa del cambio de signo."""

    # 🔍 Restaurar 'timestamp' si está como índice
    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.")

    # 1️⃣ Convertir 'timestamp' a datetime
    df["timestamp"] = pd.to_datetime(df["timestamp"], format="%m/%d/%Y %I:%M:%S %p", errors="coerce")

    # 2️⃣ Filtrar solo el último timestamp disponible
    latest_timestamp = df["timestamp"].max()
    df_latest = df[df["timestamp"] == latest_timestamp].copy()

    # print(f"✅ Último timestamp disponible: {latest_timestamp}")  # DEBUG

    # 3️⃣ Obtener el último precio subyacente
    spot_price = df_latest["underlying_price"].iloc[0]

    # 4️⃣ Filtrar solo los strikes dentro de un rango ±20 puntos del SPX para mayor precisión
    df_latest = df_latest[(df_latest["strike"] >= spot_price - 20) & (df_latest["strike"] <= spot_price + 20)].copy()

    # 5️⃣ Calcular GEX por strike
    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"]

    # 6️⃣ Ordenar por strike y calcular GEX acumulado
    df_sorted = df_latest.sort_values(by="strike")
    df_sorted["gex_cumsum"] = df_sorted["gex_total"].cumsum()

    # 7️⃣ Aplicar media móvil de 3 strikes para suavizar `gex_cumsum`
    df_sorted["gex_smooth"] = df_sorted["gex_cumsum"].rolling(window=3, center=True, min_periods=1).mean()

    # 8️⃣ Encontrar el Gamma Flip Level como el primer strike donde `gex_smooth` cambia de signo
    gamma_flip_strike = None
    prev_gex = None

    for idx, row in df_sorted.iterrows():
        current_gex = row["gex_smooth"]

        if prev_gex is not None:
            if prev_gex * current_gex < 0:  # Cambio de signo detectado
                gamma_flip_strike = row["strike"]
                break
        prev_gex = current_gex

    # 🔍 Si no encontramos un cambio de signo, tomar el strike más cercano a `gex_smooth = 0`
    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"]

    # 🔟 Calcular el Gamma Exposure total
    gex_total = df_latest["gex_total"].sum()

    # 🔟 Determinar si la Gamma es Positiva o Negativa
    gamma_state = "Positiva" if gex_total > 0 else "Negativa"

    # print(f"📊 🎯 Gamma Flip Level Detectado: {gamma_flip_strike}")  # Log importante para validar

    return round(gex_total, 2), gamma_state, gamma_flip_strike


# ------------------------------------------------
# BLACK-SCHOLES PARA MOVIMIENTO ESPERADO
# ------------------------------------------------
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:
        print("Error: Falta columna 'timestamp'.")
        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]
    # Strike más cercano al precio actual (ATM)
    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  # un día
    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)


def get_ironCondor_idea(df, simulated_time, tendencia, distance=5, adjust=0):
    """
    Genera UNA idea de Iron Condor, devolviendo "NO DATA: <razon>" si falla algo.

    - Si la tendencia es 'UP':
        call_delta ~ +0.10
        put_delta  ~ -0.12
    - Si la tendencia es 'DOWN':
        call_delta ~ +0.12
        put_delta  ~ -0.10

    Ajusta los strikes cortos con 'adjust' y define las patas largas
    a ±distance del strike corto. Retorna el string de la operación
    o "NO DATA: <razon>" si no se pudo construir el Iron Condor.
    """

    # 1) Validar DataFrame
    if df.empty:
        return "NO DATA: DataFrame vacío"

    # 2) Filtrar por último timestamp
    last_ts = df.index.max()
    df_last = df.loc[[last_ts]].copy()
    if df_last.empty:
        return f"NO DATA: No hay filas con timestamp={last_ts}"

    # 3) Ajustar deltas según tendencia
    tendencia_lower = tendencia.lower()
    if tendencia_lower == "up":
        call_target = 0.10
        put_target = -0.12
    elif tendencia_lower == "down":
        call_target = 0.12
        put_target = -0.10
    else:
        return f"NO DATA: Tendencia desconocida => {tendencia}"

    # 4) Calcular diffs y extraer la mejor fila para CALL y PUT
    df_last["call_diff"] = (df_last["delta_call"] - call_target).abs()
    df_last["put_diff"] = (df_last["delta_put"] - put_target).abs()

    df_calls_sorted = df_last.sort_values("call_diff")
    if df_calls_sorted.empty:
        return "NO DATA: No se encontraron opciones para el lado CALL"

    row_call_short = df_calls_sorted.iloc[0]

    df_puts_sorted = df_last.sort_values("put_diff")
    if df_puts_sorted.empty:
        return "NO DATA: No se encontraron opciones para el lado PUT"

    row_put_short = df_puts_sorted.iloc[0]

    expiry_str = simulated_time.strftime("%d %b %y")
    # expiry_str = datetime.now().strftime("%d %b %y")

    # =========================================================
    # LADO CALL
    # =========================================================
    short_call_strike = row_call_short["strike"] - adjust
    long_call_strike = short_call_strike + distance

    df_short_call = df_last[df_last["strike"] == short_call_strike]
    if df_short_call.empty:
        return f"NO DATA: Strike CALL corto ({short_call_strike}) no existe"

    df_long_call = df_last[df_last["strike"] == long_call_strike]
    if df_long_call.empty:
        return f"NO DATA: Strike CALL largo ({long_call_strike}) no existe"

    row_short_call = df_short_call.iloc[0]
    row_long_call = df_long_call.iloc[0]

    short_call_mid = (row_short_call["bid_call"] + row_short_call["ask_call"]) / 2
    long_call_mid = (row_long_call["bid_call"] + row_long_call["ask_call"]) / 2
    net_credit_call = short_call_mid - long_call_mid

    # =========================================================
    # LADO PUT
    # =========================================================
    short_put_strike = row_put_short["strike"] + adjust
    long_put_strike = short_put_strike - distance

    df_short_put = df_last[df_last["strike"] == short_put_strike]
    if df_short_put.empty:
        return f"NO DATA: Strike PUT corto ({short_put_strike}) no existe"

    df_long_put = df_last[df_last["strike"] == long_put_strike]
    if df_long_put.empty:
        return f"NO DATA: Strike PUT largo ({long_put_strike}) no existe"

    row_short_put = df_short_put.iloc[0]
    row_long_put = df_long_put.iloc[0]

    short_put_mid = (row_short_put["bid_put"] + row_short_put["ask_put"]) / 2
    long_put_mid = (row_long_put["bid_put"] + row_long_put["ask_put"]) / 2
    net_credit_put = short_put_mid - long_put_mid

    # =========================================================
    # PRIMA NETA TOTAL
    # =========================================================
    net_credit = round(net_credit_call + net_credit_put, 2)

    # Retorno final
    return (
        f"SELL -1 IRON CONDOR {symbol.replace('$', '')} {expiry_str}"
        f"[CALLS {int(short_call_strike)}/{int(long_call_strike)}] + "
        f"[PUTS {int(short_put_strike)}/{int(long_put_strike)}] @"
        f"{net_credit} LMT"
    )


def get_vertical_idea(df, simulated_time, trend, distance=5, adjust=0):
    """
    Genera la 'Idea' de un vertical credit spread buscando delta ~ ±0.20,
    ordenando por la diferencia (call_diff / put_diff) en lugar de usar idxmin().

    Pasos:
    1) Toma el último timestamp (df.index.max()) => df_last
    2) Según trend:
       - DOWN => call_diff = |delta_call - 0.20|
       -  UP  => put_diff  = |delta_put  + 0.20|
       Ordena por esa diferencia de menor a mayor y toma la 1ra fila (iloc[0]).
    3) Determina la pata de compra (long_strike) a +/- distance.
    4) Calcula la prima neta (mid short - mid long).
    5) Retorna un string con la operación o "NO DATA" si algo falla.
    """

    if df.empty:
        print("[DEBUG] DataFrame vacío => NO DATA")
        return "NO DATA"

    # 1) Último timestamp en el índice
    last_ts = df.index.max()
    df_last = df.loc[[last_ts]].copy()
    if df_last.empty:
        print(f"[DEBUG] No hay filas con timestamp={last_ts}")
        return "NO DATA"

    expiry_str = simulated_time.strftime("%d %b %y")
    # expiry_str = datetime.now().strftime("%d %b %y")

    # ============================================================
    # CASO TENDENCIA 'DOWN': CALL CREDIT SPREAD (delta_call ~ +0.20)
    # ============================================================
    if trend.upper() == "DOWN":
        # Calculamos la diferencia para delta_call y ordenamos
        df_last["call_diff"] = (df_last["delta_call"] - 0.20).abs()
        df_sorted = df_last.sort_values("call_diff")
        # Seleccionamos el row con delta_call más cercano a +0.20 (sin ajuste)
        row_short = df_sorted.iloc[0]

        # Ajustamos el short_strike: restamos el adjust
        short_strike = row_short["strike"] - adjust
        # El long_strike se define a partir del short_strike ajustado (suma distance)
        long_strike = short_strike + distance

        # Buscamos el row correspondiente al short_strike ajustado
        df_short_adjusted = df_last[df_last["strike"] == short_strike]
        if df_short_adjusted.empty:
            print("[DEBUG] => Strike short ajustado no existe => NO DATA")
            return "NO DATA"
        row_short_adjusted = df_short_adjusted.iloc[0]

        # Buscamos el row correspondiente al long_strike ajustado
        df_long = df_last[df_last["strike"] == long_strike]
        print(f"[DEBUG] => Buscando long_strike={long_strike}, filas={df_long.shape[0]}")
        if df_long.empty:
            print("[DEBUG] => Strike de compra no existe => NO DATA")
            return "NO DATA"
        row_long = df_long.iloc[0]

        # Calculamos la prima neta usando los valores ajustados
        short_call_mid = (row_short_adjusted["bid_call"] + row_short_adjusted["ask_call"]) / 2
        long_call_mid = (row_long["bid_call"] + row_long["ask_call"]) / 2
        net_credit = round(short_call_mid - long_call_mid, 2)

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

    # ============================================================
    # CASO TENDENCIA 'UP': PUT CREDIT SPREAD (delta_put ~ -0.20)
    # ============================================================
    elif trend.upper() == "UP":
        # Calculamos la diferencia para delta_put
        df_last["put_diff"] = (df_last["delta_put"] + 0.20).abs()
        df_sorted = df_last.sort_values("put_diff")

        # Seleccionamos el row que tenga delta_put más cercano a -0.20 (sin ajuste)
        row_short = df_sorted.iloc[0]

        # Ajustamos el strike short y definimos el strike long
        short_strike = row_short["strike"] + adjust
        long_strike = short_strike - distance

        # Buscamos en el DataFrame el row correspondiente al short_strike ajustado
        df_short_adjusted = df_last[df_last["strike"] == short_strike]
        if df_short_adjusted.empty:
            print("[DEBUG] => Strike short ajustado no existe => NO DATA")
            return "NO DATA"
        row_short_adjusted = df_short_adjusted.iloc[0]

        # Buscamos el row correspondiente al long_strike ajustado
        df_long = df_last[df_last["strike"] == long_strike]
        print(f"[DEBUG] => Buscando long_strike={long_strike}, filas={df_long.shape[0]}")
        if df_long.empty:
            print("[DEBUG] => Strike de compra no existe => NO DATA")
            return "NO DATA"
        row_long = df_long.iloc[0]

        # Calculamos la prima neta usando los valores del strike ajustado
        short_put_mid = (row_short_adjusted["bid_put"] + row_short_adjusted["ask_put"]) / 2
        long_put_mid = (row_long["bid_put"] + row_long["ask_put"]) / 2
        net_credit = round(short_put_mid - long_put_mid, 2)

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

    # ==========================
    # TENDENCIA NO RECONOCIDA
    # ==========================
    else:
        # print(f"[DEBUG] Tendencia desconocida => {trend}")
        return "NO DATA"


def generate_input_ondemand(INPUT_CSV, INPUT_ONDEMAND_CSV, OUTPUT_ONDEMAND_CSV):
    if not os.path.exists(INPUT_CSV):
        print(f"❌ Archivo no encontrado: {INPUT_CSV}")
        return

    df_chain = pd.read_csv(INPUT_CSV, parse_dates=['timestamp'])
    df_chain.sort_values(by=['timestamp'], inplace=True)

    # start_time = df_chain["timestamp"].min()
    raw_start_time = df_chain["timestamp"].min()
    forced_start_time = pd.Timestamp(raw_start_time.strftime("%Y-%m-%d") + " 09:35:00")
    start_time = max(raw_start_time, forced_start_time)

    end_time = df_chain["timestamp"].max()

    print(f"🔄 Generando `{INPUT_ONDEMAND_CSV}` desde {start_time} hasta {end_time}...")

    # Leer minutos ya guardados
    minutos_guardados = set()
    if os.path.exists(OUTPUT_ONDEMAND_CSV):
        try:
            df_out = pd.read_csv(OUTPUT_ONDEMAND_CSV, parse_dates=["timestamp"])
            minutos_guardados = set(df_out["timestamp"].dt.strftime("%Y-%m-%d %H:%M"))
        except Exception as e:
            print(f"⚠️ Error leyendo OUTPUT_ONDEMAND_CSV: {e}")

    simulated_time = start_time
    while simulated_time <= end_time:
        df_simulated = df_chain[df_chain["timestamp"] <= simulated_time]
        if df_simulated.empty:
            simulated_time += timedelta(seconds=60)
            continue

        real_last_timestamp = df_simulated["timestamp"].max()
        minuto_str = real_last_timestamp.strftime("%Y-%m-%d %H:%M")

        # ✅ Solo procesar si ese minuto aún no fue guardado
        if minuto_str not in minutos_guardados:
            df_simulated.to_csv(INPUT_ONDEMAND_CSV, index=False)

            try:
                process_data(real_last_timestamp, INPUT_ONDEMAND_CSV, OUTPUT_ONDEMAND_CSV)
                minutos_guardados.add(minuto_str)
            except Exception as e:
                print(f"Error en la ejecución: {e}")

        simulated_time += timedelta(seconds=30)

    print(f"✅ Simulación finalizada. Minutos únicos guardados: {len(minutos_guardados)}")


# ----------------------------
# FUNCIÓN QUE PROCESA LOS DATOS
# ----------------------------


def process_data(real_last_timestamp, INPUT_ONDEMAND_CSV, OUTPUT_ONDEMAND_CSV):
    try:
        # 1) Cargar data "cruda" (cadena de opciones) y prepararla
        df = load_data(INPUT_ONDEMAND_CSV)
        if len(df) < SMA_WINDOW:
            print("No hay suficientes datos para cálculos más profundos.")
            return

        # 2) Preparamos features (por ejemplo, rolling, etc.)
        df_prepared = prepare_features(df)

        # 3) Cálculo de TENDENCIA 30 MIN + SCORE
        tendencia_30min, score_30min = calculate_tendencia_30min(df_prepared, window_minutes=30)

        # 4) Predicciones de cierre y otras (devuelven un dict con 4 valores)
        predicted_closes = predict_closing_prices(df_prepared)
        expected_move = calculate_expected_move(df_prepared)

        # 5) Insertar las 4 columnas en la última fila, para que queden en el DF
        last_ts = df_prepared.index.max()
        df_prepared.loc[last_ts, "prediccion_OI_GEX"] = predicted_closes["OI_GEX"]
        df_prepared.loc[last_ts, "prediccion_Delta_Neutro"] = predicted_closes["Delta Neutro"]
        df_prepared.loc[last_ts, "prediccion_Volumen"] = predicted_closes["Volumen"]
        df_prepared.loc[last_ts, "punto_de_gravedad"] = predicted_closes["Punto de Gravedad"]

        # ------------------------------------------------------------------
        # (NUEVO) AGRUPAR POR TIMESTAMP (1 FILA POR MINUTO)
        # ------------------------------------------------------------------
        # Esto evita que diff=0 cuando hay múltiples strikes con el mismo valor
        df_grouped = df_prepared.groupby(level='timestamp').last().copy()
        # df_grouped ahora tiene 1 fila por cada timestamp/fecha-hora

        # 6) Cálculo de TENDENCIA CORTA usando el DF agrupado
        short_trend, total_pos, total_neg = calculate_short_term_trend(
            output_csv_path=OUTPUT_ONDEMAND_CSV,
            columns=["prediccion_OI_GEX", "prediccion_Delta_Neutro", "prediccion_Volumen", "punto_de_gravedad"],
            minutes=60,
            threshold=0.95,
            debug=True  # Activa logs
        )

        # 7) Obtenemos el último precio subyacente
        #    (lo tomamos de df_prepared o del df_grouped, da igual)
        latest_price = df_prepared['underlying_price'].iloc[-1]

        # 8) STRIKES DOMINANTES
        dominant_strikes_dict = get_dominant_strikes(df_prepared, latest_price)
        OI_dominantes = dominant_strikes_dict['oi']['strike'].tolist()
        GEX_dominantes = dominant_strikes_dict['gex']['strike'].tolist()
        VOL_dominantes = dominant_strikes_dict['volume']['strike'].tolist()

        # 9) Estado de Gamma y Gamma Flip Level (ya calculados en 'predicted_closes')
        gamma_state = predicted_closes["Gamma State"]
        gex_value = predicted_closes["GEX"]
        gamma_flip_strike = predicted_closes["Gamma Flip Level"]


        if symbol in ["$SPX", "$RUT"]:
            distance = 5
            adjust_values = [0, 5, 10, 15]
        else:
            distance = 1
            adjust_values = [0, 1, 2, 3]



        # 10) Idea de vertical spread
        df_latest = df_prepared.loc[[last_ts]].copy()  # o df_grouped.tail(1)
        # vertical_idea = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=0)
        # vertical_idea_adjust_5 = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=5)
        # vertical_idea_adjust_10 = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=10)
        # vertical_idea_adjust_15 = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=15)

        # ironCondor_idea = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=0)
        # ironCondor_idea_adjust_5 = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=5)
        # ironCondor_idea_adjust_10 = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=10)
        # ironCondor_idea_adjust_15 = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=5, adjust=15)


        vertical_idea = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[0])
        vertical_idea_adjust_5 = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[1])
        vertical_idea_adjust_10 = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[2])
        vertical_idea_adjust_15 = get_vertical_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[3])
        
        ironCondor_idea = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[0])
        ironCondor_idea_adjust_5 = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[1])
        ironCondor_idea_adjust_10 = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[2])
        ironCondor_idea_adjust_15 = get_ironCondor_idea(df_latest, real_last_timestamp, tendencia_30min, distance=distance, adjust=adjust_values[3])

        # 11) Guardar resultado en OUTPUT_ONDEMAND_CSV con timestamp simulado
        data_to_save = {
            "timestamp": real_last_timestamp.strftime('%Y-%m-%d %H:%M:%S'),
            "precio_actual": latest_price,
            "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": vertical_idea,
            "IDEA_adj5": vertical_idea_adjust_5,
            "IDEA_adj10": vertical_idea_adjust_10,
            "IDEA_adj15": vertical_idea_adjust_15,
            "IDEA_IC": ironCondor_idea,
            "IDEA_IC_adj5": ironCondor_idea_adjust_5,
            "IDEA_IC_adj10": ironCondor_idea_adjust_10,
            "IDEA_IC_adj15": ironCondor_idea_adjust_15
        }
        save_to_csv(data_to_save, OUTPUT_ONDEMAND_CSV)

    except Exception as e:
        print(f"Error en la ejecución: {e}")


# ----------------------------
# FUNCIÓN PARA PROCESAR CADA FECHA
# ----------------------------
def process_date(fecha_actual):
    # Definir rutas de archivos
    INPUT_CSV = f"{PATH_UBUNTU}/flask_project/chains/optionChain_{symbol}_{fecha_actual}.csv"
    INPUT_ONDEMAND_CSV = f"{PATH_UBUNTU}/backtestingmarket/predictor_data/data2/optionChain_{symbol}_{fecha_actual}_OD8.csv"
    OUTPUT_ONDEMAND_CSV = f"{PATH_UBUNTU}/backtestingmarket/predictor_data/data2/prediction_{symbol}_{fecha_actual}_OD8.csv"

    # Eliminar archivos previos si existen
    for archivo in [INPUT_ONDEMAND_CSV, OUTPUT_ONDEMAND_CSV]:
        if os.path.exists(archivo):
            os.remove(archivo)
            print(f"✅ Archivo eliminado: {archivo}")

    # Ejecutar simulación para la fecha actual
    generate_input_ondemand(INPUT_CSV, INPUT_ONDEMAND_CSV, OUTPUT_ONDEMAND_CSV)


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):
    """
    Lee el CSV acumulado (OUTPUT_ONDEMAND_CSV) con una fila por minuto
    y las columnas de predicción. Retorna (trend, total_pos, total_neg)
    donde trend es "UP", "DOWN", "FLAT" o "NO DATA".
    """

    import pandas as pd

    # 1) Cargar CSV
    try:
        df = pd.read_csv(output_csv_path, parse_dates=["timestamp"])
    except FileNotFoundError:
        if debug:
            print(f"[DEBUG] No se encontró el archivo: {output_csv_path}")
        return ("NO DATA", 0, 0)  # <-- 3 elementos

    if df.empty:
        if debug:
            print("[DEBUG] El CSV está vacío => NO DATA")
        return ("NO DATA", 0, 0)  # <-- 3 elementos

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

    if not pd.api.types.is_datetime64_any_dtype(df.index):
        if debug:
            print("[DEBUG] Índice no es datetime => NO DATA")
        return ("NO DATA", 0, 0)  # <-- 3 elementos

    # 2) Filtrar los últimos `minutes` de datos
    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:
        if debug:
            print(f"[DEBUG] Menos de 2 filas en los últimos {minutes} min => NO DATA")
        return ("NO DATA", 0, 0)  # <-- 3 elementos

    if debug:
        print("[DEBUG] df_window shape:", df_window.shape)
        print("[DEBUG] df_window columns:", df_window.columns)

    # 3) Contar diffs
    total_pos = 0
    total_neg = 0

    for col in columns:
        if col not in df_window.columns:
            if debug:
                print(f"[DEBUG] Columna '{col}' no existe => se ignora.")
            continue

        series = df_window[col].dropna()
        if len(series) < 2:
            if debug:
                print(f"[DEBUG] Columna '{col}' con <2 datos => se ignora.")
            continue

        diffs = series.diff().dropna()
        col_pos = (diffs > 0).sum()
        col_neg = (diffs < 0).sum()
        total_pos += col_pos
        total_neg += col_neg

        if debug:
            print(f"[DEBUG] {col}: diffs={len(diffs)}, +={col_pos}, -={col_neg}")

    total_changes = total_pos + total_neg

    if total_changes == 0:
        if debug:
            print("[DEBUG] total_changes=0 => todos diffs=0 => FLAT")
        return ("FLAT", total_pos, total_neg)  # <-- 3 elementos

    pos_ratio = total_pos / total_changes
    neg_ratio = total_neg / total_changes

    if debug:
        print(f"[DEBUG] total_pos={total_pos}, total_neg={total_neg}, "
              f"pos_ratio={pos_ratio:.2f}, neg_ratio={neg_ratio:.2f}")

    # 4) Determinar la tendencia
    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)


# ----------------------------
# EJECUCIÓN DEL CÓDIGO
# ----------------------------
if __name__ == "__main__":
    for fecha in fechas_a_procesar:
        print(f"\n🚀 Procesando fecha: {fecha}")
        process_date(fecha)


# for f in prediction_*_FAST.csv; do mv "$f" "${f/_FAST.csv/.csv}"; done
