1328 lines
48 KiB
Python
1328 lines
48 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
财务指标分析器
|
||
用于分析MongoDB中eastmoney_financial_data_v2集合的财务数据
|
||
只返回具体的数值信息,不进行评估
|
||
"""
|
||
|
||
import sys
|
||
import pymongo
|
||
import datetime
|
||
import logging
|
||
from typing import Dict, List, Optional, Union
|
||
from pathlib import Path
|
||
from sqlalchemy import create_engine, text
|
||
import numpy as np
|
||
|
||
# 添加项目根路径到Python路径
|
||
project_root = Path(__file__).parent.parent.parent
|
||
sys.path.append(str(project_root))
|
||
|
||
# 导入配置
|
||
from src.valuation_analysis.config import MONGO_CONFIG2, DB_URL
|
||
|
||
# 导入股票代码格式转换工具
|
||
from tools.stock_code_formatter import StockCodeFormatter
|
||
|
||
# 设置日志
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class FinancialIndicatorAnalyzer:
|
||
"""财务指标分析器"""
|
||
|
||
def __init__(self):
|
||
"""初始化"""
|
||
# MongoDB连接
|
||
self.mongo_client = None
|
||
self.db = None
|
||
self.collection_name = 'eastmoney_financial_data_v2'
|
||
self.collection = None
|
||
|
||
# MySQL连接
|
||
self.mysql_engine = None
|
||
|
||
# 股票代码格式转换器
|
||
self.code_formatter = StockCodeFormatter()
|
||
|
||
self.connect_mongodb()
|
||
self.connect_mysql()
|
||
|
||
def connect_mongodb(self):
|
||
"""连接MongoDB数据库"""
|
||
try:
|
||
self.mongo_client = pymongo.MongoClient(
|
||
host=MONGO_CONFIG2['host'],
|
||
port=MONGO_CONFIG2['port'],
|
||
username=MONGO_CONFIG2['username'],
|
||
password=MONGO_CONFIG2['password']
|
||
)
|
||
self.db = self.mongo_client[MONGO_CONFIG2['db']]
|
||
self.collection = self.db[self.collection_name]
|
||
|
||
# 测试连接
|
||
self.mongo_client.admin.command('ping')
|
||
logger.info(f"MongoDB连接成功,使用集合: {self.collection_name}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"MongoDB连接失败: {str(e)}")
|
||
raise
|
||
|
||
def connect_mysql(self):
|
||
"""连接MySQL数据库"""
|
||
try:
|
||
self.mysql_engine = create_engine(
|
||
DB_URL,
|
||
pool_size=5,
|
||
max_overflow=10,
|
||
pool_recycle=3600
|
||
)
|
||
|
||
# 测试连接
|
||
with self.mysql_engine.connect() as conn:
|
||
conn.execute(text("SELECT 1"))
|
||
|
||
logger.info("MySQL数据库连接成功")
|
||
|
||
except Exception as e:
|
||
logger.error(f"MySQL数据库连接失败: {str(e)}")
|
||
raise
|
||
|
||
def normalize_stock_code(self, stock_code: str) -> str:
|
||
"""
|
||
标准化股票代码格式,转换为数据库中使用的格式 (如 SZ300661)
|
||
|
||
Args:
|
||
stock_code: 输入的股票代码,支持多种格式:
|
||
- 300661.SZ -> SZ300661
|
||
- 300661 -> SZ300661
|
||
- SZ300661 -> SZ300661 (已是标准格式)
|
||
|
||
Returns:
|
||
str: 标准化后的股票代码
|
||
"""
|
||
stock_code = stock_code.strip().upper()
|
||
|
||
if '.' in stock_code: # 处理 300661.SZ 格式
|
||
parts = stock_code.split('.')
|
||
if len(parts) == 2:
|
||
stock_code = f"{parts[1]}{parts[0]}"
|
||
elif stock_code.isdigit(): # 处理 300661 格式
|
||
if stock_code.startswith(('60', '68')):
|
||
stock_code = f"SH{stock_code}"
|
||
elif stock_code.startswith(('00', '30', '20')):
|
||
stock_code = f"SZ{stock_code}"
|
||
elif stock_code.startswith(('8', '43', '87')):
|
||
stock_code = f"BJ{stock_code}"
|
||
# 如果已经是 SZ300661 格式,则不需要处理
|
||
|
||
return stock_code
|
||
|
||
def get_latest_pe_pb_data(self, stock_code: str) -> Optional[Dict]:
|
||
"""
|
||
从gp_day_data表获取股票最新的PE和PB数据
|
||
|
||
Args:
|
||
stock_code: 股票代码,支持多种格式 (300661.SZ, 300661, SZ300661)
|
||
|
||
Returns:
|
||
Dict: 包含pe和pb的字典,如果没有找到则返回None
|
||
"""
|
||
try:
|
||
# 标准化股票代码格式
|
||
normalized_code = self.code_formatter.to_prefix_format(stock_code)
|
||
|
||
query = text("""
|
||
SELECT pe, pb, `timestamp`
|
||
FROM gp_day_data
|
||
WHERE symbol = :stock_code
|
||
ORDER BY `timestamp` DESC
|
||
LIMIT 1
|
||
""")
|
||
|
||
with self.mysql_engine.connect() as conn:
|
||
result = conn.execute(query, {"stock_code": normalized_code}).fetchone()
|
||
|
||
if result:
|
||
return {
|
||
'pe': float(result[0]) if result[0] is not None else None,
|
||
'pb': float(result[1]) if result[1] is not None else None,
|
||
'timestamp': result[2]
|
||
}
|
||
else:
|
||
logger.warning(f"未找到股票 {stock_code} (标准化后: {normalized_code}) 的PE/PB数据")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取股票 {stock_code} PE/PB数据失败: {str(e)}")
|
||
return None
|
||
|
||
def get_financial_data(self, stock_code: str, report_date: Optional[str] = None) -> Optional[Dict]:
|
||
"""
|
||
获取指定股票的财务数据
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,格式 'YYYY-MM-DD',如果为None则获取最新数据
|
||
|
||
Returns:
|
||
Dict: 财务数据,如果没有找到则返回None
|
||
"""
|
||
try:
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
if report_date:
|
||
# 查询指定日期的数据
|
||
query = {
|
||
'stock_code': formatted_stock_code,
|
||
'report_date': report_date
|
||
}
|
||
data = self.collection.find_one(query)
|
||
else:
|
||
# 查询最新数据
|
||
query = {'stock_code': formatted_stock_code}
|
||
data = self.collection.find_one(
|
||
query,
|
||
sort=[('report_date', -1)]
|
||
)
|
||
|
||
return data
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取财务数据失败 {stock_code} - {report_date}: {str(e)}")
|
||
return None
|
||
|
||
def safe_divide(self, numerator: Union[float, int, None], denominator: Union[float, int, None]) -> Optional[float]:
|
||
"""
|
||
安全除法,处理None值和零除错误
|
||
|
||
Args:
|
||
numerator: 分子
|
||
denominator: 分母
|
||
|
||
Returns:
|
||
float: 计算结果,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
if numerator is None or denominator is None or denominator == 0:
|
||
return None
|
||
return float(numerator) / float(denominator)
|
||
except (TypeError, ValueError, ZeroDivisionError):
|
||
return None
|
||
|
||
# ==================== 盈利能力指标 ====================
|
||
|
||
def analyze_roe(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析净资产收益率(ROE)
|
||
公式: ROE = 归母净利润 / 归母股东权益
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: ROE值(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取归母净利润
|
||
profit = data.get('profit_statement', {}).get('PARENT_NETPROFIT')
|
||
# 获取归母股东权益
|
||
equity = data.get('balance_sheet', {}).get('TOTAL_PARENT_EQUITY')
|
||
|
||
roe = self.safe_divide(profit, equity)
|
||
if roe:
|
||
roe = roe * 100 # 转换为百分比
|
||
|
||
return roe
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析ROE失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_gross_profit_margin(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析毛利率
|
||
公式: 毛利率 = (营业收入 - 营业成本) / 营业收入
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 毛利率(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
fm_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
data = self.get_financial_data(fm_stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取营业收入和营业成本
|
||
revenue = data.get('profit_statement', {}).get('OPERATE_INCOME')
|
||
cost = data.get('profit_statement', {}).get('OPERATE_COST')
|
||
|
||
if revenue and cost:
|
||
gross_profit = revenue - cost
|
||
margin = self.safe_divide(gross_profit, revenue)
|
||
if margin:
|
||
margin = margin * 100 # 转换为百分比
|
||
else:
|
||
margin = None
|
||
|
||
return margin
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析毛利率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_net_profit_margin(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析净利率
|
||
公式: 净利率 = 归母净利润 / 营业收入
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 净利率(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取归母净利润和营业收入
|
||
profit = data.get('profit_statement', {}).get('PARENT_NETPROFIT')
|
||
revenue = data.get('profit_statement', {}).get('OPERATE_INCOME')
|
||
|
||
margin = self.safe_divide(profit, revenue)
|
||
if margin:
|
||
margin = margin * 100 # 转换为百分比
|
||
|
||
return margin
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析净利率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
# ==================== 成长能力指标 ====================
|
||
|
||
def analyze_revenue_growth(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析营业收入增长率(使用YOY数据)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 增长率(%),如果无法获取则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取营业收入同比增长率
|
||
growth_rate = data.get('profit_statement', {}).get('OPERATE_INCOME_YOY')
|
||
|
||
return growth_rate
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析营业收入增长率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_profit_growth(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析净利润增长率(使用YOY数据)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 增长率(%),如果无法获取则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取归母净利润同比增长率
|
||
growth_rate = data.get('profit_statement', {}).get('PARENT_NETPROFIT_YOY')
|
||
|
||
return growth_rate
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析净利润增长率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_total_assets_growth(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析总资产增长率(使用YOY数据)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 增长率(%),如果无法获取则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取总资产同比增长率
|
||
growth_rate = data.get('balance_sheet', {}).get('TOTAL_ASSETS_YOY')
|
||
|
||
return growth_rate
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析总资产增长率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_growth_capability(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析成长能力指标
|
||
公式: 成长能力 = 最新净利润增长率 / 过去8个季度净利润同比变化的标准差
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 成长能力指标,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
# 将股票代码转换为点分格式 (如 430139.BJ)
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
if not formatted_stock_code:
|
||
logger.error(f"无法格式化股票代码: {stock_code}")
|
||
return None
|
||
|
||
# 生成需要查询的季度列表(往前推13个季度)
|
||
# 原因:需要8个季度计算增长率,每个增长率需要去年同期数据(+4),
|
||
# 加上1个季度缓冲:8+4+1=13个季度
|
||
quarters = self._generate_quarters(report_date or "2025-03-31", 13)
|
||
|
||
# 获取多个季度的财务数据
|
||
quarterly_data = []
|
||
for quarter in quarters:
|
||
data = self.get_financial_data(formatted_stock_code, quarter)
|
||
if data:
|
||
profit = data.get('profit_statement', {}).get('PARENT_NETPROFIT')
|
||
quarterly_data.append({
|
||
'quarter': quarter,
|
||
'profit': profit
|
||
})
|
||
|
||
if len(quarterly_data) < 9:
|
||
logger.warning(f"股票 {stock_code} (格式化后: {formatted_stock_code}) 原始季度数据不足({len(quarterly_data)}),无法计算成长能力")
|
||
return None
|
||
|
||
# 处理季度数据的累积性,计算单季度净利润
|
||
processed_data = self._process_quarterly_profits(quarterly_data)
|
||
|
||
if len(processed_data) < 9:
|
||
logger.warning(f"股票 {stock_code} (格式化后: {formatted_stock_code}) 处理后季度数据不足({len(processed_data)})")
|
||
return None
|
||
|
||
# 计算每个季度的同比增长率
|
||
growth_rates = self._calculate_yoy_growth_rates(processed_data)
|
||
|
||
if len(growth_rates) < 8:
|
||
logger.warning(f"股票 {stock_code} (格式化后: {formatted_stock_code}) 同比增长率数据不足")
|
||
return None
|
||
|
||
# 取最新8个季度的增长率计算标准差
|
||
recent_8_quarters = growth_rates[-8:]
|
||
|
||
# 最新的净利润增长率
|
||
latest_growth_rate = recent_8_quarters[-1]
|
||
|
||
# 计算标准差
|
||
std_dev = np.std(recent_8_quarters)
|
||
|
||
if std_dev == 0:
|
||
return None # 避免除零错误
|
||
|
||
# 成长能力 = 净利润增长率 / 标准差
|
||
growth_capability = latest_growth_rate / std_dev
|
||
|
||
return growth_capability
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析成长能力失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def _generate_quarters(self, start_date: str, num_quarters: int) -> List[str]:
|
||
"""
|
||
生成季度列表
|
||
|
||
Args:
|
||
start_date: 起始日期 (YYYY-MM-DD)
|
||
num_quarters: 需要的季度数量
|
||
|
||
Returns:
|
||
List[str]: 季度列表,格式为 YYYY-MM-DD
|
||
"""
|
||
from datetime import datetime, timedelta
|
||
import calendar
|
||
|
||
quarters = []
|
||
year, month, day = map(int, start_date.split('-'))
|
||
|
||
# 确定起始季度
|
||
if month <= 3:
|
||
quarter = 1
|
||
elif month <= 6:
|
||
quarter = 2
|
||
elif month <= 9:
|
||
quarter = 3
|
||
else:
|
||
quarter = 4
|
||
|
||
current_year = year
|
||
current_quarter = quarter
|
||
|
||
for _ in range(num_quarters):
|
||
# 根据季度生成日期
|
||
if current_quarter == 1:
|
||
quarter_date = f"{current_year}-03-31"
|
||
elif current_quarter == 2:
|
||
quarter_date = f"{current_year}-06-30"
|
||
elif current_quarter == 3:
|
||
quarter_date = f"{current_year}-09-30"
|
||
else: # quarter == 4
|
||
quarter_date = f"{current_year}-12-31"
|
||
|
||
quarters.append(quarter_date)
|
||
|
||
# 移动到上一个季度
|
||
current_quarter -= 1
|
||
if current_quarter == 0:
|
||
current_quarter = 4
|
||
current_year -= 1
|
||
|
||
return quarters
|
||
|
||
def _process_quarterly_profits(self, quarterly_data: List[Dict]) -> List[Dict]:
|
||
"""
|
||
处理季度数据的累积性,计算单季度净利润
|
||
|
||
Args:
|
||
quarterly_data: 包含季度和累积利润的数据列表
|
||
|
||
Returns:
|
||
List[Dict]: 处理后的单季度利润数据
|
||
"""
|
||
# 按季度排序(从旧到新)
|
||
quarterly_data.sort(key=lambda x: x['quarter'])
|
||
|
||
processed_data = []
|
||
|
||
# 创建一个字典来快速查找季度数据
|
||
quarter_dict = {data['quarter']: data['profit'] for data in quarterly_data if data['profit'] is not None}
|
||
|
||
for data in quarterly_data:
|
||
quarter = data['quarter']
|
||
cumulative_profit = data['profit']
|
||
|
||
if cumulative_profit is None:
|
||
continue
|
||
|
||
# 判断季度类型
|
||
month = quarter.split('-')[1]
|
||
|
||
if month == '03': # 一季报,直接使用
|
||
single_quarter_profit = cumulative_profit
|
||
else:
|
||
# 需要减去前面季度的累积值
|
||
if month == '06': # 半年报 = 半年报 - 一季报
|
||
prev_quarter = quarter.replace('-06-30', '-03-31')
|
||
elif month == '09': # 三季报 = 三季报 - 半年报
|
||
prev_quarter = quarter.replace('-09-30', '-06-30')
|
||
else: # month == '12', 年报 = 年报 - 三季报
|
||
prev_quarter = quarter.replace('-12-31', '-09-30')
|
||
|
||
# 查找前一个季度的累积利润
|
||
prev_cumulative_profit = quarter_dict.get(prev_quarter)
|
||
|
||
if prev_cumulative_profit is not None:
|
||
single_quarter_profit = cumulative_profit - prev_cumulative_profit
|
||
else:
|
||
# 如果缺少前一季度数据,我们仍然可以尝试使用这个数据
|
||
# 但需要标记它可能不准确
|
||
logger.warning(f"缺少前一季度数据 {prev_quarter},直接使用累积值 {quarter}")
|
||
single_quarter_profit = cumulative_profit
|
||
|
||
processed_data.append({
|
||
'quarter': quarter,
|
||
'single_quarter_profit': single_quarter_profit
|
||
})
|
||
|
||
return processed_data
|
||
|
||
def _calculate_yoy_growth_rates(self, processed_data: List[Dict]) -> List[float]:
|
||
"""
|
||
计算同比增长率
|
||
|
||
Args:
|
||
processed_data: 处理后的单季度利润数据
|
||
|
||
Returns:
|
||
List[float]: 同比增长率列表
|
||
"""
|
||
growth_rates = []
|
||
|
||
# 按季度排序
|
||
processed_data.sort(key=lambda x: x['quarter'])
|
||
|
||
for i in range(4, len(processed_data)): # 从第5个季度开始计算同比
|
||
current_profit = processed_data[i]['single_quarter_profit']
|
||
prev_year_profit = processed_data[i-4]['single_quarter_profit'] # 去年同期
|
||
|
||
if prev_year_profit is not None and prev_year_profit != 0 and current_profit is not None:
|
||
growth_rate = (current_profit - prev_year_profit) / abs(prev_year_profit) * 100
|
||
growth_rates.append(growth_rate)
|
||
|
||
return growth_rates
|
||
|
||
# ==================== 营运效率指标 ====================
|
||
|
||
def analyze_admin_expense_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析管理费用/营业收入占比
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 管理费用率(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
data = self.get_financial_data(formatted_stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取管理费用和营业收入
|
||
admin_expense = data.get('profit_statement', {}).get('MANAGE_EXPENSE')
|
||
revenue = data.get('profit_statement', {}).get('OPERATE_INCOME')
|
||
|
||
ratio = self.safe_divide(admin_expense, revenue)
|
||
if ratio:
|
||
ratio = ratio * 100 # 转换为百分比
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析管理费用率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_rd_expense_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析研发费用/营业收入占比
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 研发费用率(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取研发费用和营业收入
|
||
rd_expense = data.get('profit_statement', {}).get('RESEARCH_EXPENSE')
|
||
revenue = data.get('profit_statement', {}).get('OPERATE_INCOME')
|
||
|
||
ratio = self.safe_divide(rd_expense, revenue)
|
||
if ratio:
|
||
ratio = ratio * 100 # 转换为百分比
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析研发费用率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_sales_expense_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析销售费用/营业收入占比
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 销售费用率(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取销售费用和营业收入
|
||
sales_expense = data.get('profit_statement', {}).get('SALE_EXPENSE')
|
||
revenue = data.get('profit_statement', {}).get('OPERATE_INCOME')
|
||
|
||
ratio = self.safe_divide(sales_expense, revenue)
|
||
if ratio:
|
||
ratio = ratio * 100 # 转换为百分比
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析销售费用率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
# ==================== 规模扩张指标 ====================
|
||
|
||
def analyze_asset_liability_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析资产负债率
|
||
公式: 资产负债率 = 总负债 / 总资产
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 资产负债率(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
data = self.get_financial_data(formatted_stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取总负债和总资产
|
||
total_liabilities = data.get('balance_sheet', {}).get('TOTAL_LIABILITIES')
|
||
total_assets = data.get('balance_sheet', {}).get('TOTAL_ASSETS')
|
||
|
||
ratio = self.safe_divide(total_liabilities, total_assets)
|
||
if ratio:
|
||
ratio = ratio * 100 # 转换为百分比
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析资产负债率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_current_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析流动比率
|
||
公式: 流动比率 = 流动资产 / 流动负债
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 流动比率,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取流动资产和流动负债
|
||
current_assets = data.get('balance_sheet', {}).get('TOTAL_CURRENT_ASSETS')
|
||
current_liabilities = data.get('balance_sheet', {}).get('TOTAL_CURRENT_LIAB')
|
||
|
||
ratio = self.safe_divide(current_assets, current_liabilities)
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析流动比率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_quick_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析速动比率
|
||
公式: 速动比率 = (流动资产 - 存货) / 流动负债
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 速动比率,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取流动资产、存货和流动负债
|
||
current_assets = data.get('balance_sheet', {}).get('TOTAL_CURRENT_ASSETS')
|
||
inventory = data.get('balance_sheet', {}).get('INVENTORY', 0)
|
||
current_liabilities = data.get('balance_sheet', {}).get('TOTAL_CURRENT_LIAB')
|
||
|
||
if current_assets and current_liabilities:
|
||
quick_assets = current_assets - (inventory or 0)
|
||
ratio = self.safe_divide(quick_assets, current_liabilities)
|
||
else:
|
||
ratio = None
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析速动比率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
# ==================== 研发投入指标 ====================
|
||
|
||
def analyze_rd_investment_growth(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析研发费用增长率(使用YOY数据)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 增长率(%),如果无法获取则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取研发费用同比增长率
|
||
growth_rate = data.get('profit_statement', {}).get('RESEARCH_EXPENSE_YOY')
|
||
|
||
return growth_rate
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析研发费用增长率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_rd_intensity(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析研发强度(研发费用/总资产)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 研发强度(%),如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取研发费用和总资产
|
||
rd_expense = data.get('profit_statement', {}).get('RESEARCH_EXPENSE')
|
||
total_assets = data.get('balance_sheet', {}).get('TOTAL_ASSETS')
|
||
|
||
ratio = self.safe_divide(rd_expense, total_assets)
|
||
if ratio:
|
||
ratio = ratio * 100 # 转换为百分比
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析研发强度失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
# ==================== 供应商客户占比指标 ====================
|
||
|
||
def analyze_supplier_concentration(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析前五供应商占比
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 供应商集中度(%),如果无法获取则返回None
|
||
"""
|
||
try:
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
data = self.get_financial_data(formatted_stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取前五供应商占比
|
||
supplier_ratio = data.get('top_five_suppliers_ratio')
|
||
|
||
if supplier_ratio:
|
||
supplier_ratio = supplier_ratio * 100 # 转换为百分比
|
||
|
||
return supplier_ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析供应商集中度失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_customer_concentration(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析前五客户占比
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 客户集中度(%),如果无法获取则返回None
|
||
"""
|
||
try:
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
data = self.get_financial_data(formatted_stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取前五客户占比
|
||
customer_ratio = data.get('top_five_customers_ratio')
|
||
|
||
if customer_ratio:
|
||
customer_ratio = customer_ratio * 100 # 转换为百分比
|
||
|
||
return customer_ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析客户集中度失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
# ==================== 现金流指标 ====================
|
||
|
||
def analyze_operating_cash_flow_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析经营现金流量比率
|
||
公式: 经营现金流量比率 = 经营活动现金流量净额 / 流动负债
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 经营现金流量比率,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取经营活动现金流量净额和流动负债
|
||
operating_cash_flow = data.get('cash_flow_statement', {}).get('NETCASH_OPERATE')
|
||
current_liabilities = data.get('balance_sheet', {}).get('TOTAL_CURRENT_LIAB')
|
||
|
||
ratio = self.safe_divide(operating_cash_flow, current_liabilities)
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析经营现金流量比率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_cash_flow_coverage_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析现金流量覆盖比率
|
||
公式: 现金流量覆盖比率 = 经营活动现金流量净额 / 归母净利润
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: 现金流量覆盖比率,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
data = self.get_financial_data(stock_code, report_date)
|
||
if not data:
|
||
return None
|
||
|
||
# 获取经营活动现金流量净额和归母净利润
|
||
operating_cash_flow = data.get('cash_flow_statement', {}).get('NETCASH_OPERATE')
|
||
net_profit = data.get('profit_statement', {}).get('PARENT_NETPROFIT')
|
||
|
||
ratio = self.safe_divide(operating_cash_flow, net_profit)
|
||
|
||
return ratio
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析现金流量覆盖比率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
# ==================== 估值指标 ====================
|
||
|
||
def analyze_pe_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析PE比率,从gp_day_data表获取最新PE数据
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新(注意:当前只支持获取最新PE数据)
|
||
|
||
Returns:
|
||
float: PE比率,如果无法获取则返回None
|
||
"""
|
||
try:
|
||
pe_pb_data = self.get_latest_pe_pb_data(stock_code)
|
||
if pe_pb_data and pe_pb_data.get('pe') is not None:
|
||
return pe_pb_data['pe']
|
||
else:
|
||
logger.warning(f"未找到股票 {stock_code} 的PE数据")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析PE比率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_pb_ratio(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析PB比率,从gp_day_data表获取最新PB数据
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新(注意:当前只支持获取最新PB数据)
|
||
|
||
Returns:
|
||
float: PB比率,如果无法获取则返回None
|
||
"""
|
||
try:
|
||
pe_pb_data = self.get_latest_pe_pb_data(stock_code)
|
||
if pe_pb_data and pe_pb_data.get('pb') is not None:
|
||
return pe_pb_data['pb']
|
||
else:
|
||
logger.warning(f"未找到股票 {stock_code} 的PB数据")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析PB比率失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def get_all_stocks_pb_data(self) -> Dict[str, float]:
|
||
"""
|
||
获取全A股最新的PB数据
|
||
|
||
Returns:
|
||
Dict[str, float]: {股票代码: PB值} 的字典
|
||
"""
|
||
try:
|
||
query = text("""
|
||
SELECT symbol, pb
|
||
FROM gp_day_data
|
||
WHERE `timestamp` = (
|
||
SELECT MAX(`timestamp`) FROM gp_day_data
|
||
)
|
||
AND pb IS NOT NULL
|
||
AND pb > 0
|
||
""")
|
||
|
||
pb_data = {}
|
||
with self.mysql_engine.connect() as conn:
|
||
result = conn.execute(query)
|
||
for row in result:
|
||
symbol = row[0]
|
||
pb = float(row[1])
|
||
# 转换为点分格式
|
||
formatted_code = self.code_formatter.to_dot_format(symbol)
|
||
if formatted_code:
|
||
pb_data[formatted_code] = pb
|
||
|
||
logger.info(f"获取到 {len(pb_data)} 只股票的PB数据")
|
||
return pb_data
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取全A股PB数据失败: {str(e)}")
|
||
return {}
|
||
|
||
def get_all_stocks_roe_data(self, report_date: Optional[str] = None) -> Dict[str, float]:
|
||
"""
|
||
获取全A股的ROE数据(使用MongoDB聚合查询优化性能)
|
||
|
||
Args:
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
Dict[str, float]: {股票代码: ROE值} 的字典
|
||
"""
|
||
try:
|
||
if report_date:
|
||
# 查询指定日期的数据
|
||
match_stage = {"report_date": report_date}
|
||
else:
|
||
# 查询每只股票的最新数据
|
||
match_stage = {}
|
||
|
||
pipeline = [
|
||
{"$match": match_stage},
|
||
{
|
||
"$sort": {
|
||
"stock_code": 1,
|
||
"report_date": -1
|
||
}
|
||
}
|
||
]
|
||
|
||
# 如果没有指定日期,则获取每只股票的最新数据
|
||
if not report_date:
|
||
pipeline.extend([
|
||
{
|
||
"$group": {
|
||
"_id": "$stock_code",
|
||
"latest_data": {"$first": "$$ROOT"}
|
||
}
|
||
},
|
||
{
|
||
"$replaceRoot": {"newRoot": "$latest_data"}
|
||
}
|
||
])
|
||
|
||
# 添加ROE计算
|
||
pipeline.extend([
|
||
{
|
||
"$addFields": {
|
||
"parent_netprofit": {
|
||
"$toDouble": "$profit_statement.PARENT_NETPROFIT"
|
||
},
|
||
"total_parent_equity": {
|
||
"$toDouble": "$balance_sheet.TOTAL_PARENT_EQUITY"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"$match": {
|
||
"parent_netprofit": {"$ne": None, "$ne": 0},
|
||
"total_parent_equity": {"$ne": None, "$ne": 0}
|
||
}
|
||
},
|
||
{
|
||
"$addFields": {
|
||
"roe": {
|
||
"$multiply": [
|
||
{"$divide": ["$parent_netprofit", "$total_parent_equity"]},
|
||
100
|
||
]
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"$project": {
|
||
"stock_code": 1,
|
||
"roe": 1
|
||
}
|
||
}
|
||
])
|
||
|
||
roe_data = {}
|
||
cursor = self.collection.aggregate(pipeline)
|
||
|
||
for doc in cursor:
|
||
stock_code = doc.get('stock_code')
|
||
roe = doc.get('roe')
|
||
if stock_code and roe is not None:
|
||
roe_data[stock_code] = float(roe)
|
||
|
||
logger.info(f"获取到 {len(roe_data)} 只股票的ROE数据")
|
||
return roe_data
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取全A股ROE数据失败: {str(e)}")
|
||
return {}
|
||
|
||
def calculate_pb_roe_rank_factor(self, stock_code: str, all_pb_data: Dict[str, float], all_roe_data: Dict[str, float]) -> Optional[float]:
|
||
"""
|
||
计算单只股票的PB-ROE排名因子
|
||
公式: Rank(PB_MRQ) - Rank(ROE_TTM)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
all_pb_data: 全A股PB数据字典
|
||
all_roe_data: 全A股ROE数据字典
|
||
|
||
Returns:
|
||
float: PB-ROE排名因子,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
# 标准化股票代码
|
||
formatted_stock_code = self.code_formatter.to_dot_format(stock_code)
|
||
if not formatted_stock_code:
|
||
return None
|
||
|
||
# 计算PB排名(升序,越小越好)
|
||
pb_values = list(all_pb_data.values())
|
||
pb_values.sort()
|
||
target_pb = all_pb_data[formatted_stock_code]
|
||
pb_rank = pb_values.index(target_pb) + 1
|
||
|
||
# 计算ROE排名(降序,越大越好)
|
||
roe_values = list(all_roe_data.values())
|
||
roe_values.sort(reverse=True)
|
||
target_roe = all_roe_data[formatted_stock_code]
|
||
roe_rank = roe_values.index(target_roe) + 1
|
||
|
||
# 计算因子:Rank(PB) - Rank(ROE)
|
||
pb_roe_factor = pb_rank - roe_rank
|
||
|
||
return float(pb_roe_factor)
|
||
|
||
except Exception as e:
|
||
logger.error(f"计算股票 {stock_code} PB-ROE排名因子失败: {str(e)}")
|
||
return None
|
||
|
||
def analyze_pb_roe_rank_factor(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析PB-ROE排名因子(保持向后兼容)
|
||
公式: Rank(PB_MRQ) - Rank(ROE_TTM)
|
||
排名越小越好,因子值越低说明PB估值较低、ROE较高
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: PB-ROE排名因子,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
# 获取全A股数据
|
||
all_pb_data = self.get_all_stocks_pb_data()
|
||
all_roe_data = self.get_all_stocks_roe_data(report_date)
|
||
|
||
if not all_pb_data or not all_roe_data:
|
||
return None
|
||
|
||
return self.calculate_pb_roe_rank_factor(stock_code, all_pb_data, all_roe_data)
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析PB-ROE排名因子失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def analyze_pb_roe(self, stock_code: str, report_date: Optional[str] = None) -> Optional[float]:
|
||
"""
|
||
分析PB-ROE指标(原有方法,保持向后兼容)
|
||
公式: PB/ROE,用于评估股票的价值投资潜力
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
report_date: 报告日期,默认最新
|
||
|
||
Returns:
|
||
float: PB-ROE指标,如果无法计算则返回None
|
||
"""
|
||
try:
|
||
# 获取PB值
|
||
pb = self.analyze_pb_ratio(stock_code, report_date)
|
||
# 获取ROE值
|
||
roe = self.analyze_roe(stock_code, report_date)
|
||
|
||
if pb is not None and roe is not None and roe != 0:
|
||
# ROE以百分比形式返回,需要转换为小数
|
||
roe_decimal = roe / 100
|
||
pb_roe = pb / roe_decimal
|
||
return pb_roe
|
||
else:
|
||
logger.warning(f"无法计算股票 {stock_code} 的PB-ROE指标: PB={pb}, ROE={roe}")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"分析PB-ROE指标失败 {stock_code}: {str(e)}")
|
||
return None
|
||
|
||
def close_connection(self):
|
||
"""关闭数据库连接"""
|
||
try:
|
||
if self.mongo_client:
|
||
self.mongo_client.close()
|
||
logger.info("MongoDB连接已关闭")
|
||
except Exception as e:
|
||
logger.error(f"关闭MongoDB连接失败: {str(e)}")
|
||
|
||
try:
|
||
if self.mysql_engine:
|
||
self.mysql_engine.dispose()
|
||
logger.info("MySQL连接已关闭")
|
||
except Exception as e:
|
||
logger.error(f"关闭MySQL连接失败: {str(e)}")
|
||
|
||
|
||
def main():
|
||
"""主函数 - 示例用法"""
|
||
analyzer = None
|
||
try:
|
||
# 创建分析器实例
|
||
analyzer = FinancialIndicatorAnalyzer()
|
||
|
||
# 示例:分析某只股票的财务指标
|
||
stock_code = "300661.SZ" # 圣邦股份
|
||
|
||
print(f"=== 财务指标分析:{stock_code} ===")
|
||
|
||
# 盈利能力指标
|
||
roe = analyzer.analyze_roe(stock_code)
|
||
print(f"ROE: {roe:.2f}%" if roe else "ROE: 无数据")
|
||
|
||
gross_margin = analyzer.analyze_gross_profit_margin(stock_code)
|
||
print(f"毛利率: {gross_margin:.2f}%" if gross_margin else "毛利率: 无数据")
|
||
|
||
net_margin = analyzer.analyze_net_profit_margin(stock_code)
|
||
print(f"净利率: {net_margin:.2f}%" if net_margin else "净利率: 无数据")
|
||
|
||
# 成长能力指标
|
||
growth_capability = analyzer.analyze_growth_capability(stock_code)
|
||
print(f"成长能力: {growth_capability:.2f}" if growth_capability else "成长能力: 无数据")
|
||
|
||
# 营运效率指标
|
||
admin_ratio = analyzer.analyze_admin_expense_ratio(stock_code)
|
||
print(f"管理费用率: {admin_ratio:.2f}%" if admin_ratio else "管理费用率: 无数据")
|
||
|
||
rd_ratio = analyzer.analyze_rd_expense_ratio(stock_code)
|
||
print(f"研发费用率: {rd_ratio:.2f}%" if rd_ratio else "研发费用率: 无数据")
|
||
|
||
# 规模扩张指标
|
||
asset_liability = analyzer.analyze_asset_liability_ratio(stock_code)
|
||
print(f"资产负债率: {asset_liability:.2f}%" if asset_liability else "资产负债率: 无数据")
|
||
|
||
current_ratio = analyzer.analyze_current_ratio(stock_code)
|
||
print(f"流动比率: {current_ratio:.2f}" if current_ratio else "流动比率: 无数据")
|
||
|
||
# 供应商客户占比指标
|
||
supplier_conc = analyzer.analyze_supplier_concentration(stock_code,'2024-12-31')
|
||
print(f"供应商集中度: {supplier_conc:.2f}%" if supplier_conc else "供应商集中度: 无数据")
|
||
|
||
customer_conc = analyzer.analyze_customer_concentration(stock_code, '2024-12-31')
|
||
print(f"客户集中度: {customer_conc:.2f}%" if customer_conc else "客户集中度: 无数据")
|
||
|
||
# 估值指标
|
||
pe_ratio = analyzer.analyze_pe_ratio(stock_code)
|
||
print(f"PE比率: {pe_ratio:.2f}" if pe_ratio else "PE比率: 无数据")
|
||
|
||
pb_ratio = analyzer.analyze_pb_ratio(stock_code)
|
||
print(f"PB比率: {pb_ratio:.2f}" if pb_ratio else "PB比率: 无数据")
|
||
|
||
pb_roe = analyzer.analyze_pb_roe(stock_code)
|
||
print(f"PB-ROE指标: {pb_roe:.2f}" if pb_roe else "PB-ROE指标: 无数据")
|
||
|
||
except Exception as e:
|
||
logger.error(f"程序执行失败: {str(e)}")
|
||
finally:
|
||
if analyzer:
|
||
analyzer.close_connection()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |