#!/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()