from flask import Flask, request, jsonify
from flask import Flask, render_template, request, jsonify
import pandas as pd
import json
import glob
import os
import time
import pprint
from datetime import datetime
from collections import deque
import numpy as np
from concurrent.futures import ProcessPoolExecutor

    

PATH_UBUNTU = "/var/www/html/flask_project/"
pd.set_option('display.max_rows', None)  # Muestra todas las filas
# pd.set_option('display.max_columns', None)  # Muestra todas las columnas

app = Flask(__name__)

# Ruta para renderizar la página principal


@app.route('/')
def index():
    return render_template('backtesting.html')


# Modo Debug: True para ver mensajes en consola, False para ocultarlos
DEBUG_MODE = False


def debug_print(*args, **kwargs):
    """Imprime mensajes solo si DEBUG_MODE está activado."""
    if DEBUG_MODE:
        print(*args, **kwargs)

def load_csv(file_info):
    """Carga un CSV y devuelve su contenido."""
    file_date, file_path = file_info
    try:
        return file_date, pd.read_csv(file_path)
    except Exception as e:
        print(f"⚠ Error cargando {file_path}: {e}")
        return file_date, None  # Devolver None si hay error

def process_day_wrapper(args):
    """Función wrapper para `executor.map()`, desempaqueta `group` correctamente."""
    (file_date, day_df), horario, call_delta, put_delta, call_spread, put_spread, takeProfit, stopLoss, unit, desplazamiento = args
    return process_day(day_df, horario, call_delta, put_delta, call_spread, put_spread, takeProfit, stopLoss, unit, desplazamiento)


@app.route('/process_form', methods=['POST'])
def process_form():
    data = request.get_json()

    # Extraer información desde el JSON
    symbol = data.get('symbol')
    if symbol in ['SPX', 'XSP', 'RUT']:
        symbol = '$' + symbol

    fecha_desde = data.get('fechaDesde')
    fecha_hasta = data.get('fechaHasta')
    horario = data.get('horario') + ":00"
    call_delta = data['deltas']['call']
    put_delta = data['deltas']['put']
    takeProfit = data.get('takeProfit')
    stopLoss = data.get('stopLoss')
    unit = data.get('unit')  # Asegurar que se obtiene unit
    call_spread = data['spreads']['call']
    put_spread = data['spreads']['put']
    desplazamiento = float(data['desplazamiento'])


    # Convertir fechas a objetos datetime
    fecha_desde_dt = datetime.strptime(fecha_desde, '%Y-%m-%d')
    fecha_hasta_dt = datetime.strptime(fecha_hasta, '%Y-%m-%d')

    # Directorio donde están los archivos CSV
    directory_path = '/var/www/html/flask_project/chains'

    # Construir el patrón de búsqueda
    pattern = f"{directory_path}/optionChain_{symbol}_*.csv"

    files_with_dates = []

    for file_path in glob.glob(pattern):
        base_name = os.path.basename(file_path)
        parts = base_name.replace('.csv', '').split('_')

        # Verificar si el nombre tiene formato esperado
        if len(parts) < 3:
            print(f"⚠ Advertencia: Nombre inesperado en {base_name}, archivo ignorado.")
            continue

        file_date_str = parts[-1]

        try:
            file_date_dt = datetime.strptime(file_date_str, '%Y-%m-%d')
        except ValueError:
            print(f"⚠ Error: Fecha inválida en {base_name}, archivo ignorado.")
            continue

        # Agregar solo si la fecha está en el rango
        if fecha_desde_dt <= file_date_dt <= fecha_hasta_dt:
            files_with_dates.append((file_date_dt, file_path))

    # Ordenar archivos por fecha
    files_with_dates.sort()

    # Si no hay archivos válidos
    if not files_with_dates:
        return jsonify({
            'status': 'error',
            'message': 'No se encontraron archivos en el rango seleccionado.',
            'data': []
        })

    # 🔹 Leer archivos en paralelo
    with ProcessPoolExecutor() as executor:
        grouped = list(executor.map(load_csv, files_with_dates))

    # Filtrar días donde la carga de datos falló
    grouped = [(date, df) for date, df in grouped if df is not None]

    # Crear una lista de argumentos para `executor.map()`
    args_list = [(group, horario, call_delta, put_delta, call_spread, put_spread, takeProfit, stopLoss, unit, desplazamiento) for group in grouped]

    # 🔹 Procesar cada día en paralelo sin lambda
    with ProcessPoolExecutor() as executor:
        results = list(executor.map(process_day_wrapper, args_list))

    # Filtrar None (días descartados)
    results = [res for res in results if res is not None]

    csv_path = '/var/www/html/flask_project/spx_history_with_gap.csv'
    df_gap = pd.read_csv(csv_path)
    gap_dict = dict(zip(df_gap['date'], df_gap['gap']))

    for res in results:
        date_str = res['Time'].split(" ")[0]
        res['Gap'] = gap_dict.get(date_str)


    return jsonify({
        'status': 'success' if results else 'error',
        'data': results,
        'message': 'Datos procesados correctamente' if results else 'No se encontraron datos procesables.'
    })


