update
This commit is contained in:
parent
e3bfa597c9
commit
ae762c94cb
@ -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股智能体配置
|
||||
|
||||
@ -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 以兼容现有代码
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user