|
|
|
|
|
"""
|
|
|
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
|
|
|
|
|
|
|
|
|
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 symbol.endswith("USDT"):
|
|
|
return symbol
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
binance_symbol = self._normalize_symbol(symbol)
|
|
|
|
|
|
|
|
|
binance_interval = self.timeframe_map.get(timeframe, "1h")
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
ohlcv_data = []
|
|
|
for kline in klines:
|
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
|
|
|
|
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
|
|
|
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()
|
|
|
|
|
|
|
|
|
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)}"
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
binance_client = BinanceClient()
|
|
|
|
|
|
|
|
|
__all__ = ["BinanceClient", "binance_client"]
|
|
|
|