def process_day(day_df, horario, call_delta, put_delta, call_spread, put_spread, takeProfit, stopLoss, unit, desplazamiento):
    """Procesa un solo día de datos y devuelve el resultado si es válido."""
    result = find_next_closest_time_rows(day_df, horario, call_delta, put_delta, call_spread, put_spread, takeProfit, stopLoss, unit, desplazamiento)
    return result if result else None


def find_next_closest_time_rows(df, target_time_str, call_delta, put_delta, call_spread, put_spread, takeProfit, stopLoss, unit, desplazamiento):
    """Encuentra las filas con el timestamp más cercano o posterior al tiempo objetivo, selecciona los strikes del Iron Condor y calcula el crédito inicial."""

    # Convertir la columna 'timestamp' a tipo datetime
    df['Time'] = pd.to_datetime(df['timestamp'], format='%Y-%m-%d %H:%M:%S')

    target_time = datetime.strptime(target_time_str, '%H:%M:%S').time()  # Solo la hora

    # Extraer solo la hora de los timestamps para comparar
    df['only_time'] = df['Time'].dt.time

    # Filtrar solo los timestamps que son iguales o posteriores al target_time
    future_times = df[df['only_time'] >= target_time].copy()

    if future_times.empty:
        debug_print(f"⚠ No hay timestamps iguales o posteriores a {target_time_str}")
        return None  # Retornar None si no hay datos

    # Calcular la diferencia de tiempo en segundos y encontrar el más cercano
    future_times['time_diff'] = future_times['only_time'].apply(
        lambda x: abs((datetime.combine(datetime.min, x) - datetime.combine(datetime.min, target_time)).total_seconds())
    )
    min_diff = future_times['time_diff'].min()

    # Seleccionar las filas con la menor diferencia de tiempo
    closest_rows = future_times[future_times['time_diff'] == min_diff].copy()

    # Obtener el timestamp exacto de apertura del Iron Condor
    opening_timestamp = closest_rows.iloc[0]['timestamp']

    # Mostrar en pantalla el timestamp más cercano encontrado y sus datos completos
    debug_print("Timestamp más cercano encontrado:")
    debug_print(closest_rows[['timestamp']].drop_duplicates())

    debug_print("\nFilas completas con el timestamp más cercano:")
    debug_print(closest_rows)

    # Calcular la diferencia de delta respecto a los valores deseados
    closest_rows['delta_call_diff'] = abs(closest_rows['delta_call'] - float(call_delta))
    closest_rows['delta_put_diff'] = abs(closest_rows['delta_put'] - float(put_delta))

    # Encontrar los strikes más cercanos a los deltas deseados (venta)
    idx_min_call_delta = closest_rows['delta_call_diff'].idxmin()
    idx_min_put_delta = closest_rows['delta_put_diff'].idxmin()

    sell_call = closest_rows.loc[idx_min_call_delta, 'strike']
    sell_put = closest_rows.loc[idx_min_put_delta, 'strike']

    # Determinar los strikes de compra en base a call_spread y put_spread
    buy_call = sell_call + float(call_spread)
    buy_put = sell_put - float(put_spread)

    # Mostrar los 4 strikes que conforman el Iron Condor
    debug_print("Strikes del Iron Condor:")
    debug_print(f"📌 Sell Call: {sell_call}")
    debug_print(f"📌 Buy Call: {buy_call}")
    debug_print(f"📌 Sell Put: {sell_put}")
    debug_print(f"📌 Buy Put: {buy_put}")

    # Calcular el crédito inicial
    credit = calculate_iron_condor_credit(closest_rows, sell_call, buy_call, sell_put, buy_put) 
    
    # Extraer el precio del SPX en el timestamp de apertura
    spx_price = closest_rows.iloc[0]["underlying_price"]

    if credit is None:
        debug_print(f"🚨 Día descartado: {opening_timestamp[:10]} - No se pudo calcular el crédito.")
        return None  # Devuelve None y evita continuar con este día

    # Mostrar el crédito recibido si no es None
    credit = credit - (desplazamiento/100)

    debug_print(f"\n💰 Crédito Inicial del Iron Condor: {credit:.2f}")

    # **LLAMAMOS A LA NUEVA FUNCIÓN DESDE AQUÍ CON EL TIMESTAMP DE APERTURA**
    resultado, final_credit, occur_time = evaluate_iron_condor_performance(df, sell_call, buy_call, sell_put, buy_put, credit, takeProfit, stopLoss, opening_timestamp)
    debug_print(f"📌 Resultado final del Iron Condor: {resultado}")

    return {
        "Time": opening_timestamp,
        "SPX Price": spx_price,  
        "Outcome": resultado,
        "Initial Credit": credit,
        "Credit at Occurrence": final_credit,
        "Occur Time": occur_time,
        "Stop Loss": stopLoss,
        "Take Profit": takeProfit,
        "Sell Call Strike": sell_call,
        "Buy Call Strike": buy_call,
        "Sell Put Strike": sell_put,
        "Buy Put Strike": buy_put
    }


