CoolFace
Apppublic

Marchelo23/mempool

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
app.py388 linesDownload Raw Back to root
1"""2Bitcoin Mempool Fee Prediction API — Hugging Face Spaces Edition3================================================================4Self-contained FastAPI application with XGBoost + LightGBM ensemble.5No Redis dependency. Open access for public demo.6 7Models: XGBoost 2.1+ / LightGBM 4.5+8Port: 7860 (HF Spaces requirement)9"""10 11from fastapi import FastAPI, HTTPException, BackgroundTasks, Request12from fastapi.middleware.cors import CORSMiddleware13from fastapi.responses import JSONResponse14from contextlib import asynccontextmanager15from datetime import datetime, timezone16import logging17import os18import sys19from pathlib import Path20 21# Ensure app root is in path22sys.path.insert(0, str(Path(__file__).parent))23 24from src.ingestion import MempoolDataIngestion25from src.inference import FeeModelInference26 27# ---------------------------------------------------------------------------28# Logging29# ---------------------------------------------------------------------------30logging.basicConfig(31    level=logging.INFO,32    format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",33)34logger = logging.getLogger("mempool-api")35 36# ---------------------------------------------------------------------------37# Global state38# ---------------------------------------------------------------------------39ingestion: MempoolDataIngestion = None40inference: FeeModelInference = None41cached_prediction = None42cache_timestamp = None43CACHE_TTL = 30  # seconds — fast refresh for live demo44 45 46# ---------------------------------------------------------------------------47# Lifespan48# ---------------------------------------------------------------------------49@asynccontextmanager50async def lifespan(app: FastAPI):51    global ingestion, inference52 53    logger.info("⚡ Starting Bitcoin Mempool Fee Prediction API (HF Spaces)")54    ingestion = MempoolDataIngestion()55    inference = FeeModelInference()56    inference.load_all_models()57 58    model_info = inference.get_loaded_models_info()59    logger.info(f"✓ Models loaded: {model_info}")60 61    yield62 63    logger.info("🔌 Shutting down API")64 65 66# ---------------------------------------------------------------------------67# App68# ---------------------------------------------------------------------------69app = FastAPI(70    title="Bitcoin Mempool Fee Predictor",71    description=(72        "ML-powered fee prediction for Bitcoin block inclusion. "73        "Predicts the optimal fee rate (sats/vByte) using an XGBoost + LightGBM ensemble "74        "trained on real-time mempool data from mempool.space."75    ),76    version="3.0.0",77    lifespan=lifespan,78    docs_url="/docs",79    redoc_url="/redoc",80)81 82# CORS — wide open for HF Spaces public demo83app.add_middleware(84    CORSMiddleware,85    allow_origins=["*"],86    allow_credentials=False,87    allow_methods=["GET"],88    allow_headers=["*"],89    max_age=600,90)91 92 93# ============================================================================94# ENDPOINTS95# ============================================================================96 97@app.get("/", tags=["General"])98async def root():99    """Root endpoint — API overview"""100    return {101        "service": "Bitcoin Mempool Fee Predictor",102        "version": "3.0.0",103        "status": "operational",104        "models": "XGBoost 2.1 + LightGBM 4.5 Ensemble",105        "network": "Bitcoin Mainnet",106        "docs": "/docs",107        "endpoints": {108            "predict": "/fees/predict",109            "current": "/fees/current",110            "health": "/health",111            "models": "/models",112            "metadata": "/model-metadata",113            "mempool_blocks": "/mempool/blocks",114        },115    }116 117 118@app.get("/health", tags=["General"])119async def health_check():120    """Health check"""121    model_info = inference.get_loaded_models_info() if inference else {}122    return {123        "status": "healthy",124        "timestamp": datetime.now(timezone.utc).isoformat(),125        "models_loaded": model_info.get("total_models", 0),126        "xgb_horizons": model_info.get("xgb_models", []),127        "lgb_horizons": model_info.get("lgb_models", []),128        "version": "3.0.0",129    }130 131 132@app.get("/fees/predict", tags=["Fee Prediction"])133async def predict_fees(134    request: Request,135    background_tasks: BackgroundTasks,136    use_ensemble: bool = True,137):138    """139    🔮 Predict optimal fee rates for Bitcoin block inclusion.140 141    Returns ML predictions for 1-block, 3-block, and 6-block horizons142    with confidence intervals and a recommendation.143 144    **No API key needed** — this is a public demo.145    """146    global cached_prediction, cache_timestamp147 148    # Serve from cache if fresh149    if (150        cached_prediction is not None151        and cache_timestamp is not None152        and (datetime.now() - cache_timestamp).total_seconds() < CACHE_TTL153    ):154        return cached_prediction155 156    try:157        # Fetch live mempool snapshot158        snapshot = ingestion.fetch_full_snapshot()159        if snapshot is None:160            raise HTTPException(161                status_code=503,162                detail="Could not fetch mempool data from mempool.space",163            )164 165        # Load historical data for rolling features166        snapshots_df = ingestion.load_snapshots()167        import pandas as pd168 169        if snapshots_df is None or len(snapshots_df) < 10:170            snapshots_df = pd.DataFrame([snapshot])171        else:172            snapshots_df = pd.concat(173                [snapshots_df, pd.DataFrame([snapshot])],174                ignore_index=True,175            )176 177        # Make predictions178        response = inference.predict_from_snapshot(179            snapshots_df, use_ensemble=use_ensemble180        )181 182        # Cache183        cached_prediction = response184        cache_timestamp = datetime.now()185 186        # Save snapshot in background187        background_tasks.add_task(ingestion.save_snapshot, snapshot)188 189        return response190 191    except HTTPException:192        raise193    except Exception as e:194        logger.error(f"Prediction error: {e}", exc_info=True)195        raise HTTPException(status_code=500, detail="Prediction failed")196 197 198@app.get("/fees/current", tags=["Fee Prediction"])199async def get_current_fees():200    """201    📊 Current mempool fees from mempool.space (no ML — raw network data).202    """203    try:204        fees = ingestion.fetch_recommended_fees()205        mempool = ingestion.fetch_mempool_state()206 207        if fees is None:208            raise HTTPException(status_code=503, detail="Could not fetch fee data")209 210        return {211            "timestamp": datetime.now(timezone.utc).isoformat(),212            "fees": {213                "fastest": fees.get("fastestFee", 0),214                "half_hour": fees.get("halfHourFee", 0),215                "hour": fees.get("hourFee", 0),216                "economy": fees.get("economyFee", 0),217                "minimum": fees.get("minimumFee", 0),218            },219            "mempool": {220                "tx_count": mempool.get("count", 0) if mempool else 0,221                "vsize_mb": round(mempool.get("vsize", 0) / 1e6, 1)222                if mempool223                else 0,224                "total_fee_btc": round(mempool.get("total_fee", 0) / 1e8, 4)225                if mempool226                else 0,227            },228            "source": "mempool.space",229        }230 231    except HTTPException:232        raise233    except Exception as e:234        logger.error(f"Current fees error: {e}")235        raise HTTPException(status_code=500, detail="Error fetching current fees")236 237 238@app.get("/mempool/blocks", tags=["Mempool"])239async def get_mempool_blocks():240    """📦 Projected mempool blocks with fee ranges"""241    try:242        blocks = ingestion.fetch_mempool_blocks()243        if blocks is None:244            raise HTTPException(245                status_code=503, detail="Could not fetch mempool blocks"246            )247 248        return {249            "timestamp": datetime.now(timezone.utc).isoformat(),250            "projected_blocks": blocks,251            "n_blocks": len(blocks),252        }253    except HTTPException:254        raise255    except Exception as e:256        raise HTTPException(status_code=500, detail="Error fetching mempool blocks")257 258 259@app.get("/models", tags=["Models"])260async def list_models():261    """🧠 List loaded model information"""262    if inference:263        return inference.get_loaded_models_info()264    return {"error": "Models not loaded"}265 266 267@app.get("/model-metadata", tags=["Models"])268async def get_model_metadata():269    """270    📋 Comprehensive model metadata: version, training info, performance metrics.271    """272    try:273        import json274 275        metadata = {276            "model_version": "3.0.0",277            "api_version": "3.0.0",278            "framework": "XGBoost 2.1 + LightGBM 4.5 Ensemble",279            "horizons_supported": [1, 3, 6],280            "timestamp": datetime.now(timezone.utc).isoformat(),281            "deployment": "Hugging Face Spaces (Docker)",282            "models": {},283        }284 285        for horizon in [1, 3, 6]:286            model_info = {287                "horizon_blocks": horizon,288                "loaded": False,289                "xgboost": None,290                "lightgbm": None,291                "metrics": {},292            }293 294            # XGBoost model295            xgb_path = Path(f"models/production/xgb_fee_{horizon}block.json")296            if xgb_path.exists():297                model_info["xgboost"] = {298                    "loaded": True,299                    "file_size_kb": round(xgb_path.stat().st_size / 1024, 2),300                    "last_modified": datetime.fromtimestamp(301                        xgb_path.stat().st_mtime302                    ).isoformat(),303                }304                model_info["loaded"] = True305 306            # LightGBM model307            lgb_path = Path(f"models/production/lgbm_fee_{horizon}block.txt")308            if lgb_path.exists():309                model_info["lightgbm"] = {310                    "loaded": True,311                    "file_size_kb": round(lgb_path.stat().st_size / 1024, 2),312                    "last_modified": datetime.fromtimestamp(313                        lgb_path.stat().st_mtime314                    ).isoformat(),315                }316                model_info["loaded"] = True317 318            # Production meta metrics319            meta_path = Path(f"models/production/meta_fee_{horizon}block.json")320            if meta_path.exists():321                try:322                    with open(meta_path) as f:323                        meta = json.load(f)324                    model_info["metrics"] = {325                        "xgb": meta.get("xgb_metrics", {}),326                        "lgb": meta.get("lgb_metrics", {}),327                    }328                except Exception:329                    pass330 331            metadata["models"][f"{horizon}_block"] = model_info332 333        # System status334        loaded = inference.get_loaded_models_info() if inference else {}335        metadata["system_status"] = {336            "total_models_loaded": loaded.get("total_models", 0),337            "xgb_models": loaded.get("xgb_models", []),338            "lgb_models": loaded.get("lgb_models", []),339        }340 341        return metadata342 343    except Exception as e:344        logger.error(f"Metadata error: {e}")345        raise HTTPException(status_code=500, detail="Error fetching model metadata")346 347 348# ============================================================================349# ERROR HANDLERS350# ============================================================================351 352@app.exception_handler(HTTPException)353async def http_exception_handler(request, exc):354    return JSONResponse(355        status_code=exc.status_code,356        content={357            "error": exc.detail,358            "timestamp": datetime.now(timezone.utc).isoformat(),359        },360    )361 362 363@app.exception_handler(Exception)364async def general_exception_handler(request, exc):365    logger.error(f"Unhandled exception: {exc}", exc_info=True)366    return JSONResponse(367        status_code=500,368        content={369            "error": "Internal server error",370            "timestamp": datetime.now(timezone.utc).isoformat(),371        },372    )373 374 375# ============================================================================376# RUN377# ============================================================================378 379if __name__ == "__main__":380    import uvicorn381 382    uvicorn.run(383        "app:app",384        host="0.0.0.0",385        port=int(os.getenv("PORT", "7860")),386        log_level="info",387    )388