diff --git a/backend/app/config.py b/backend/app/config.py index b1704b5..3ca5c9a 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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股智能体配置 diff --git a/backend/app/services/yfinance_service.py b/backend/app/services/yfinance_service.py index 65ea145..20ec48a 100644 --- a/backend/app/services/yfinance_service.py +++ b/backend/app/services/yfinance_service.py @@ -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 以兼容现有代码 diff --git a/backend/requirements.txt b/backend/requirements.txt index 3716871..5289341 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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