Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
raw
history blame
8.36 kB
"""
OHLCV Service with Multi-Provider Fallback
Automatically switches between Binance, CoinGecko, and other providers
"""
import logging
from typing import Dict, List, Any, Optional
from fastapi import HTTPException
from .api_fallback_manager import get_fallback_manager
logger = logging.getLogger(__name__)
class OHLCVService:
"""Service for fetching OHLCV data with automatic fallback"""
def __init__(self):
self.manager = get_fallback_manager("OHLCV")
self._setup_providers()
def _setup_providers(self):
"""Setup OHLCV providers in priority order"""
# Priority 1: Binance (fastest, most reliable - but may have regional restrictions)
self.manager.add_provider(
name="Binance",
priority=1,
fetch_function=self._fetch_binance,
cooldown_seconds=180,
max_failures=3
)
# Priority 2: CoinGecko (reliable alternative, no geo-restrictions)
self.manager.add_provider(
name="CoinGecko",
priority=2,
fetch_function=self._fetch_coingecko,
cooldown_seconds=60,
max_failures=3
)
# Priority 3: HuggingFace Space (fallback)
self.manager.add_provider(
name="HuggingFace",
priority=3,
fetch_function=self._fetch_huggingface,
cooldown_seconds=300,
max_failures=5
)
# Priority 4: Mock/Demo data (always available)
self.manager.add_provider(
name="Demo",
priority=999,
fetch_function=self._fetch_demo,
cooldown_seconds=0,
max_failures=999 # Never fails
)
logger.info("✅ OHLCV Service initialized with 4 providers (Binance, CoinGecko, HuggingFace, Demo)")
async def _fetch_binance(self, symbol: str, timeframe: str, limit: int = 100) -> Dict:
"""Fetch from Binance API"""
try:
from backend.services.binance_client import BinanceClient
client = BinanceClient()
candles = await client.get_ohlcv(symbol, timeframe=timeframe, limit=limit)
return {
"symbol": symbol.upper(),
"timeframe": timeframe,
"interval": timeframe,
"limit": limit,
"count": len(candles),
"ohlcv": candles,
"source": "binance"
}
except HTTPException as e:
if e.status_code == 451:
logger.warning(f"⚠️ Binance access restricted (HTTP 451). Falling back to CoinGecko.")
else:
logger.error(f"Binance fetch failed: {e.detail}")
raise
except Exception as e:
logger.error(f"Binance fetch failed: {e}")
raise
async def _fetch_coingecko(self, symbol: str, timeframe: str, limit: int = 100) -> Dict:
"""Fetch from CoinGecko API"""
try:
from backend.services.coingecko_client import CoinGeckoClient
client = CoinGeckoClient()
# CoinGecko uses days, not limit
days = self._timeframe_to_days(timeframe, limit)
data = await client.get_ohlcv(symbol, days=days)
return {
"symbol": symbol.upper(),
"timeframe": timeframe,
"interval": timeframe,
"limit": limit,
"count": len(data.get("prices", [])),
"ohlcv": self._format_coingecko_data(data),
"source": "coingecko"
}
except Exception as e:
logger.error(f"CoinGecko fetch failed: {e}")
raise
def _timeframe_to_days(self, timeframe: str, limit: int) -> int:
"""Convert timeframe and limit to days for CoinGecko"""
# Map timeframes to approximate days
timeframe_hours = {
"1m": 1/60, "5m": 5/60, "15m": 15/60, "30m": 0.5,
"1h": 1, "4h": 4, "1d": 24, "1w": 168
}
hours = timeframe_hours.get(timeframe, 1)
days = max(1, int((hours * limit) / 24))
return min(days, 365) # CoinGecko max 365 days
def _format_coingecko_data(self, data: Dict) -> List[Dict]:
"""Format CoinGecko data to standard OHLCV format"""
candles = []
prices = data.get("prices", [])
for price_point in prices:
timestamp, price = price_point
candles.append({
"timestamp": int(timestamp),
"open": price,
"high": price * 1.01, # Approximate
"low": price * 0.99, # Approximate
"close": price,
"volume": 0 # CoinGecko doesn't provide volume in this endpoint
})
return candles
async def _fetch_huggingface(self, symbol: str, timeframe: str, limit: int = 100) -> Dict:
"""Fetch from HuggingFace Space"""
import httpx
import os
base_url = os.getenv("HF_SPACE_BASE_URL", "https://really-amin-datasourceforcryptocurrency.hf.space")
token = os.getenv("HF_API_TOKEN", "").strip()
headers = {"Authorization": f"Bearer {token}"} if token else {}
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/api/ohlcv/{symbol}",
params={"interval": timeframe, "limit": limit},
headers=headers,
timeout=15.0
)
response.raise_for_status()
return response.json()
async def _fetch_demo(self, symbol: str, timeframe: str, limit: int = 100) -> Dict:
"""Fetch demo/fallback data"""
import time
import random
# Generate realistic demo candles
base_price = 50000 if symbol.upper() == "BTC" else 3000
candles = []
for i in range(limit):
timestamp = int(time.time()) - (i * 3600) # 1 hour intervals
open_price = base_price + random.uniform(-1000, 1000)
close_price = open_price + random.uniform(-500, 500)
high_price = max(open_price, close_price) + random.uniform(0, 300)
low_price = min(open_price, close_price) - random.uniform(0, 300)
volume = random.uniform(1000, 10000)
candles.append({
"t": timestamp * 1000,
"o": round(open_price, 2),
"h": round(high_price, 2),
"l": round(low_price, 2),
"c": round(close_price, 2),
"v": round(volume, 2)
})
return {
"symbol": symbol.upper(),
"timeframe": timeframe,
"interval": timeframe,
"limit": limit,
"count": len(candles),
"ohlcv": candles[::-1], # Reverse to oldest first
"source": "demo",
"warning": "Using demo data - live data unavailable"
}
async def get_ohlcv(
self,
symbol: str,
timeframe: str = "1h",
limit: int = 100
) -> Dict[str, Any]:
"""
Get OHLCV data with automatic fallback
Args:
symbol: Trading symbol (e.g., "BTC", "ETH")
timeframe: Timeframe (e.g., "1h", "4h", "1d")
limit: Number of candles
Returns:
Dict with OHLCV data and metadata
"""
result = await self.manager.fetch_with_fallback(
symbol=symbol,
timeframe=timeframe,
limit=limit
)
if not result["success"]:
logger.error(f"All OHLCV providers failed for {symbol}")
return result
def get_status(self) -> Dict[str, Any]:
"""Get status of all OHLCV providers"""
return self.manager.get_status()
# Global singleton
_ohlcv_service: Optional[OHLCVService] = None
def get_ohlcv_service() -> OHLCVService:
"""Get or create the OHLCV service singleton"""
global _ohlcv_service
if _ohlcv_service is None:
_ohlcv_service = OHLCVService()
return _ohlcv_service