aurora-brain / app.py
redradios's picture
v2.3: klines frescas en cada predict (no mas datos estaticos)
06dbe48
"""
╔══════════════════════════════════════════════════════════════╗
║ 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)