This commit is contained in:
aaron 2026-03-13 22:16:21 +08:00
parent e3bfa597c9
commit ae762c94cb
3 changed files with 181 additions and 27 deletions

View File

@ -187,7 +187,8 @@ class Settings(BaseSettings):
# 消费:名创优品/泡泡玛特/安踏体育
# 能源:中海油/中石油/中国神华
stock_symbols_hk: str = "700.HK,9988.HK,3690.HK,1810.HK,9618.HK,9999.HK,9888.HK,1024.HK,2390.HK,9626.HK,1211.HK,2015.HK,9868.HK,1772.HK,916.HK,3868.HK,981.HK,1347.HK,1385.HK,20.HK,6682.HK,2121.HK,1357.HK,9959.HK,6608.HK,5.HK,939.HK,1398.HK,1288.HK,3988.HK,1299.HK,2318.HK,2628.HK,3908.HK,6030.HK,9866.HK,2333.HK,175.HK,2359.HK,2269.HK,6160.HK,1801.HK,1093.HK,9896.HK,9992.HK,2020.HK,883.HK,857.HK,1088.HK"
stock_analysis_interval: int = 300 # 分析间隔默认5分钟
# 注意:实际执行为每小时整点,此配置已废弃
stock_analysis_interval: int = 3600 # 分析间隔(秒,整点执行)
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
# A股智能体配置

View File