def calculate_iron_condor_credit(df, sell_call, buy_call, sell_put, buy_put):
    """Calcula el crédito inicial del Iron Condor, validando que cada strike esté presente."""

    # Convertir a float para evitar problemas de comparación
    sell_call, buy_call, sell_put, buy_put = map(float, [sell_call, buy_call, sell_put, buy_put])

    # Asegurar que la columna 'strike' es float
    df['strike'] = df['strike'].astype(float)

    # Verificar si cada strike está presente en el DataFrame
    missing_strikes = [strike for strike in [sell_call, buy_call, sell_put, buy_put] if strike not in df['strike'].values]

    if missing_strikes:
        debug_print(f"⚠ Strikes faltantes en el DataFrame: {missing_strikes}")
        debug_print(f"📌 Strikes disponibles en el df: {df['strike'].unique()}")
        debug_print("❌ Día descartado debido a datos insuficientes para el Iron Condor.")
        return None  # ❌ Se retorna None para indicar que no se puede procesar

    try:
        # Obtener las filas correspondientes a cada strike
        sell_call_row = df[df['strike'] == sell_call].iloc[0]
        buy_call_row = df[df['strike'] == buy_call].iloc[0]
        sell_put_row = df[df['strike'] == sell_put].iloc[0]
        buy_put_row = df[df['strike'] == buy_put].iloc[0]

        # Calcular el precio promedio (Bid + Ask) / 2 para cada opción
        sell_call_price = (sell_call_row['bid_call'] + sell_call_row['ask_call']) / 2
        buy_call_price = (buy_call_row['bid_call'] + buy_call_row['ask_call']) / 2
        sell_put_price = (sell_put_row['bid_put'] + sell_put_row['ask_put']) / 2
        buy_put_price = (buy_put_row['bid_put'] + buy_put_row['ask_put']) / 2

        # Calcular el crédito inicial del Iron Condor
        credit = (sell_call_price - buy_call_price) + (sell_put_price - buy_put_price)

        debug_print("\n🔹 Valores de precios:")
        debug_print(f"📌 Sell Call ({sell_call}): {sell_call_price:.2f}")
        debug_print(f"📌 Buy Call ({buy_call}): {buy_call_price:.2f}")
        debug_print(f"📌 Sell Put ({sell_put}): {sell_put_price:.2f}")
        debug_print(f"📌 Buy Put ({buy_put}): {buy_put_price:.2f}")

        return credit

    except IndexError as e:
        debug_print(f"⚠ Error al seleccionar filas del DataFrame: {e}")
        return None


