551 lines
21 KiB
Python
551 lines
21 KiB
Python
"""
|
||
基本面因子数据服务
|
||
获取美股和港股的基本面数据,包括估值、盈利能力、成长性等指标
|
||
"""
|
||
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
|