""" 基本面因子数据服务 获取美股和港股的基本面数据,包括估值、盈利能力、成长性等指标 """ from typing import Dict, Any, Optional, List from datetime import datetime import pandas as pd try: import yfinance as yf YFINANCE_AVAILABLE = True except ImportError: YFINANCE_AVAILABLE = False from app.utils.logger import logger class FundamentalService: """基本面因子数据服务""" def __init__(self): """初始化服务""" if not YFINANCE_AVAILABLE: logger.warning("yfinance 未安装,基本面数据功能将不可用") return self._cache = {} # 数据缓存 self._cache_time = {} # 缓存时间 self._cache_ttl = 3600 # 缓存有效期1小时 logger.info("基本面数据服务初始化成功") @staticmethod def _normalize_hk_symbol(symbol: str) -> str: """ 标准化港股代码格式为 yfinance 要求的格式 - 4位及以下:左侧补零到4位,如 700.HK → 0700.HK, 5.HK → 0005.HK - 5位及以上:去掉前导零,如 09618.HK → 9618.HK """ if not symbol.endswith('.HK'): return symbol # 分离代码和后缀 code_part = symbol[:-3] # 去掉 .HK suffix = '.HK' # 如果是纯数字代码 if code_part.isdigit(): # 4位及以下:补零到4位 if len(code_part) <= 4: normalized_code = code_part.zfill(4) # 5位及以上:去掉前导零 else: normalized_code = code_part.lstrip('0') or '0' else: normalized_code = code_part return normalized_code + suffix def get_fundamental_data(self, symbol: str) -> Optional[Dict[str, Any]]: """ 获取股票的基本面数据 Args: symbol: 股票代码,如 'AAPL', '0700.HK' Returns: 基本面数据字典,包含估值、盈利、成长等指标 """ if not YFINANCE_AVAILABLE: return None try: # 标准化港股代码格式 normalized_symbol = self._normalize_hk_symbol(symbol) ticker = yf.Ticker(normalized_symbol) # 获取股票信息 info = ticker.info if not info: logger.warning(f"无法获取 {symbol} 的基本面数据") return None # 提取关键指标 fundamental_data = { 'symbol': symbol, 'timestamp': datetime.now().isoformat(), # 基本信息 'company_name': info.get('longName', info.get('shortName', 'N/A')), 'sector': info.get('sector', 'N/A'), 'industry': info.get('industry', 'N/A'), 'market_cap': info.get('marketCap'), 'shares_outstanding': info.get('sharesOutstanding'), # 估值指标 'valuation': self._extract_valuation_metrics(info), # 盈利能力 'profitability': self._extract_profitability_metrics(info), # 成长性 'growth': self._extract_growth_metrics(info), # 财务健康 'financial_health': self._extract_financial_health_metrics(info), # 股票回报 'returns': self._extract_return_metrics(info), # 分析师建议 'analyst': self._extract_analyst_metrics(info), } # 计算综合评分 fundamental_data['score'] = self._calculate_fundamental_score(fundamental_data) # 输出基本面关键指标 score = fundamental_data.get('score', {}) logger.info(f"✓ {symbol} 基本面数据获取成功") logger.info(f" 【公司】{fundamental_data.get('company_name', 'N/A')} | {fundamental_data.get('sector', 'N/A')}") logger.info(f" 【评分】总分: {score.get('total', 0):.0f}/100 ({score.get('rating', 'N/A')}级) | " f"估值:{score.get('valuation', 0)} 盈利:{score.get('profitability', 0)} " f"成长:{score.get('growth', 0)} 财务:{score.get('financial_health', 0)}") # 估值指标 val = fundamental_data.get('valuation', {}) if val.get('pe_ratio'): pe = val['pe_ratio'] pb = val.get('pb_ratio') ps = val.get('ps_ratio') peg = val.get('peg_ratio') pb_str = f"{pb:.2f}" if pb is not None else "N/A" ps_str = f"{ps:.2f}" if ps is not None else "N/A" peg_str = f"{peg:.2f}" if peg is not None else "N/A" logger.info(f" 【估值】PE:{pe:.2f} | PB:{pb_str} | PS:{ps_str} | PEG:{peg_str}") # 盈利能力 prof = fundamental_data.get('profitability', {}) if prof.get('return_on_equity'): roe = prof['return_on_equity'] pm = prof.get('profit_margin') gm = prof.get('gross_margin') pm_str = f"{pm:.1f}" if pm is not None else "N/A" gm_str = f"{gm:.1f}" if gm is not None else "N/A" logger.info(f" 【盈利】ROE:{roe:.2f}% | 净利率:{pm_str}% | 毛利率:{gm_str}%") # 成长性 growth = fundamental_data.get('growth', {}) rg = growth.get('revenue_growth') eg = growth.get('earnings_growth') if rg is not None or eg is not None: rg_str = f"{rg:.1f}" if rg is not None else "N/A" eg_str = f"{eg:.1f}" if eg is not None else "N/A" logger.info(f" 【成长】营收增长:{rg_str}% | 盈利增长:{eg_str}%") # 财务健康 fin = fundamental_data.get('financial_health', {}) if fin.get('debt_to_equity'): de = fin['debt_to_equity'] cr = fin.get('current_ratio') cr_str = f"{cr:.2f}" if cr is not None else "N/A" logger.info(f" 【财务】债务股本比:{de:.2f} | 流动比率:{cr_str}") # 分析师建议 analyst = fundamental_data.get('analyst', {}) tp = analyst.get('target_price') if tp: rec = analyst.get('recommendation', 'N/A') logger.info(f" 【分析师】目标价:${tp:.2f} | 评级:{rec}") return fundamental_data except Exception as e: logger.error(f"获取 {symbol} 基本面数据失败: {e}") return None def _extract_valuation_metrics(self, info: Dict) -> Dict[str, Any]: """提取估值指标""" return { 'pe_ratio': info.get('trailingPE'), # 市盈率 'forward_pe': info.get('forwardPE'), # 远期市盈率 'peg_ratio': info.get('pegRatio'), # PEG 'pb_ratio': info.get('priceToBook'), # 市净率 'ps_ratio': info.get('priceToSalesTrailing12M'), # 市销率 'ev_to_ebitda': info.get('enterpriseToEbitda'), # EV/EBITDA 'enterprise_value': info.get('enterpriseValue'), # 企业价值 } def _extract_profitability_metrics(self, info: Dict) -> Dict[str, Any]: """提取盈利能力指标""" return { 'eps': info.get('trailingEps'), # 每股收益 'forward_eps': info.get('forwardEps'), # 预期每股收益 'revenue': info.get('totalRevenue'), # 总收入 'net_income': info.get('netIncomeToCommon'), # 净收入 'profit_margin': info.get('profitMargins'), # 利润率 'operating_margin': info.get('operatingMargins'), # 营业利润率 'gross_margin': info.get('grossMargins'), # 毛利率 'ebitda': info.get('ebitda'), # EBITDA 'ebitda_margins': info.get('ebitdaMargins'), # EBITDA利润率 } def _extract_growth_metrics(self, info: Dict) -> Dict[str, Any]: """提取成长性指标""" return { 'revenue_growth': info.get('revenueGrowth'), # 营收增长率 'earnings_growth': info.get('earningsGrowth'), # 盈利增长 'earnings_quarterly_growth': info.get('earningsQuarterlyGrowth'), # 季度盈利增长 'revenue_quarterly_growth': info.get('revenueQuarterlyGrowth'), # 季度营收增长 } def _extract_financial_health_metrics(self, info: Dict) -> Dict[str, Any]: """提取财务健康指标""" return { 'debt_to_equity': info.get('debtToEquity'), # 债务股本比 'current_ratio': info.get('currentRatio'), # 流动比率 'quick_ratio': info.get('quickRatio'), # 速动比率 'total_cash': info.get('totalCash'), # 总现金 'total_debt': info.get('totalDebt'), # 总债务 'operating_cashflow': info.get('operatingCashflow'), # 经营现金流 'free_cashflow': info.get('freeCashflow'), # 自由现金流 } def _extract_return_metrics(self, info: Dict) -> Dict[str, Any]: """提取股票回报指标""" return { 'dividend_rate': info.get('dividendRate'), # 股息率 'dividend_yield': info.get('dividendYield'), # 股息收益率 'payout_ratio': info.get('payoutRatio'), # 派息比率 'five_year_avg_dividend_yield': info.get('fiveYearAvgDividendYield'), # 5年平均股息率 'return_on_equity': info.get('returnOnEquity'), # ROE 'return_on_assets': info.get('returnOnAssets'), # ROA } def _extract_analyst_metrics(self, info: Dict) -> Dict[str, Any]: """提取分析师建议""" return { 'target_price': info.get('targetMeanPrice'), # 目标价 'target_high': info.get('targetHighPrice'), # 目标价上限 'target_low': info.get('targetLowPrice'), # 目标价下限 'recommendation': info.get('recommendationKey'), # 分析师建议 'number_of_analysts': info.get('numberOfAnalystOpinions'), # 分析师数量 } def _calculate_fundamental_score(self, data: Dict[str, Any]) -> Dict[str, Any]: """ 计算基本面综合评分(0-100分) 评分维度: 1. 估值合理性 (0-25分) 2. 盈利能力 (0-25分) 3. 成长性 (0-25分) 4. 财务健康 (0-25分) """ scores = { 'valuation': 0, 'profitability': 0, 'growth': 0, 'financial_health': 0, 'total': 0 } try: # 1. 估值评分 (0-25分) valuation = data.get('valuation', {}) if valuation.get('pe_ratio'): pe = valuation['pe_ratio'] # PE < 15: 优秀,15-25: 良好,25-40: 一般,>40: 偏高 if pe < 15: scores['valuation'] = 25 elif pe < 25: scores['valuation'] = 20 elif pe < 40: scores['valuation'] = 10 else: scores['valuation'] = 5 # 2. 盈利能力评分 (0-25分) profitability = data.get('profitability', {}) roe = profitability.get('return_on_equity') profit_margin = profitability.get('profit_margin') # 处理 None 值 if roe is None: roe = 0 if profit_margin is None: profit_margin = 0 if roe > 0: # ROE > 20%: 优秀,15-20%: 良好,10-15%: 一般,< 10%: 较差 if roe > 20: scores['profitability'] += 15 elif roe > 15: scores['profitability'] += 12 elif roe > 10: scores['profitability'] += 8 else: scores['profitability'] += 4 if profit_margin > 0: # 净利率 > 20%: 优秀,10-20%: 良好,5-10%: 一般 if profit_margin > 20: scores['profitability'] += 10 elif profit_margin > 10: scores['profitability'] += 7 else: scores['profitability'] += 4 # 3. 成长性评分 (0-25分) growth = data.get('growth', {}) revenue_growth = growth.get('revenue_growth') earnings_growth = growth.get('earnings_growth') # 处理 None 值 if revenue_growth is None: revenue_growth = 0 if earnings_growth is None: earnings_growth = 0 if revenue_growth > 0: # 营收增长 > 30%: 优秀,20-30%: 良好,10-20%: 一般,< 10%: 较差 if revenue_growth > 30: scores['growth'] += 12 elif revenue_growth > 20: scores['growth'] += 10 elif revenue_growth > 10: scores['growth'] += 6 else: scores['growth'] += 3 if earnings_growth > 0: # 盈利增长 > 30%: 优秀,20-30%: 良好,10-20%: 一般 if earnings_growth > 30: scores['growth'] += 13 elif earnings_growth > 20: scores['growth'] += 10 elif earnings_growth > 10: scores['growth'] += 6 else: scores['growth'] += 3 # 4. 财务健康评分 (0-25分) financial = data.get('financial_health', {}) debt_to_equity = financial.get('debt_to_equity') current_ratio = financial.get('current_ratio') # 处理 None 值 if debt_to_equity is None: debt_to_equity = 0 if current_ratio is None: current_ratio = 0 # 债务股本比 < 1: 优秀,1-2: 良好,2-3: 一般,> 3: 风险高 if debt_to_equity < 1: scores['financial_health'] += 12 elif debt_to_equity < 2: scores['financial_health'] += 10 elif debt_to_equity < 3: scores['financial_health'] += 5 else: scores['financial_health'] += 2 # 流动比率 > 2: 优秀,1.5-2: 良好,1-1.5: 一般,< 1: 风险 if current_ratio > 2: scores['financial_health'] += 13 elif current_ratio > 1.5: scores['financial_health'] += 10 elif current_ratio > 1: scores['financial_health'] += 5 else: scores['financial_health'] += 0 # 现金流评分 fc = financial.get('free_cashflow') if fc is not None and fc > 0: scores['financial_health'] += 0 # 已在盈利能力中考虑 # 计算总分 scores['total'] = sum([scores['valuation'], scores['profitability'], scores['growth'], scores['financial_health']]) # 添加评级 if scores['total'] >= 80: scores['rating'] = 'A' elif scores['total'] >= 60: scores['rating'] = 'B' elif scores['total'] >= 40: scores['rating'] = 'C' else: scores['rating'] = 'D' except Exception as e: logger.error(f"计算基本面评分失败: {e}") return scores def get_fundamental_summary(self, symbol: str, data: Dict[str, Any] = None) -> str: """ 生成基本面数据摘要文本,用于 LLM 分析 Args: symbol: 股票代码 data: 可选,已获取的基本面数据。如果为None,则自动获取 Returns: 基本面摘要文本 """ if data is None: data = self.get_fundamental_data(symbol) if not data: return f"{symbol}: 暂无基本面数据" summary_parts = [] # 基本信息 summary_parts.append(f"【公司信息】{data.get('company_name', 'N/A')} | " f"行业: {data.get('sector', 'N/A')}") # 估值情况 val = data.get('valuation', {}) if val.get('pe_ratio'): summary_parts.append(f"【估值】PE: {val['pe_ratio']:.2f} | " f"PB: {val.get('pb_ratio', 'N/A')} | " f"PS: {val.get('ps_ratio', 'N/A')}") # 盈利能力 prof = data.get('profitability', {}) if prof.get('return_on_equity'): pm = prof.get('profit_margin') gm = prof.get('gross_margin') pm_str = f"{pm:.1f}" if pm is not None else "N/A" gm_str = f"{gm:.1f}" if gm is not None else "N/A" summary_parts.append(f"【盈利】ROE: {prof['return_on_equity']:.2f}% | " f"净利率: {pm_str}% | " f"毛利率: {gm_str}%") # 成长性 growth = data.get('growth', {}) rg = growth.get('revenue_growth') eg = growth.get('earnings_growth') if rg is not None or eg is not None: rg_str = f"{rg:.1f}" if rg is not None else "N/A" eg_str = f"{eg:.1f}" if eg is not None else "N/A" summary_parts.append(f"【成长】营收增长: {rg_str}% | " f"盈利增长: {eg_str}%") # 财务健康 fin = data.get('financial_health', {}) if fin.get('debt_to_equity'): cr = fin.get('current_ratio') cr_str = f"{cr:.2f}" if cr is not None else "N/A" summary_parts.append(f"【财务】债务股本比: {fin['debt_to_equity']:.2f} | " f"流动比率: {cr_str}") # 分析师建议 analyst = data.get('analyst', {}) if analyst.get('target_price'): summary_parts.append(f"【分析师建议】目标价: ${analyst['target_price']:.2f} | " f"评级: {analyst.get('recommendation', 'N/A')}") # 基本面评分 score = data.get('score', {}) summary_parts.append(f"【基本面评分】{score.get('total', 0):.0f}/100 ({score.get('rating', 'N/A')}级)") return "\n".join(summary_parts) def batch_get_fundamentals(self, symbols: List[str]) -> Dict[str, Dict[str, Any]]: """ 批量获取多只股票的基本面数据 Args: symbols: 股票代码列表 Returns: 股票代码到基本面数据的映射 """ results = {} for symbol in symbols: data = self.get_fundamental_data(symbol) if data: results[symbol] = data logger.info(f"批量获取基本面数据完成: {len(results)}/{len(symbols)} 只股票") return results def compare_stocks(self, symbols: List[str]) -> Dict[str, Any]: """ 比较多只股票的基本面指标 Args: symbols: 股票代码列表 Returns: 比较结果 """ fundamentals = self.batch_get_fundamentals(symbols) comparison = { 'symbols': symbols, 'metrics': {} } # 提取可比较的指标 metrics_to_compare = [ ('valuation', ['pe_ratio', 'pb_ratio']), ('profitability', ['return_on_equity', 'profit_margin']), ('growth', ['revenue_growth', 'earnings_growth']), ('financial_health', ['debt_to_equity', 'current_ratio']), ] for category, metric_names in metrics_to_compare: comparison['metrics'][category] = {} for metric in metric_names: values = {} for symbol in symbols: if symbol in fundamentals: category_data = fundamentals[symbol].get(category, {}) value = category_data.get(metric) if value is not None: values[symbol] = value if values: comparison['metrics'][category][metric] = values # 计算排名 comparison['rankings'] = {} if 'valuation' in comparison['metrics']: pe_ratios = {s: v.get('valuation', {}).get('pe_ratio') for s, v in fundamentals.items() if v.get('valuation', {}).get('pe_ratio')} if pe_ratios: # PE 越低越好 sorted_pe = sorted(pe_ratios.items(), key=lambda x: x[1]) comparison['rankings']['pe_low_to_high'] = [s[0] for s in sorted_pe] return comparison # 全局单例 _fundamental_service: Optional[FundamentalService] = None def get_fundamental_service() -> FundamentalService: """获取基本面数据服务单例""" global _fundamental_service if _fundamental_service is None: _fundamental_service = FundamentalService() return _fundamental_service