def evaluate_iron_condor_performance(df, sell_call, buy_call, sell_put, buy_put, initial_credit, take_profit, stop_loss, opening_timestamp):
    """Evalúa el desempeño del Iron Condor de manera optimizada."""

    take_profit = float(take_profit)  # TP en puntos
    stop_loss = float(stop_loss)  # SL en puntos
    initial_credit = float(initial_credit) * 100  # Multiplicar Crédito Inicial por 100
    debug_print("\n📊 Evaluando el desempeño del Iron Condor...")

    # Convertir y ordenar timestamps
    df["timestamp"] = pd.to_datetime(df["timestamp"])
    df = df[df["timestamp"] >= opening_timestamp].sort_values("timestamp")

    # Filtrar solo los strikes relevantes en una sola operación
    strikes_needed = {sell_call, buy_call, sell_put, buy_put}
    df = df[df["strike"].isin(strikes_needed)]

    if df.empty:
        debug_print("⚠ No hay datos suficientes después del timestamp de apertura.")
        return "Neutro", initial_credit, opening_timestamp.strftime('%H:%M:%S')

    # Convertir df en un diccionario de acceso rápido
    df_grouped = df.groupby("timestamp")

    # Calcular el mid-price para cada strike
    df["mid_call"] = (df["bid_call"] + df["ask_call"]) / 2
    df["mid_put"] = (df["bid_put"] + df["ask_put"]) / 2
    # Crear DataFrame pivotado para acceso rápido
    pivot_df = df.pivot(index="timestamp", columns="strike", values=["mid_call", "mid_put"])
    # Aplicar rolling window para suavizar precios (ventana de 3)
    pivot_df = pivot_df.rolling(window=3, min_periods=1).mean()

    # Calcular el crédito inicial del Iron Condor en cada timestamp
    pivot_df["credit"] = (
        (pivot_df["mid_call", sell_call] - pivot_df["mid_call", buy_call]) +
        (pivot_df["mid_put", sell_put] - pivot_df["mid_put", buy_put])
    ) * 100  # Multiplicar por 100


    # Iterar sobre los timestamps y evaluar TP/SL
    for timestamp, row in pivot_df.iterrows():
        # Asegurar que current_credit es un escalar, no una Serie
        current_credit = row["credit"]
        if isinstance(current_credit, pd.Series):
            current_credit = current_credit.iloc[0]

        debug_print(f"⏳ {timestamp} | Crédito: {current_credit:.2f} | P&L: {(initial_credit - current_credit):.2f}")

        if current_credit <= initial_credit - take_profit:  # Take Profit alcanzado
            debug_print(f"✅ Take Profit alcanzado en {timestamp} con crédito de {current_credit:.2f}")
            return "Ganancia", current_credit, timestamp.strftime('%H:%M:%S')

        if current_credit >= initial_credit + stop_loss:  # Stop Loss alcanzado
            debug_print(f"❌ Stop Loss alcanzado en {timestamp} con crédito de {current_credit:.2f}")
            return "Pérdida", current_credit, timestamp.strftime('%H:%M:%S')

    debug_print("📌 Iron Condor llegó al final sin alcanzar TP ni SL")
    return "Neutro", current_credit, timestamp.strftime('%H:%M:%S')

