@@ -473,6 +473,28 @@ class AShareDataSource(BaseDataSource, TencentDataMixin):
|
||||
except Exception as e:
|
||||
logger.debug(f"Tencent ticker failed for {symbol}: {e}")
|
||||
|
||||
# 第三备选: Akshare
|
||||
try:
|
||||
import akshare as ak
|
||||
# 使用 akshare 获取实时行情
|
||||
df = ak.stock_zh_a_spot_em()
|
||||
if df is not None and not df.empty:
|
||||
# 在数据中查找对应股票
|
||||
row = df[df['代码'] == symbol]
|
||||
if not row.empty:
|
||||
row = row.iloc[0]
|
||||
return {
|
||||
'last': float(row.get('最新价', 0) or 0),
|
||||
'change': float(row.get('涨跌额', 0) or 0),
|
||||
'changePercent': float(row.get('涨跌幅', 0) or 0),
|
||||
'high': float(row.get('最高', 0) or 0),
|
||||
'low': float(row.get('最低', 0) or 0),
|
||||
'open': float(row.get('今开', 0) or 0),
|
||||
'previousClose': float(row.get('昨收', 0) or 0)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"Akshare ticker failed for {symbol}: {e}")
|
||||
|
||||
return {'last': 0, 'symbol': symbol}
|
||||
|
||||
|
||||
@@ -776,4 +798,44 @@ class HShareDataSource(BaseDataSource, TencentDataMixin):
|
||||
except Exception as e:
|
||||
logger.debug(f"Eastmoney ticker failed for {symbol}: {e}")
|
||||
|
||||
# 第三备选: yfinance
|
||||
try:
|
||||
import yfinance as yf
|
||||
# 港股在 yfinance 中的格式是 XXXX.HK
|
||||
yf_symbol = f"{symbol.zfill(4)}.HK"
|
||||
ticker = yf.Ticker(yf_symbol)
|
||||
|
||||
# Try fast_info first (faster)
|
||||
try:
|
||||
info = ticker.fast_info
|
||||
if hasattr(info, 'last_price') and info.last_price and info.last_price > 0:
|
||||
return {
|
||||
'last': float(info.last_price),
|
||||
'change': float(info.last_price - info.previous_close) if hasattr(info, 'previous_close') and info.previous_close else 0,
|
||||
'changePercent': float((info.last_price - info.previous_close) / info.previous_close * 100) if hasattr(info, 'previous_close') and info.previous_close else 0,
|
||||
'high': float(info.day_high) if hasattr(info, 'day_high') and info.day_high else float(info.last_price),
|
||||
'low': float(info.day_low) if hasattr(info, 'day_low') and info.day_low else float(info.last_price),
|
||||
'open': float(info.open) if hasattr(info, 'open') and info.open else float(info.last_price),
|
||||
'previousClose': float(info.previous_close) if hasattr(info, 'previous_close') and info.previous_close else 0
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to history
|
||||
hist = ticker.history(period="2d")
|
||||
if hist is not None and not hist.empty:
|
||||
current = float(hist['Close'].iloc[-1])
|
||||
prev_close = float(hist['Close'].iloc[-2]) if len(hist) > 1 else current
|
||||
return {
|
||||
'last': current,
|
||||
'change': current - prev_close,
|
||||
'changePercent': (current - prev_close) / prev_close * 100 if prev_close else 0,
|
||||
'high': float(hist['High'].iloc[-1]),
|
||||
'low': float(hist['Low'].iloc[-1]),
|
||||
'open': float(hist['Open'].iloc[-1]),
|
||||
'previousClose': prev_close
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"yfinance ticker failed for {symbol}: {e}")
|
||||
|
||||
return {'last': 0, 'symbol': symbol}
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import requests
|
||||
import threading
|
||||
|
||||
from app.data_sources.base import BaseDataSource, TIMEFRAME_SECONDS
|
||||
from app.utils.logger import get_logger
|
||||
@@ -13,6 +14,11 @@ from app.config import TiingoConfig, APIKeys
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 全局缓存 - 减少 Tiingo API 调用
|
||||
_forex_cache: Dict[str, Dict[str, Any]] = {}
|
||||
_forex_cache_lock = threading.Lock()
|
||||
_FOREX_CACHE_TTL = 60 # 外汇价格缓存 60 秒 (Tiingo 免费 API 限制严格)
|
||||
|
||||
|
||||
class ForexDataSource(BaseDataSource):
|
||||
"""外汇数据源 (Tiingo)"""
|
||||
@@ -60,6 +66,7 @@ class ForexDataSource(BaseDataSource):
|
||||
获取外汇实时报价
|
||||
|
||||
使用 Tiingo FX Top-of-Book API 获取实时报价
|
||||
带有 60 秒缓存以避免频繁触发 Tiingo 速率限制
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
@@ -75,6 +82,16 @@ class ForexDataSource(BaseDataSource):
|
||||
logger.warning("Tiingo API key not configured")
|
||||
return {'last': 0, 'symbol': symbol}
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"ticker_{symbol}"
|
||||
with _forex_cache_lock:
|
||||
cached = _forex_cache.get(cache_key)
|
||||
if cached:
|
||||
cache_time = cached.get('_cache_time', 0)
|
||||
if time.time() - cache_time < _FOREX_CACHE_TTL:
|
||||
logger.debug(f"Using cached forex ticker for {symbol}")
|
||||
return cached
|
||||
|
||||
try:
|
||||
# 解析 symbol
|
||||
tiingo_symbol = self.SYMBOL_MAP.get(symbol)
|
||||
@@ -93,12 +110,20 @@ class ForexDataSource(BaseDataSource):
|
||||
for attempt in range(3):
|
||||
response = requests.get(url, params=params, timeout=TiingoConfig.TIMEOUT)
|
||||
if response.status_code == 429:
|
||||
time.sleep(2 * (attempt + 1))
|
||||
wait_time = 2 * (attempt + 1)
|
||||
logger.warning(f"Tiingo rate limit (429), waiting {wait_time}s before retry ({attempt+1}/3)")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
break
|
||||
|
||||
if response.status_code == 429:
|
||||
logger.warning("Tiingo rate limit exceeded for ticker request")
|
||||
logger.info("Note: Tiingo 1-minute forex data requires a paid subscription")
|
||||
# 返回缓存数据(如果有的话,即使已过期)
|
||||
with _forex_cache_lock:
|
||||
if cache_key in _forex_cache:
|
||||
logger.info(f"Returning stale cache for {symbol} due to rate limit")
|
||||
return _forex_cache[cache_key]
|
||||
return {'last': 0, 'symbol': symbol}
|
||||
|
||||
response.raise_for_status()
|
||||
@@ -144,15 +169,22 @@ class ForexDataSource(BaseDataSource):
|
||||
except Exception:
|
||||
pass # 涨跌计算失败不影响主要功能
|
||||
|
||||
return {
|
||||
result = {
|
||||
'last': round(last_price, 5),
|
||||
'bid': round(bid, 5),
|
||||
'ask': round(ask, 5),
|
||||
'change': round(change, 5),
|
||||
'changePercent': round(change_pct, 2),
|
||||
'previousClose': round(prev_close, 5) if prev_close else 0
|
||||
'previousClose': round(prev_close, 5) if prev_close else 0,
|
||||
'_cache_time': time.time()
|
||||
}
|
||||
|
||||
# 缓存结果
|
||||
with _forex_cache_lock:
|
||||
_forex_cache[cache_key] = result
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get forex ticker for {symbol}: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user