Files
DinQuant/backend_api_python/app/services/kline.py
T
TIANHE 714dd47c86 feat: add real-time price fetching and fix data delays
- Add get_ticker() method for real-time quotes across all markets

- Add get_realtime_price() service with ticker/kline fallback chain

- Fix yfinance end date issue for US stocks and futures

- Fix forex timezone parsing for Tiingo UTC timestamps

- Add retry mechanism with exponential backoff for Tiingo API

- Add API rate limiting for portfolio (3 concurrent, 0.3s interval)

- Add force refresh option to bypass price cache on manual refresh
2026-01-13 00:21:33 +08:00

191 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
K线数据服务
"""
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线数据服务"""
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]]:
"""
获取K线数据
Args:
market: 市场类型 (Crypto, USStock, AShare, HShare, Forex, Futures)
symbol: 交易对/股票代码
timeframe: 时间周期
limit: 数据条数
before_time: 获取此时间之前的数据
Returns:
K线数据列表
"""
# 构建缓存键(历史数据不缓存)
if not before_time:
cache_key = f"kline:{market}:{symbol}:{timeframe}:{limit}"
cached = self.cache.get(cache_key)
if cached:
# logger.info(f"命中缓存: {cache_key}")
return cached
# 获取数据
klines = DataSourceFactory.get_kline(
market=market,
symbol=symbol,
timeframe=timeframe,
limit=limit,
before_time=before_time
)
# 设置缓存(仅最新数据)
if klines and not before_time:
ttl = self.cache_ttl.get(timeframe, 300)
self.cache.set(cache_key, klines, ttl)
# logger.info(f"缓存设置: {cache_key}, TTL: {ttl}s")
return klines
def get_latest_price(self, market: str, symbol: str) -> Optional[Dict[str, Any]]:
"""获取最新价格(使用1分钟K线,已弃用,建议使用 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]:
"""
获取实时价格(优先使用 ticker API,降级使用分钟 K 线)
Args:
market: 市场类型 (Crypto, USStock, AShare, HShare, Forex, Futures)
symbol: 交易对/股票代码
force_refresh: 是否强制刷新(跳过缓存)
Returns:
实时价格数据: {
'price': 最新价格,
'change': 涨跌额,
'changePercent': 涨跌幅,
'high': 最高价,
'low': 最低价,
'open': 开盘价,
'previousClose': 昨收价,
'source': 数据来源 ('ticker' 或 'kline')
}
"""
# 构建缓存键(短时间缓存,避免频繁请求)
cache_key = f"realtime_price:{market}:{symbol}"
# 如果不是强制刷新,尝试使用缓存
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'
}
# 优先尝试使用 ticker API 获取实时价格
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'
}
# 缓存 30 秒
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}")
# 降级:使用 1 分钟 K 线
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'
}
# 缓存 30 秒
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}")
# 最后降级:使用日线数据(适用于非交易时间)
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'
}
# 日线数据缓存 5 分钟
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