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:
TIANHE
2026-01-13 00:21:33 +08:00
parent ac932aebf0
commit 714dd47c86
10 changed files with 643 additions and 78 deletions
@@ -386,6 +386,94 @@ class AShareDataSource(BaseDataSource, TencentDataMixin):
logger.error(traceback.format_exc())
return klines
def get_ticker(self, symbol: str) -> Dict[str, Any]:
"""
获取A股实时报价
使用东方财富实时行情API获取实时报价
Returns:
dict: {
'last': 当前价格,
'change': 涨跌额,
'changePercent': 涨跌幅,
'high': 最高价,
'low': 最低价,
'open': 开盘价,
'previousClose': 昨收价
}
"""
symbol = (symbol or '').strip()
# 优先使用东方财富实时行情 API
try:
# 判断市场
if symbol.startswith('6'):
secid = f"1.{symbol}" # 上海
elif symbol.startswith('0') or symbol.startswith('3'):
secid = f"0.{symbol}" # 深圳
elif symbol.startswith('4') or symbol.startswith('8'):
secid = f"0.{symbol}" # 北交所
else:
secid = f"1.{symbol}"
# 东方财富实时行情接口
url = "https://push2.eastmoney.com/api/qt/stock/get"
params = {
'secid': secid,
'fields': 'f43,f44,f45,f46,f47,f48,f57,f58,f60,f169,f170',
# f43=最新价, f44=最高价, f45=最低价, f46=开盘价
# f60=昨收价, f169=涨跌额, f170=涨跌幅
}
session = get_retry_session()
response = session.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
if data and data.get('data'):
d = data['data']
last_price = d.get('f43', 0)
# 东方财富返回的价格是整数(分),需要除以100
if last_price and last_price > 0:
divisor = 100 if last_price > 1000 else 1 # 价格超过10元时用分表示
return {
'last': last_price / divisor,
'high': d.get('f44', 0) / divisor,
'low': d.get('f45', 0) / divisor,
'open': d.get('f46', 0) / divisor,
'previousClose': d.get('f60', 0) / divisor,
'change': d.get('f169', 0) / divisor,
'changePercent': d.get('f170', 0) / 100 # 涨跌幅是整数(%*100
}
except Exception as e:
logger.debug(f"Eastmoney ticker failed for {symbol}: {e}")
# 降级使用腾讯实时报价
try:
tencent_symbol = self._to_tencent_symbol(symbol)
if tencent_symbol:
url = f"http://qt.gtimg.cn/q={tencent_symbol}"
response = requests.get(url, timeout=10)
content = response.content.decode('gbk', errors='ignore')
if '="' in content:
data_str = content.split('="')[1].strip('";\n')
if data_str:
parts = data_str.split('~')
if len(parts) > 32:
return {
'last': float(parts[3]) if parts[3] else 0,
'change': float(parts[31]) if parts[31] else 0,
'changePercent': float(parts[32]) if parts[32] else 0,
'high': float(parts[33]) if len(parts) > 33 and parts[33] else 0,
'low': float(parts[34]) if len(parts) > 34 and parts[34] else 0,
'open': float(parts[5]) if len(parts) > 5 and parts[5] else 0,
'previousClose': float(parts[4]) if parts[4] else 0
}
except Exception as e:
logger.debug(f"Tencent ticker failed for {symbol}: {e}")
return {'last': 0, 'symbol': symbol}
class HShareDataSource(BaseDataSource, TencentDataMixin):
@@ -613,3 +701,79 @@ class HShareDataSource(BaseDataSource, TencentDataMixin):
logger.error(traceback.format_exc())
return klines
def get_ticker(self, symbol: str) -> Dict[str, Any]:
"""
获取港股实时报价
使用腾讯财经实时行情API获取实时报价
Returns:
dict: {
'last': 当前价格,
'change': 涨跌额,
'changePercent': 涨跌幅,
'high': 最高价,
'low': 最低价,
'open': 开盘价,
'previousClose': 昨收价
}
"""
symbol = (symbol or '').strip()
# 使用腾讯财经实时报价
try:
tencent_symbol = self._to_tencent_symbol(symbol)
url = f"http://qt.gtimg.cn/q={tencent_symbol}"
response = requests.get(url, timeout=10)
content = response.content.decode('gbk', errors='ignore')
if '="' in content:
data_str = content.split('="')[1].strip('";\n')
if data_str:
parts = data_str.split('~')
if len(parts) > 32:
return {
'last': float(parts[3]) if parts[3] else 0,
'change': float(parts[31]) if parts[31] else 0,
'changePercent': float(parts[32]) if parts[32] else 0,
'high': float(parts[33]) if len(parts) > 33 and parts[33] else 0,
'low': float(parts[34]) if len(parts) > 34 and parts[34] else 0,
'open': float(parts[5]) if len(parts) > 5 and parts[5] else 0,
'previousClose': float(parts[4]) if parts[4] else 0
}
except Exception as e:
logger.debug(f"Tencent ticker failed for {symbol}: {e}")
# 降级使用东方财富
try:
hk_symbol = symbol.zfill(5)
secid = f"116.{hk_symbol}"
url = "https://push2.eastmoney.com/api/qt/stock/get"
params = {
'secid': secid,
'fields': 'f43,f44,f45,f46,f47,f48,f57,f58,f60,f169,f170',
}
session = get_retry_session()
response = session.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
if data and data.get('data'):
d = data['data']
last_price = d.get('f43', 0)
if last_price and last_price > 0:
divisor = 1000 if last_price > 10000 else 100 if last_price > 1000 else 1
return {
'last': last_price / divisor,
'high': d.get('f44', 0) / divisor,
'low': d.get('f45', 0) / divisor,
'open': d.get('f46', 0) / divisor,
'previousClose': d.get('f60', 0) / divisor,
'change': d.get('f169', 0) / divisor,
'changePercent': d.get('f170', 0) / 100
}
except Exception as e:
logger.debug(f"Eastmoney ticker failed for {symbol}: {e}")
return {'last': 0, 'symbol': symbol}