@app.route('/log_intraday', methods=['POST'])
def log_intraday():
    data = request.get_json()
    date_str = data.get('date')           # Ej: "2025-03-06"
    opening_time = data.get('openingTime')  # Ej: "10:30"
    strikes = data.get('strikes', {})
    timeOcurr= data.get('occurTime')
    initialCredit = data.get('initialCredit')

    print("=== Datos recibidos para evolución intradía ===")
    print("Fecha:", date_str)
    print("Hora de apertura:", opening_time)
    print("Strikes:", strikes)
    print("HORA TP/SL:", timeOcurr)
    print("Credito:", initialCredit)

    
    # Aseguramos que la hora tenga el formato HH:MM:SS
    if len(opening_time.split(':')) == 2:
        opening_time += ":00"
    
    symbol = "SPX"  # Se asume, ajustar si es variable
    csv_filename = f"optionChain_${symbol}_{date_str}.csv"
    csv_path = os.path.join('/var/www/html/flask_project/chains', csv_filename)
    
    try:
        # Calcular el crédito inicial y obtener el timestamp elegido
        initial_credit, chosen_timestamp = get_initial_credit(
            csv_path,
            opening_time,
            strikes.get('sellCall'),
            strikes.get('buyCall'),
            strikes.get('sellPut'),
            strikes.get('buyPut')
        )
        
        if initial_credit is None or chosen_timestamp is None:
            return jsonify({'status': 'error', 'message': 'No se pudo calcular el crédito inicial'}), 400
        
        print("Horario más cercano encontrado (apertura):", chosen_timestamp)
        print("Crédito inicial calculado:", initial_credit)
        initial_credit=float(initialCredit)*100
        
        # Obtener la evolución del crédito a partir de chosen_timestamp
        evolution = get_credit_evolution(
            csv_path,
            chosen_timestamp,
            strikes.get('sellCall'),
            strikes.get('buyCall'),
            strikes.get('sellPut'),
            strikes.get('buyPut'),
            initial_credit
        )
        
        # Convertir los timestamps de la evolución a string para JSON
        evolution_serializable = [
            {
                'timestamp': rec['timestamp'].strftime('%Y-%m-%d %H:%M:%S'),
                'credit': rec['credit'],
                'profit_loss': rec['profit_loss']
            }
            for rec in evolution
        ]
        
        return jsonify({
            'status': 'success', 
            'credit': initial_credit,
            'chosen_timestamp': chosen_timestamp.strftime('%Y-%m-%d %H:%M:%S'),
            'evolution': evolution_serializable,
            'message': 'Crédito inicial y evolución calculados correctamente'
        }), 200
        
    except Exception as e:
        print("Error al procesar el archivo:", e)
        return jsonify({'status': 'error', 'message': str(e)}), 500



def get_initial_credit(csv_file, target_horario, sell_call, buy_call, sell_put, buy_put):
    """
    csv_file: ruta del CSV (por ejemplo, "/var/www/html/flask_project/chains/optionChain_$SPX_2025-03-06.csv")
    target_horario: hora de apertura en formato "HH:MM:SS" (string)
    sell_call, buy_call, sell_put, buy_put: strikes (números o strings convertibles a float)
    
    Retorna:
      credit: crédito inicial calculado (multiplicado por 100)
      chosen_timestamp: el timestamp (datetime) del registro seleccionado
    """
    # Cargar el CSV
    df = pd.read_csv(csv_file)
    
    # Convertir la columna 'timestamp' a datetime
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    
    # Extraer la parte de la hora
    df['only_time'] = df['timestamp'].dt.time
    
    # Convertir target_horario a objeto time
    target_time = datetime.strptime(target_horario, '%H:%M:%S').time()
    
    # Filtrar filas cuya hora sea >= target_time
    df_after = df[df['only_time'] >= target_time]
    
    if not df_after.empty:
        # De las filas con hora >= target, elegir la que tenga la mínima diferencia
        df_after['time_diff'] = df_after['only_time'].apply(
            lambda t: (datetime.combine(datetime.min, t) - datetime.combine(datetime.min, target_time)).total_seconds()
        )
        min_diff = df_after['time_diff'].min()
        chosen_rows = df_after[df_after['time_diff'] == min_diff]
    else:
        # Si no hay filas con hora >= target, se elige la fila más cercana en sentido absoluto
        df['time_diff'] = df['only_time'].apply(
            lambda t: abs((datetime.combine(datetime.min, t) - datetime.combine(datetime.min, target_time)).total_seconds())
        )
        min_diff = df['time_diff'].min()
        chosen_rows = df[df['time_diff'] == min_diff]
    
    # Tomamos la primera fila del resultado
    chosen_timestamp = chosen_rows.iloc[0]['timestamp']
    
    # Función auxiliar para obtener el mid-price
    def get_mid_price(df_rows, strike, bid_col, ask_col):
        row = df_rows[df_rows['strike'] == float(strike)]
        if row.empty:
            return None
        bid = float(row.iloc[0][bid_col])
        ask = float(row.iloc[0][ask_col])
        return (bid + ask) / 2

    mid_sell_call = get_mid_price(chosen_rows, sell_call, 'bid_call', 'ask_call')
    mid_buy_call  = get_mid_price(chosen_rows, buy_call, 'bid_call', 'ask_call')
    mid_sell_put  = get_mid_price(chosen_rows, sell_put, 'bid_put', 'ask_put')
    mid_buy_put   = get_mid_price(chosen_rows, buy_put, 'bid_put', 'ask_put')
    
    if None in [mid_sell_call, mid_buy_call, mid_sell_put, mid_buy_put]:
        print("Error: No se encontraron datos para alguno de los strikes.")
        return None, None
    
    # Calcular el crédito inicial
    credit = ((mid_sell_call - mid_buy_call) + (mid_sell_put - mid_buy_put)) * 100
    return credit, chosen_timestamp

