import os import sys sys.stdout.reconfigure(encoding='utf-8') from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel import ccxt from typing import Optional import modal from modal.functions import FunctionCall app = FastAPI(title="Binance Data Dashboard API") # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Binance client try: binance = ccxt.binance() except Exception as e: print(f"Warning: Could not init Binance client: {e}") binance = None class DownloadRequest(BaseModel): symbol: str interval: str data_type: str start_date: str end_date: str threshold: Optional[float] = 1_000_000 agg_mode: Optional[str] = "Standard (Klines + Liq)" @app.get("/") def health(): return {"status": "ok", "version": "2026-03-11-v2", "message": "Binance Data Dashboard API on Hugging Face"} @app.get("/api/symbols") def get_symbols(): try: markets = binance.load_markets() return {"symbols": sorted(list(markets.keys()))} except: fallback = ["BTC/USDT", "ETH/USDT", "BNB/USDT", "SOL/USDT"] return {"symbols": sorted(fallback)} @app.post("/api/download") def download_data(request: DownloadRequest): print(f"[CLOUD] New Task: {request.data_type} for {request.symbol}") hf_repo = os.environ.get("HF_REPO", "Vycka12/Base") hf_token = os.environ.get("HF_TOKEN") mapping = { 'Klines (OHLCV)': ('fetch_klines_cloud', (request.symbol, request.interval, request.start_date, request.end_date, hf_repo, hf_token)), 'Liquidations': ('fetch_liquidations_cloud', (request.symbol, request.start_date, request.end_date, hf_repo, hf_token)), 'AggTrades': ('fetch_aggtrades_cloud', (request.symbol, request.start_date, request.end_date, hf_repo, hf_token)), 'Raw Trades (Tick-Level)': ('fetch_raw_trades_cloud', (request.symbol, request.start_date, request.end_date, hf_repo, hf_token)), 'Dollar Bars (ML Ready)': ('fetch_dollar_bars_cloud', (request.symbol, request.start_date, request.end_date, request.threshold, hf_repo, hf_token)), 'Volume Bars (ML Ready)': ('fetch_volume_bars_cloud', (request.symbol, request.start_date, request.end_date, request.threshold, hf_repo, hf_token)), 'VPIN (Flow Toxicity)': ('fetch_vpin_cloud', (request.symbol, request.start_date, request.end_date, int(request.threshold or 50), hf_repo, hf_token)), 'CDF Table (Flow Toxicity)': ('fetch_flow_toxicity_cloud', (request.symbol, request.start_date, request.end_date, int(request.threshold or 50), 50, hf_repo, hf_token)) } if request.data_type not in mapping: raise HTTPException(status_code=400, detail="Unknown data type.") fn_name, args = mapping[request.data_type] try: # SPAWN instead of REMOTE for async execution fn = modal.Function.from_name("snowsliper", "binance-data-dashboard", fn_name) call = fn.spawn(*args) return { "success": True, "job_id": call.object_id, "status": "pending", "message": "Task started in cloud background." } except Exception as e: print(f"SPAWN ERROR: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/api/status/{job_id}") def check_status(job_id: str): try: call = FunctionCall.from_id(job_id) try: # Attempt to get result without long poll result = call.get(timeout=0) return { "status": "completed", "result": result } except TimeoutError: # If not ready, it's still running return {"status": "running"} except Exception as e: if "not finished" in str(e).lower(): return {"status": "running"} raise e except Exception as e: print(f"STATUS ERR: {e}") return {"status": "error", "message": str(e)}