2025-12-29 03:06:49 +08:00
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
K-line data service
|
2025-12-29 03:06:49 +08:00
|
|
|
"""
|
|
|
|
|
|
2026-04-09 14:30:51 +07:00
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
from app.config import CacheConfig
|
2025-12-29 03:06:49 +08:00
|
|
|
from app.data_sources import DataSourceFactory
|
|
|
|
|
from app.utils.cache import CacheManager
|
|
|
|
|
from app.utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KlineService:
|
2026-04-06 16:47:36 +07:00
|
|
|
"""K-line data service"""
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
def __init__(self):
|
|
|
|
|
self.cache = CacheManager()
|
|
|
|
|
self.cache_ttl = CacheConfig.KLINE_CACHE_TTL
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
def get_kline(
|
2026-04-09 14:30:51 +07:00
|
|
|
self, market: str, symbol: str, timeframe: str, limit: int = 300, before_time: Optional[int] = None
|
2025-12-29 03:06:49 +08:00
|
|
|
) -> List[Dict[str, Any]]:
|
|
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
Get K-line data
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
Args:
|
2026-04-06 16:47:36 +07:00
|
|
|
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
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
Returns:
|
2026-04-06 16:47:36 +07:00
|
|
|
K-line data list
|
2025-12-29 03:06:49 +08:00
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
# Build a cache key (historical data is not cached)
|
2025-12-29 03:06:49 +08:00
|
|
|
if not before_time:
|
|
|
|
|
cache_key = f"kline:{market}:{symbol}:{timeframe}:{limit}"
|
|
|
|
|
cached = self.cache.get(cache_key)
|
|
|
|
|
if cached:
|
2026-04-06 16:47:36 +07:00
|
|
|
# logger.info(f"Hit cache: {cache_key}")
|
2025-12-29 03:06:49 +08:00
|
|
|
return cached
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Get data
|
2025-12-29 03:06:49 +08:00
|
|
|
klines = DataSourceFactory.get_kline(
|
2026-04-09 14:30:51 +07:00
|
|
|
market=market, symbol=symbol, timeframe=timeframe, limit=limit, before_time=before_time
|
2025-12-29 03:06:49 +08:00
|
|
|
)
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Set cache (latest data only)
|
2025-12-29 03:06:49 +08:00
|
|
|
if klines and not before_time:
|
|
|
|
|
ttl = self.cache_ttl.get(timeframe, 300)
|
|
|
|
|
self.cache.set(cache_key, klines, ttl)
|
2026-04-06 16:47:36 +07:00
|
|
|
# logger.info(f"Cache settings: {cache_key}, TTL: {ttl}s")
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
return klines
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2025-12-29 03:06:49 +08:00
|
|
|
def get_latest_price(self, market: str, symbol: str) -> Optional[Dict[str, Any]]:
|
2026-04-06 16:47:36 +07:00
|
|
|
"""Get the latest price (use 1-minute K-line, deprecated, it is recommended to use get_realtime_price)"""
|
2026-04-09 14:30:51 +07:00
|
|
|
klines = self.get_kline(market, symbol, "1m", 1)
|
2025-12-29 03:06:49 +08:00
|
|
|
if klines:
|
|
|
|
|
return klines[-1]
|
|
|
|
|
return None
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
def get_realtime_price(self, market: str, symbol: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
Get real-time prices (priority to use ticker API, downgrade to minute K-line)
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
Args:
|
2026-04-06 16:47:36 +07:00
|
|
|
market: market type (Crypto, USStock, Forex, Futures)
|
|
|
|
|
symbol: trading pair/stock code
|
|
|
|
|
force_refresh: whether to force refresh (skip cache)
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
Returns:
|
2026-04-06 16:47:36 +07:00
|
|
|
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')
|
2026-01-13 00:21:33 +08:00
|
|
|
}
|
|
|
|
|
"""
|
2026-04-06 16:47:36 +07:00
|
|
|
# Build a cache key (short-term cache to avoid frequent requests)
|
2026-01-13 00:21:33 +08:00
|
|
|
cache_key = f"realtime_price:{market}:{symbol}"
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# If it is not a forced refresh, try using caching
|
2026-01-13 00:21:33 +08:00
|
|
|
if not force_refresh:
|
|
|
|
|
cached = self.cache.get(cache_key)
|
|
|
|
|
if cached:
|
|
|
|
|
return cached
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
result = {
|
2026-04-09 14:30:51 +07:00
|
|
|
"price": 0,
|
|
|
|
|
"change": 0,
|
|
|
|
|
"changePercent": 0,
|
|
|
|
|
"high": 0,
|
|
|
|
|
"low": 0,
|
|
|
|
|
"open": 0,
|
|
|
|
|
"previousClose": 0,
|
|
|
|
|
"source": "unknown",
|
2026-01-13 00:21:33 +08:00
|
|
|
}
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# First try to use the ticker API to get real-time prices
|
2026-01-13 00:21:33 +08:00
|
|
|
try:
|
|
|
|
|
ticker = DataSourceFactory.get_ticker(market, symbol)
|
2026-04-09 14:30:51 +07:00
|
|
|
if ticker and ticker.get("last", 0) > 0:
|
2026-01-13 00:21:33 +08:00
|
|
|
result = {
|
2026-04-09 14:30:51 +07:00
|
|
|
"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",
|
2026-01-13 00:21:33 +08:00
|
|
|
}
|
2026-04-06 16:47:36 +07:00
|
|
|
# Cache for 30 seconds
|
2026-01-13 00:21:33 +08:00
|
|
|
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}")
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Downgrade: Use 1 minute candlestick
|
2026-01-13 00:21:33 +08:00
|
|
|
try:
|
2026-04-09 14:30:51 +07:00
|
|
|
klines = self.get_kline(market, symbol, "1m", 2)
|
2026-01-13 00:21:33 +08:00
|
|
|
if klines and len(klines) > 0:
|
|
|
|
|
latest = klines[-1]
|
2026-04-09 14:30:51 +07:00
|
|
|
prev_close = klines[-2]["close"] if len(klines) > 1 else latest.get("open", 0)
|
|
|
|
|
current_price = latest.get("close", 0)
|
|
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
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
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
result = {
|
2026-04-09 14:30:51 +07:00
|
|
|
"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",
|
2026-01-13 00:21:33 +08:00
|
|
|
}
|
2026-04-06 16:47:36 +07:00
|
|
|
# Cache for 30 seconds
|
2026-01-13 00:21:33 +08:00
|
|
|
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}")
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-04-06 16:47:36 +07:00
|
|
|
# Last downgrade: using daily data (applies to non-trading hours)
|
2026-01-13 00:21:33 +08:00
|
|
|
try:
|
2026-04-09 14:30:51 +07:00
|
|
|
klines = self.get_kline(market, symbol, "1D", 2)
|
2026-01-13 00:21:33 +08:00
|
|
|
if klines and len(klines) > 0:
|
|
|
|
|
latest = klines[-1]
|
2026-04-09 14:30:51 +07:00
|
|
|
prev_close = klines[-2]["close"] if len(klines) > 1 else latest.get("open", 0)
|
|
|
|
|
current_price = latest.get("close", 0)
|
|
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
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
|
2026-04-09 14:30:51 +07:00
|
|
|
|
2026-01-13 00:21:33 +08:00
|
|
|
result = {
|
2026-04-09 14:30:51 +07:00
|
|
|
"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",
|
2026-01-13 00:21:33 +08:00
|
|
|
}
|
2026-04-06 16:47:36 +07:00
|
|
|
# Daily data cache for 5 minutes
|
2026-01-13 00:21:33 +08:00
|
|
|
self.cache.set(cache_key, result, 300)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"All price sources failed for {market}:{symbol}: {e}")
|
2025-12-29 03:06:49 +08:00
|
|
|
|
2026-04-09 14:30:51 +07:00
|
|
|
return result
|