def compute_credit_for_group(group, sell_call, buy_call, sell_put, buy_put):
    """
    Calcula el crédito para un grupo de registros (mismo timestamp)
    utilizando las columnas bid/ask correspondientes a cada strike.
    Devuelve None si alguno de los strikes no se encuentra.
    """
    def get_mid(series_bid, series_ask):
        # Se asume que en el grupo habrá al menos una fila para el strike
        return (float(series_bid.iloc[0]) + float(series_ask.iloc[0])) / 2

    # Filtrar para cada strike y obtener el mid price
    try:
        mid_sell_call = get_mid(group[group['strike'] == float(sell_call)]['bid_call'],
                                  group[group['strike'] == float(sell_call)]['ask_call'])
        mid_buy_call = get_mid(group[group['strike'] == float(buy_call)]['bid_call'],
                               group[group['strike'] == float(buy_call)]['ask_call'])
        mid_sell_put = get_mid(group[group['strike'] == float(sell_put)]['bid_put'],
                               group[group['strike'] == float(sell_put)]['ask_put'])
        mid_buy_put = get_mid(group[group['strike'] == float(buy_put)]['bid_put'],
                              group[group['strike'] == float(buy_put)]['ask_put'])
    except IndexError:
        return None

    # Crédito calculado, se multiplica por 100 (según la lógica actual)
    credit = ((mid_sell_call - mid_buy_call) + (mid_sell_put - mid_buy_put)) * 100
    return credit


def get_credit_evolution(csv_file, start_timestamp, sell_call, buy_call, sell_put, buy_put, initial_credit):
    """
    csv_file: ruta del CSV diario.
    start_timestamp: timestamp (datetime) a partir del cual se evaluará la evolución.
    Los strikes son los valores correspondientes.
    initial_credit: el crédito inicial calculado.
    
    Devuelve una lista de diccionarios con:
      - timestamp (datetime)
      - credit (crédito calculado en ese timestamp)
      - profit_loss (diferencia entre initial_credit y el crédito actual)
    """
    df = pd.read_csv(csv_file)
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    
    # Filtrar registros con timestamp >= start_timestamp
    df = df[df['timestamp'] >= start_timestamp]
    
    evolution = []
    # Agrupar por timestamp (ya que en el CSV hay varias filas por cada timestamp)
    for ts, group in df.groupby('timestamp'):
        credit = compute_credit_for_group(group, sell_call, buy_call, sell_put, buy_put)
        if credit is not None:
            profit_loss = initial_credit - credit  # Ejemplo: si el crédito baja, se obtiene ganancia (o viceversa, según convención)
            evolution.append({
                'timestamp': ts,
                'credit': credit,
                'profit_loss': profit_loss
            })
    # Ordenar la evolución por timestamp (por si acaso)
    evolution = sorted(evolution, key=lambda x: x['timestamp'])
    return evolution


# Correr la aplicación en modo de desarrollo
if __name__ == "__main__":
    app.run(debug=True, port=5001)
