""" ╔══════════════════════════════════════════════════════════════╗ ║ AURORA BRAIN — API Server v1.3 (HuggingFace Space) ║ ║ ║ ║ FastAPI server que expone las predicciones del Brain. ║ ║ Endpoints: ║ ║ GET /health — Estado del servicio ║ ║ POST /regime — Régimen actual del mercado ║ ║ POST /predict — Predicción completa ║ ║ GET /models — Info de modelos cargados ║ ╚══════════════════════════════════════════════════════════════╝ """ import os import json import logging import pickle from datetime import datetime, timezone import numpy as np import pandas as pd from fastapi import FastAPI, HTTPException from pydantic import BaseModel logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger("AuroraBrain.API") app = FastAPI(title="Aurora Brain API", version="1.3.0") MODELS_DIR = os.path.join(os.path.dirname(__file__), "models") DATA_DIR = os.path.join(os.path.dirname(__file__), "data") # Modelos en memoria _regime_model = None _regime_metadata = None _signal_model = None _signal_metadata = None _startup_time = None REGIME_NAMES = {0: "TRENDING", 1: "RANGING", 2: "VOLATILE"} @app.on_event("startup") async def load_models(): global _regime_model, _regime_metadata, _signal_model, _signal_metadata, _startup_time _startup_time = datetime.now(timezone.utc) # Regime model p = os.path.join(MODELS_DIR, "regime_model.pkl") if os.path.exists(p): with open(p, "rb") as f: _regime_model = pickle.load(f) logger.info("Regime model loaded") p = os.path.join(MODELS_DIR, "regime_metadata.json") if os.path.exists(p): with open(p, "r") as f: _regime_metadata = json.load(f) logger.info("Regime metadata loaded (acc: %.1f%%)", _regime_metadata.get("accuracy", 0)) # Signal model p = os.path.join(MODELS_DIR, "signal_model.pkl") if os.path.exists(p): with open(p, "rb") as f: _signal_model = pickle.load(f) logger.info("Signal model loaded") p = os.path.join(MODELS_DIR, "signal_metadata.json") if os.path.exists(p): with open(p, "r") as f: _signal_metadata = json.load(f) bt = _signal_metadata.get("backtest", {}) logger.info("Signal metadata loaded (acc: %.1f%%, backtest WR: %.1f%%)", _signal_metadata.get("accuracy", 0), bt.get("win_rate", 0)) logger.info("Aurora Brain API ready") # ── Schemas ── class RegimeRequest(BaseModel): symbol: str = "BTCUSDT" timeframe: str = "4h" class PredictRequest(BaseModel): symbol: str = "BTCUSDT" timeframe: str = "4h" class RegimeResponse(BaseModel): regime: str regime_id: int confidence: float probabilities: dict model_accuracy: float timestamp: str class HealthResponse(BaseModel): status: str uptime_seconds: float models_loaded: dict version: str # ── Endpoints ── @app.get("/") async def root(): return {"service": "Aurora Brain API", "version": "1.3.0", "docs": "/docs"} @app.get("/health", response_model=HealthResponse) async def health(): uptime = (datetime.now(timezone.utc) - _startup_time).total_seconds() if _startup_time else 0 return HealthResponse( status="ok", uptime_seconds=round(uptime, 0), models_loaded={ "regime_detector": _regime_model is not None, "regime_accuracy": _regime_metadata.get("accuracy", 0) if _regime_metadata else 0, "signal_model": _signal_model is not None, "signal_accuracy": _signal_metadata.get("accuracy", 0) if _signal_metadata else 0, "signal_backtest_wr": _signal_metadata.get("backtest", {}).get("win_rate", 0) if _signal_metadata else 0, }, version="1.3.0", ) @app.post("/regime", response_model=RegimeResponse) async def get_regime(req: RegimeRequest): if _regime_model is None or _regime_metadata is None: raise HTTPException(status_code=503, detail="Regime model not loaded") # Descargar klines frescas y generar features en tiempo real try: import download_data import feature_engine df_fresh = download_data.download_klines(req.symbol, req.timeframe, days=120) if not df_fresh.empty and len(df_fresh) > 220: tmp_path = os.path.join(DATA_DIR, f"klines_{req.symbol}_{req.timeframe}.parquet") df_fresh.to_parquet(tmp_path) df = feature_engine.generate_features(req.symbol, req.timeframe) else: # Fallback a datos estaticos features_path = os.path.join(DATA_DIR, f"features_{req.symbol}_{req.timeframe}.parquet") if not os.path.exists(features_path): raise HTTPException(status_code=404, detail=f"No features for {req.symbol}") df = pd.read_parquet(features_path) except HTTPException: raise except Exception as e: logger.warning("Fresh klines failed, using static: %s", str(e)[:100]) features_path = os.path.join(DATA_DIR, f"features_{req.symbol}_{req.timeframe}.parquet") if not os.path.exists(features_path): raise HTTPException(status_code=404, detail=f"No features for {req.symbol}") df = pd.read_parquet(features_path) feature_cols = _regime_metadata["feature_cols"] available = [c for c in feature_cols if c in df.columns] missing = [c for c in feature_cols if c not in df.columns] last_row = df[available].iloc[-1:].fillna(df[available].median()) for col in missing: last_row[col] = 0 last_row = last_row[feature_cols] proba = _regime_model.predict_proba(last_row)[0] regime_id = int(np.argmax(proba)) return RegimeResponse( regime=REGIME_NAMES[regime_id], regime_id=regime_id, confidence=round(float(proba[regime_id]), 4), probabilities={REGIME_NAMES[i]: round(float(p), 4) for i, p in enumerate(proba)}, model_accuracy=_regime_metadata.get("accuracy", 0), timestamp=datetime.now(timezone.utc).isoformat(), ) @app.post("/predict") async def predict(req: PredictRequest): """Prediccion completa: regimen + signal.""" # Regime regime = None try: regime = await get_regime(RegimeRequest(symbol=req.symbol, timeframe=req.timeframe)) except HTTPException: pass # Signal signal_result = None if _signal_model and _signal_metadata: try: # Descargar klines frescas y generar features en tiempo real import download_data import feature_engine df_fresh = download_data.download_klines(req.symbol, req.timeframe, days=120) if not df_fresh.empty and len(df_fresh) > 220: # Guardar temporalmente para que feature_engine las encuentre tmp_path = os.path.join(DATA_DIR, f"klines_{req.symbol}_{req.timeframe}.parquet") df_fresh.to_parquet(tmp_path) df_feat = feature_engine.generate_features(req.symbol, req.timeframe) if not df_feat.empty: import model_signals signal_result = model_signals.predict_signal(df_feat, req.symbol) except Exception as e: logger.warning("Signal prediction error: %s", str(e)[:150]) # Fallback: usar features estáticas si las hay features_path = os.path.join(DATA_DIR, f"features_{req.symbol}_{req.timeframe}.parquet") if os.path.exists(features_path): try: import model_signals df_feat = pd.read_parquet(features_path) signal_result = model_signals.predict_signal(df_feat, req.symbol) except Exception: pass result = { "symbol": req.symbol, "timeframe": req.timeframe, "timestamp": datetime.now(timezone.utc).isoformat(), "regime": regime.model_dump() if regime else None, "signal": signal_result, "recommendation": "HOLD", } # Recommendation logic # Signal takes priority if high confidence if signal_result and isinstance(signal_result, dict): sig = signal_result.get("signal") conf = signal_result.get("confidence", 0) if sig == "BUY" and conf > 0.7: result["recommendation"] = "BUY" result["action"] = f"BUY signal ({conf:.0%} conf, backtest WR: {signal_result.get('backtest_win_rate', 0):.0f}%)" elif sig == "SELL" and conf > 0.7: result["recommendation"] = "SELL" result["action"] = f"SELL signal ({conf:.0%} conf)" # Regime overrides signal in extreme cases if regime: if regime.regime == "VOLATILE" and regime.confidence > 0.7: result["recommendation"] = "SHIELD_MAX" result["action"] = "VOLATILE market - Guardian Shield max, NO comprar" elif regime.regime == "TRENDING" and regime.confidence > 0.7 and result["recommendation"] == "HOLD": result["recommendation"] = "SMC_ACTIVE" result["action"] = "TRENDING market - SMC strategies enabled" elif regime.regime == "RANGING" and regime.confidence > 0.7 and result["recommendation"] == "HOLD": result["recommendation"] = "GRID_SUGGEST" result["action"] = "RANGING market - grid trading suggested" return result @app.get("/models") async def models_info(): info = {"regime_detector": None, "signal_model": None} if _regime_metadata: info["regime_detector"] = { "trained_at": _regime_metadata.get("trained_at"), "accuracy": _regime_metadata.get("accuracy"), "cv_accuracy": _regime_metadata.get("cv_accuracy"), "n_features": _regime_metadata.get("n_features"), "top_features": _regime_metadata.get("top_features", [])[:10], } if _signal_metadata: info["signal_model"] = { "trained_at": _signal_metadata.get("trained_at"), "accuracy": _signal_metadata.get("accuracy"), "cv_accuracy": _signal_metadata.get("cv_accuracy"), "n_features": _signal_metadata.get("n_features"), "backtest": _signal_metadata.get("backtest"), "top_features": _signal_metadata.get("top_features", [])[:10], } return info if __name__ == "__main__": import uvicorn port = int(os.environ.get("PORT", 7860)) uvicorn.run(app, host="0.0.0.0", port=port)