@ -1,6 +1,7 @@
"""
YFinance 服务 - 美股数据获取
YFinance 服务 - 美股港股数据获取
支持获取美股的实时行情和历史 K 线数据
备用数据源Stooq
"""
import pandas as pd
from typing import Dict, List, Optional
@ -10,20 +11,36 @@ import time
class YFinanceService:
"""YFinance 服务类"""
"""YFinance 服务类(支持 Stooq 备用)"""
def __init__(self):
"""初始化服务"""
# 初始化 YFinance
try:
import yfinance as yf
self.yf = yf
self._cache = {} # 数据缓存
self._cache_time = {} # 缓存时间
self._cache_ttl = 300 # 缓存有效期(秒)
self._yf_available = True
logger.info("YFinance 服务初始化成功")
except ImportError:
logger.error("yfinance 未安装,请运行: pip install yfinance")
raise
logger.warning("yfinance 未安装")
self._yf_available = False
# 初始化 Stooq备用
try:
import pandas_datareader.data as web
self.web = web
self._stooq_available = True
logger.info("Stooq 备用数据源初始化成功")
except ImportError:
logger.warning("pandas_datareader 未安装Stooq 备用不可用")
self._stooq_available = False
if not self._yf_available and not self._stooq_available:
raise Exception("没有可用的数据源,请安装 yfinance 或 pandas_datareader")
self._cache = {} # 数据缓存
self._cache_time = {} # 缓存时间
self._cache_ttl = 300 # 缓存有效期(秒)
def _normalize_hk_symbol(self, symbol: str) -> str:
"""
@ -53,7 +70,7 @@ class YFinanceService:
def get_ticker(self, symbol: str) -> Optional[Dict]:
"""
获取股票实时行情
获取股票实时行情优先使用 YFinance失败则使用 Stooq
Args:
symbol: 股票代码 'AAPL' '0700.HK'
@ -61,20 +78,33 @@ class YFinanceService:
Returns:
行情数据字典
"""
# 优先使用 YFinance
if self._yf_available:
result = self._get_yf_ticker(symbol)
if result:
return result
logger.info(f"YFinance 获取失败,尝试使用 Stooq 备用数据源 ({symbol})")
# 备用使用 Stooq
if self._stooq_available:
result = self._get_stooq_ticker(symbol)
if result:
return result
return None
def _get_yf_ticker(self, symbol: str) -> Optional[Dict]:
"""使用 YFinance 获取行情"""
try:
# 标准化港股代码格式
normalized_symbol = self._normalize_hk_symbol(symbol)
ticker = self.yf.Ticker(normalized_symbol)
# 使用 history 方法获取数据(更可靠,避免 429 错误)
hist = ticker.history(period="2d", interval="1h")
if hist.empty:
logger.warning(f"无法获取 {symbol} 的历史数据")
logger.warning(f"YFinance 无法获取 {symbol}数据")
return None
latest = hist.iloc[-1]
return {
'symbol': symbol,
'lastPrice': float(latest['Close']),
@ -89,13 +119,62 @@ class YFinanceService:
}
except Exception as e:
error_msg = str(e)
# 过滤掉常见的 429 错误信息
if "429" in error_msg or "Too Many Requests" in error_msg:
logger.warning(f"YFinance API 限流,请稍后再试 ({symbol})")
logger.warning(f"YFinance API 限流 ({symbol})")
else:
logger.error(f"获取 {symbol} 行情失败: {error_msg}")
logger.debug(f"YFinance 获取失败 ({symbol}): {error_msg}")
return None
def _get_stooq_ticker(self, symbol: str) -> Optional[Dict]:
"""使用 Stooq 获取行情(备用)"""
try:
# Stooq 使用的港股格式
stooq_symbol = self._convert_to_stooq_symbol(symbol)
# 获取最近几天的数据
start_date = (datetime.now() - timedelta(days=5)).strftime('%Y-%m-%d')
df = self.web.DataReader(stooq_symbol, 'stooq', start=start_date)
if df.empty:
logger.warning(f"Stooq 无法获取 {symbol} 的数据")
return None
# Stooq 返回的数据是倒序的,取第一行(最新)
latest = df.iloc[-1]
return {
'symbol': symbol,
'lastPrice': float(latest['Close']),
'priceChange': float(latest['Close'] - latest['Open']),
'priceChangePercent': float((latest['Close'] - latest['Open']) / latest['Open'] * 100) if latest['Open'] > 0 else 0,
'volume': int(latest['Volume']),
'high': float(latest['High']),
'low': float(latest['Low']),
'open': float(latest['Open']),
'prevClose': float(latest['Close']),
'timestamp': datetime.now().isoformat(),
'source': 'stooq' # 标记数据来源
}
except Exception as e:
logger.error(f"Stooq 获取 {symbol} 行情失败: {e}")
return None
def _convert_to_stooq_symbol(self, symbol: str) -> str:
"""
转换股票代码为 Stooq 格式
美股AAPL -> AAPL.US
港股0700.HK -> 0700.HK
"""
if symbol.endswith('.HK'):
return symbol
elif '.' in symbol:
# 其他格式保持不变
return symbol
else:
# 美股添加 .US 后缀
return f"{symbol}.US"
def get_multi_timeframe_data(
self,
symbol: str,
@ -112,11 +191,11 @@ class YFinanceService:
多时间周期数据字典 {'1d': df, '1h': df, ...}
"""
if timeframes is None:
# 默认时间周期配置 - 股票不需要太实时的数据
# 技术面分析时间周期1h、1d、1w
timeframes = {
'1w': ('1wk', '2y'), # 周级别2年
'1d': ('1d', '6mo'), # 日级别6个月
'1h': ('1h', '1mo'), # 小时级别1个月
'1w': ('1wk', '2y'), # 周级别2年 - 长期趋势
'1d': ('1d', '6mo'), # 日级别6个月 - 中期趋势
'1h': ('1h', '1mo'), # 小时级别1个月 - 短期趋势
}
result = {}
@ -140,7 +219,7 @@ class YFinanceService:
interval: str,
period: str
) -> Optional[pd.DataFrame]:
"""获取带缓存的数据"""
"""获取带缓存的数据(优先 YFinance失败则使用 Stooq"""
# 标准化港股代码格式
normalized_symbol = self._normalize_hk_symbol(symbol)
cache_key = f"{normalized_symbol}_{interval}_{period}"
@ -153,15 +232,40 @@ class YFinanceService:
logger.debug(f"使用缓存数据: {cache_key}")
return self._cache[cache_key]
# 获取新数据
# 优先使用 YFinance
if self._yf_available:
df = self._get_yf_data(symbol, interval, period, cache_key, now)
if df is not None:
return df
logger.info(f"YFinance 获取历史数据失败,尝试 Stooq ({symbol})")
# 备用使用 Stooq
if self._stooq_available:
df = self._get_stooq_data(symbol, interval, period, cache_key, now)
if df is not None:
logger.info(f"✓ 使用 Stooq 数据源 ({symbol})")
return df
return None
def _get_yf_data(
self,
symbol: str,
interval: str,
period: str,
cache_key: str,
now: datetime
) -> Optional[pd.DataFrame]:
"""使用 YFinance 获取历史数据"""
try:
normalized_symbol = self._normalize_hk_symbol(symbol)
ticker = self.yf.Ticker(normalized_symbol)
df = ticker.history(period=period, interval=interval)
if df.empty:
return None
# 转换数据格式(兼容现有代码)
# 转换数据格式
df = self._format_dataframe(df)
# 更新缓存
@ -169,11 +273,59 @@ class YFinanceService:
self._cache_time[cache_key] = now
return df
except Exception as e:
logger.error(f"获取数据失败 {cache_key}: {e}")
logger.debug(f"YFinance 获取历史数据失败: {e}")
return None
def _get_stooq_data(
self,
symbol: str,
interval: str,
period: str,
cache_key: str,
now: datetime
) -> Optional[pd.DataFrame]:
"""使用 Stooq 获取历史数据(备用)"""
try:
# 转换为 Stooq 格式
stooq_symbol = self._convert_to_stooq_symbol(symbol)
# 将 period 转换为天数
period_days = self._period_to_days(period)
start_date = (datetime.now() - timedelta(days=period_days)).strftime('%Y-%m-%d')
# 获取数据
df = self.web.DataReader(stooq_symbol, 'stooq', start=start_date)
if df.empty:
return None
# Stooq 数据是倒序的,需要反转
df = df.iloc[::-1]
# 转换数据格式
df = self._format_dataframe(df)
# 更新缓存
self._cache[cache_key] = df
self._cache_time[cache_key] = now
return df
except Exception as e:
logger.debug(f"Stooq 获取历史数据失败: {e}")
return None
def _period_to_days(self, period: str) -> int:
"""将 YFinance period 格式转换为天数"""
period_map = {
'1mo': 30,
'3mo': 90,
'6mo': 180,
'1y': 365,
'2y': 730,
}
return period_map.get(period, 180) # 默认6个月
def _format_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
"""
格式化 DataFrame 以兼容现有代码

View File

@ -15,6 +15,7 @@ numpy>=1.26.0
python-multipart==0.0.6
aiohttp==3.9.1
yfinance>=0.2.36
pandas-datareader>=0.10.0 # Stooq 数据源支持(美股港股备用)
PyJWT==2.8.0
tencentcloud-sdk-python==3.0.1100
python-jose[cryptography]==3.3.0