Spaces:
Sleeping
Sleeping
| """ | |
| ╔══════════════════════════════════════════════════════════════╗ | |
| ║ 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"} | |
| 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 ── | |
| async def root(): | |
| return {"service": "Aurora Brain API", "version": "1.3.0", "docs": "/docs"} | |
| 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", | |
| ) | |
| 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(), | |
| ) | |
| 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 | |
| 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) | |