Spaces:
Running
Running
| from fastapi import FastAPI | |
| from pydantic import BaseModel | |
| import yfinance as yf | |
| import ta | |
| import pandas as pd | |
| import numpy as np | |
| from tensorflow.keras.models import load_model | |
| import joblib | |
| import warnings | |
| import os | |
| warnings.filterwarnings('ignore') | |
| app = FastAPI() | |
| print("🚀 Servidor FastAPI encendido. Esperando peticiones de n8n...") | |
| # ========================================================= | |
| # 🚨 TRUCO ANTI-CRASH: Arrancamos con la RAM vacía | |
| # ========================================================= | |
| MODELOS = {} | |
| # IMPORTANTE: Revisa que estos nombres coincidan EXACTAMENTE con los que subiste | |
| CONFIG = { | |
| "ORO": {"ticker": "GC=F", "modelo": "cerebro_oro.keras", "scaler": "scaler_oro.pkl"}, | |
| "NVDA": {"ticker": "NVDA", "modelo": "cerebro_nvidia.keras", "scaler": "scaler_nvda.pkl"}, | |
| "WTI": {"ticker": "CL=F", "modelo": "cerebro_wti.keras", "scaler": "scaler_wti.pkl"}, | |
| "NASDAQ": {"ticker": "^NDX", "modelo": "cerebro_nasdaq.keras", "scaler": "scaler_nasdaq.pkl"} | |
| } | |
| # 🚨 NUEVO: El servidor ahora espera recibir el símbolo y el sentimiento | |
| class PredictionRequest(BaseModel): | |
| symbol: str | |
| sentimiento: float = 0.0 # Si n8n no manda nada, asume Neutral (0.0) | |
| def predecir_mercado(req: PredictionRequest): | |
| activo_solicitado = req.symbol.upper() | |
| traductor = { | |
| "GC=F": "ORO", "XAUUSD=X": "ORO", "ORO": "ORO", | |
| "NVDA": "NVDA", | |
| "CL=F": "WTI", "WTI": "WTI", "USOIL": "WTI", | |
| "^IXIC": "NASDAQ", "^NDX": "NASDAQ", "NQ=F": "NASDAQ", "NASDAQ": "NASDAQ" | |
| } | |
| if activo_solicitado not in traductor: | |
| return {"error": f"Activo no soportado. Usa uno de estos: {list(CONFIG.keys())}"} | |
| id_activo = traductor[activo_solicitado] | |
| # ========================================================= | |
| # 🚨 LAZY LOADING: Solo carga la IA a la RAM si n8n la pide | |
| # ========================================================= | |
| if id_activo not in MODELOS: | |
| print(f"🧠 Despertando IA de {id_activo} por primera vez...") | |
| archivo_mod = CONFIG[id_activo]["modelo"] | |
| archivo_scl = CONFIG[id_activo]["scaler"] | |
| if not os.path.exists(archivo_mod) or not os.path.exists(archivo_scl): | |
| return {"error": f"¡Falta el archivo {archivo_mod} o {archivo_scl} en Hugging Face!"} | |
| try: | |
| MODELOS[id_activo] = { | |
| "ticker": CONFIG[id_activo]["ticker"], | |
| "modelo": load_model(archivo_mod), | |
| "escalador": joblib.load(archivo_scl) | |
| } | |
| except Exception as e: | |
| return {"error": f"Error al cargar la IA de {id_activo}: {str(e)}"} | |
| ticker = MODELOS[id_activo]["ticker"] | |
| modelo = MODELOS[id_activo]["modelo"] | |
| escalador = MODELOS[id_activo]["escalador"] | |
| # 🎯 Obtener Precio Vivo | |
| try: | |
| df_minuto = yf.download(ticker, period="1d", interval="1m", progress=False) | |
| if isinstance(df_minuto.columns, pd.MultiIndex): | |
| df_minuto.columns = df_minuto.columns.droplevel(1) | |
| precio_actual = float(df_minuto['Close'].dropna().iloc[-1]) | |
| except: | |
| precio_actual = 0.0 | |
| # 🧠 Obtener Datos para IA | |
| df = yf.download(ticker, period="150d", interval="1d", progress=False, auto_adjust=True) | |
| if isinstance(df.columns, pd.MultiIndex): | |
| df.columns = df.columns.droplevel(1) | |
| for col in ['Open', 'High', 'Low', 'Close', 'Volume']: | |
| df[col] = df[col].astype(float) | |
| if precio_actual == 0.0: | |
| precio_actual = float(df['Close'].dropna().iloc[-1]) | |
| # Calcular Indicadores Técnicos | |
| df['SMA_20'] = ta.trend.sma_indicator(df['Close'], window=20) | |
| df['SMA_50'] = ta.trend.sma_indicator(df['Close'], window=50) | |
| df['RSI_14'] = ta.momentum.rsi(df['Close'], window=14) | |
| df['MACD'] = ta.trend.macd(df['Close']) | |
| df['MACD_Signal'] = ta.trend.macd_signal(df['Close']) | |
| df['ATR_14'] = ta.volatility.average_true_range(df['High'], df['Low'], df['Close'], window=14) | |
| # ========================================================= | |
| # 🚨 INYECTAMOS EL SENTIMIENTO RECIBIDO DESDE N8N | |
| # ========================================================= | |
| df['Sentimiento'] = req.sentimiento | |
| df.dropna(inplace=True) | |
| # 🚨 Matriz final con las 12 VARIABLES 🚨 | |
| features = ['Open', 'High', 'Low', 'Close', 'Volume', 'SMA_20', 'SMA_50', 'RSI_14', 'MACD', 'MACD_Signal', 'ATR_14', 'Sentimiento'] | |
| X_live = df.tail(60)[features].values.astype(np.float32) | |
| X_scaled = escalador.transform(X_live) | |
| X_reshaped = np.reshape(X_scaled, (1, 60, len(features))) | |
| # Predicción | |
| prediccion = float(modelo.predict(X_reshaped, verbose=0)[0][0]) | |
| decision = "LONG (COMPRAR)" if prediccion > 0.5 else "SHORT (VENDER/ESPERAR)" | |
| confianza = round(prediccion * 100, 2) if prediccion > 0.5 else round((1 - prediccion) * 100, 2) | |
| return { | |
| "symbol": req.symbol, | |
| "ticker_ia": ticker, | |
| "precio_actual": precio_actual, | |
| "decision_ia": decision, | |
| "confianza": f"{confianza}%", | |
| "probabilidad_matematica": prediccion, | |
| "sentimiento_aplicado": req.sentimiento # Te lo devuelvo para que confirmes que llegó bien | |
| } |