191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
"""
|
|
K-line data service
|
|
"""
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
from app.data_sources import DataSourceFactory
|
|
from app.utils.cache import CacheManager
|
|
from app.utils.logger import get_logger
|
|
from app.config import CacheConfig
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class KlineService:
|
|
"""K-line data service"""
|
|
|
|
def __init__(self):
|
|
self.cache = CacheManager()
|
|
self.cache_ttl = CacheConfig.KLINE_CACHE_TTL
|
|
|
|
def get_kline(
|
|
self,
|
|
market: str,
|
|
symbol: str,
|
|
timeframe: str,
|
|
limit: int = 300,
|
|
before_time: Optional[int] = None
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get K-line data
|
|
|
|
Args:
|
|
market: market type (Crypto, USStock, Forex, Futures)
|
|
symbol: trading pair/stock code
|
|
timeframe: time period
|
|
limit: number of data items
|
|
before_time: Get data before this time
|
|
|
|
Returns:
|
|
K-line data list
|
|
"""
|
|
# Build a cache key (historical data is not cached)
|
|
if not before_time:
|
|
cache_key = f"kline:{market}:{symbol}:{timeframe}:{limit}"
|
|
cached = self.cache.get(cache_key)
|
|
if cached:
|
|
# logger.info(f"Hit cache: {cache_key}")
|
|
return cached
|
|
|
|
# Get data
|
|
klines = DataSourceFactory.get_kline(
|
|
market=market,
|
|
symbol=symbol,
|
|
timeframe=timeframe,
|
|
limit=limit,
|
|
before_time=before_time
|
|
)
|
|
|
|
# Set cache (latest data only)
|
|
if klines and not before_time:
|
|
ttl = self.cache_ttl.get(timeframe, 300)
|
|
self.cache.set(cache_key, klines, ttl)
|
|
# logger.info(f"Cache settings: {cache_key}, TTL: {ttl}s")
|
|
|
|
return klines
|
|
|
|
def get_latest_price(self, market: str, symbol: str) -> Optional[Dict[str, Any]]:
|
|
"""Get the latest price (use 1-minute K-line, deprecated, it is recommended to use get_realtime_price)"""
|
|
klines = self.get_kline(market, symbol, '1m', 1)
|
|
if klines:
|
|
return klines[-1]
|
|
return None
|
|
|
|
def get_realtime_price(self, market: str, symbol: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|
"""
|
|
Get real-time prices (priority to use ticker API, downgrade to minute K-line)
|
|
|
|
Args:
|
|
market: market type (Crypto, USStock, Forex, Futures)
|
|
symbol: trading pair/stock code
|
|
force_refresh: whether to force refresh (skip cache)
|
|
|
|
Returns:
|
|
Real-time price data: {
|
|
'price': latest price,
|
|
'change': change amount,
|
|
'changePercent': increase or decrease,
|
|
'high': highest price,
|
|
'low': lowest price,
|
|
'open': opening price,
|
|
'previousClose': yesterday's closing price,
|
|
'source': data source ('ticker' or 'kline')
|
|
}
|
|
"""
|
|
# Build a cache key (short-term cache to avoid frequent requests)
|
|
cache_key = f"realtime_price:{market}:{symbol}"
|
|
|
|
# If it is not a forced refresh, try using caching
|
|
if not force_refresh:
|
|
cached = self.cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
result = {
|
|
'price': 0,
|
|
'change': 0,
|
|
'changePercent': 0,
|
|
'high': 0,
|
|
'low': 0,
|
|
'open': 0,
|
|
'previousClose': 0,
|
|
'source': 'unknown'
|
|
}
|
|
|
|
# First try to use the ticker API to get real-time prices
|
|
try:
|
|
ticker = DataSourceFactory.get_ticker(market, symbol)
|
|
if ticker and ticker.get('last', 0) > 0:
|
|
result = {
|
|
'price': ticker.get('last', 0),
|
|
'change': ticker.get('change', 0),
|
|
'changePercent': ticker.get('changePercent', 0),
|
|
'high': ticker.get('high', 0),
|
|
'low': ticker.get('low', 0),
|
|
'open': ticker.get('open', 0),
|
|
'previousClose': ticker.get('previousClose', 0),
|
|
'source': 'ticker'
|
|
}
|
|
# Cache for 30 seconds
|
|
self.cache.set(cache_key, result, 30)
|
|
return result
|
|
except Exception as e:
|
|
logger.debug(f"Ticker API failed for {market}:{symbol}, falling back to kline: {e}")
|
|
|
|
# Downgrade: Use 1 minute candlestick
|
|
try:
|
|
klines = self.get_kline(market, symbol, '1m', 2)
|
|
if klines and len(klines) > 0:
|
|
latest = klines[-1]
|
|
prev_close = klines[-2]['close'] if len(klines) > 1 else latest.get('open', 0)
|
|
current_price = latest.get('close', 0)
|
|
|
|
change = round(current_price - prev_close, 4) if prev_close else 0
|
|
change_pct = round(change / prev_close * 100, 2) if prev_close and prev_close > 0 else 0
|
|
|
|
result = {
|
|
'price': current_price,
|
|
'change': change,
|
|
'changePercent': change_pct,
|
|
'high': latest.get('high', 0),
|
|
'low': latest.get('low', 0),
|
|
'open': latest.get('open', 0),
|
|
'previousClose': prev_close,
|
|
'source': 'kline_1m'
|
|
}
|
|
# Cache for 30 seconds
|
|
self.cache.set(cache_key, result, 30)
|
|
return result
|
|
except Exception as e:
|
|
logger.debug(f"1m kline failed for {market}:{symbol}, trying daily: {e}")
|
|
|
|
# Last downgrade: using daily data (applies to non-trading hours)
|
|
try:
|
|
klines = self.get_kline(market, symbol, '1D', 2)
|
|
if klines and len(klines) > 0:
|
|
latest = klines[-1]
|
|
prev_close = klines[-2]['close'] if len(klines) > 1 else latest.get('open', 0)
|
|
current_price = latest.get('close', 0)
|
|
|
|
change = round(current_price - prev_close, 4) if prev_close else 0
|
|
change_pct = round(change / prev_close * 100, 2) if prev_close and prev_close > 0 else 0
|
|
|
|
result = {
|
|
'price': current_price,
|
|
'change': change,
|
|
'changePercent': change_pct,
|
|
'high': latest.get('high', 0),
|
|
'low': latest.get('low', 0),
|
|
'open': latest.get('open', 0),
|
|
'previousClose': prev_close,
|
|
'source': 'kline_1d'
|
|
}
|
|
# Daily data cache for 5 minutes
|
|
self.cache.set(cache_key, result, 300)
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"All price sources failed for {market}:{symbol}: {e}")
|
|
|
|
return result
|
|
|