#!/usr/bin/env python3 """ Binance Public API Client - REAL DATA ONLY Fetches real OHLCV historical data from Binance NO MOCK DATA - All data from live Binance API """ import httpx import logging from typing import Dict, Any, List, Optional from datetime import datetime from fastapi import HTTPException logger = logging.getLogger(__name__) class BinanceClient: """ Real Binance Public API Client Primary source for real historical OHLCV candlestick data """ def __init__(self): self.base_url = "https://api.binance.com/api/v3" self.timeout = 15.0 # Timeframe mapping self.timeframe_map = { "1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m", "1h": "1h", "4h": "4h", "1d": "1d", "1w": "1w" } def _normalize_symbol(self, symbol: str) -> str: """Normalize symbol to Binance format (e.g., BTC -> BTCUSDT)""" symbol = symbol.upper().strip() # If already has USDT suffix, return as is if symbol.endswith("USDT"): return symbol # Add USDT suffix return f"{symbol}USDT" async def get_ohlcv( self, symbol: str, timeframe: str = "1h", limit: int = 1000 ) -> List[Dict[str, Any]]: """ Fetch REAL OHLCV candlestick data from Binance Args: symbol: Cryptocurrency symbol (e.g., "BTC", "ETH", "BTCUSDT") timeframe: Time interval (1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w) limit: Maximum number of candles (max 1000) Returns: List of real OHLCV candles """ try: # Normalize symbol binance_symbol = self._normalize_symbol(symbol) # Map timeframe binance_interval = self.timeframe_map.get(timeframe, "1h") # Limit to max 1000 limit = min(limit, 1000) async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.base_url}/klines", params={ "symbol": binance_symbol, "interval": binance_interval, "limit": limit } ) response.raise_for_status() klines = response.json() # Transform Binance format to standard OHLCV format ohlcv_data = [] for kline in klines: # Binance kline format: # [timestamp, open, high, low, close, volume, ...] timestamp = int(kline[0]) open_price = float(kline[1]) high_price = float(kline[2]) low_price = float(kline[3]) close_price = float(kline[4]) volume = float(kline[5]) # Filter out invalid candles if open_price > 0 and close_price > 0: ohlcv_data.append({ "timestamp": timestamp, "open": open_price, "high": high_price, "low": low_price, "close": close_price, "volume": volume }) logger.info( f"✅ Binance: Fetched {len(ohlcv_data)} real candles " f"for {binance_symbol} ({timeframe})" ) return ohlcv_data except httpx.HTTPStatusError as e: if e.response.status_code == 400: logger.error(f"❌ Binance: Invalid symbol or parameters: {symbol}") raise HTTPException( status_code=400, detail=f"Invalid symbol or parameters: {symbol}" ) elif e.response.status_code == 404: logger.error(f"❌ Binance: Symbol not found: {binance_symbol}") raise HTTPException( status_code=404, detail=f"Symbol not found on Binance: {symbol}" ) elif e.response.status_code == 451: logger.warning( f"⚠️ Binance: HTTP 451 - Access restricted (geo-blocking or legal restrictions) for {binance_symbol}. " f"Consider using alternative data sources or VPN." ) raise HTTPException( status_code=451, detail=f"Binance API access restricted for your region. Please use alternative data sources (CoinGecko, CoinMarketCap)." ) else: logger.error(f"❌ Binance API HTTP error: {e}") raise HTTPException( status_code=503, detail=f"Binance API temporarily unavailable: {str(e)}" ) except httpx.HTTPError as e: logger.error(f"❌ Binance API HTTP error: {e}") raise HTTPException( status_code=503, detail=f"Binance API temporarily unavailable: {str(e)}" ) except Exception as e: logger.error(f"❌ Binance API failed: {e}") raise HTTPException( status_code=503, detail=f"Failed to fetch real OHLCV data from Binance: {str(e)}" ) async def get_ticker(self, symbol: str) -> Dict[str, Any]: """ Fetch REAL current ticker price Args: symbol: Cryptocurrency symbol (e.g., "BTC", "ETH", "BTCUSDT") Returns: Real ticker data with current price """ try: binance_symbol = self._normalize_symbol(symbol) async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.base_url}/ticker/price", params={"symbol": binance_symbol} ) response.raise_for_status() data = response.json() return { "symbol": binance_symbol, "lastPrice": data.get("price", "0"), "price": float(data.get("price", 0)) } except httpx.HTTPStatusError as e: if e.response.status_code == 400: return None # Symbol not found raise HTTPException( status_code=503, detail=f"Failed to fetch ticker from Binance: {str(e)}" ) except Exception as e: logger.error(f"❌ Binance ticker failed: {e}") return None async def get_24h_ticker(self, symbol: str) -> Dict[str, Any]: """ Fetch REAL 24-hour ticker price change statistics Args: symbol: Cryptocurrency symbol (e.g., "BTC", "ETH") Returns: Real 24-hour ticker data """ try: binance_symbol = self._normalize_symbol(symbol) async with httpx.AsyncClient(timeout=self.timeout) as client: response = await client.get( f"{self.base_url}/ticker/24hr", params={"symbol": binance_symbol} ) response.raise_for_status() data = response.json() # Transform to standard format ticker = { "symbol": symbol.upper().replace("USDT", ""), "price": float(data.get("lastPrice", 0)), "change24h": float(data.get("priceChange", 0)), "changePercent24h": float(data.get("priceChangePercent", 0)), "volume24h": float(data.get("volume", 0)), "high24h": float(data.get("highPrice", 0)), "low24h": float(data.get("lowPrice", 0)), "source": "binance", "timestamp": int(datetime.utcnow().timestamp() * 1000) } logger.info(f"✅ Binance: Fetched real 24h ticker for {binance_symbol}") return ticker except httpx.HTTPStatusError as e: if e.response.status_code == 451: logger.warning( f"⚠️ Binance: HTTP 451 - Access restricted (geo-blocking or legal restrictions). " f"Consider using alternative data sources." ) raise HTTPException( status_code=451, detail=f"Binance API access restricted for your region. Please use alternative data sources (CoinGecko, CoinMarketCap)." ) logger.error(f"❌ Binance ticker error: {e}") raise HTTPException( status_code=503, detail=f"Failed to fetch ticker from Binance: {str(e)}" ) except Exception as e: logger.error(f"❌ Binance ticker failed: {e}") raise HTTPException( status_code=503, detail=f"Failed to fetch real ticker data: {str(e)}" ) # Global instance binance_client = BinanceClient() __all__ = ["BinanceClient", "binance_client"]