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
This commit is contained in:
@@ -65,9 +65,126 @@ class KlineService:
|
||||
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
|
||||
|
||||
|
||||
@@ -116,12 +116,11 @@ def _get_positions_for_monitor(position_ids: List[int] = None) -> List[Dict[str,
|
||||
quantity = float(row.get('quantity') or 0)
|
||||
side = row.get('side') or 'long'
|
||||
|
||||
# Get current price
|
||||
# Get current price (use realtime price API)
|
||||
current_price = 0
|
||||
try:
|
||||
klines = kline_service.get_kline(market, symbol, '1D', 1)
|
||||
if klines:
|
||||
current_price = float(klines[-1].get('close') or 0)
|
||||
price_data = kline_service.get_realtime_price(market, symbol)
|
||||
current_price = float(price_data.get('price') or 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -982,12 +981,11 @@ def _check_position_alerts():
|
||||
if not can_trigger:
|
||||
continue
|
||||
|
||||
# Get current price
|
||||
# Get current price (use realtime price API)
|
||||
current_price = 0
|
||||
try:
|
||||
klines = kline_service.get_kline(market, symbol, '1D', 1)
|
||||
if klines:
|
||||
current_price = float(klines[-1].get('close') or 0)
|
||||
price_data = kline_service.get_realtime_price(market, symbol)
|
||||
current_price = float(price_data.get('price') or 0)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
Reference in New Issue
Block a user