587 lines
24 KiB
Python
587 lines
24 KiB
Python
"""
|
||
季报数据获取模块
|
||
使用akshare获取个股最近8期季度财务报告
|
||
"""
|
||
|
||
import pandas as pd
|
||
import sys
|
||
import io
|
||
import warnings
|
||
from datetime import datetime
|
||
import akshare as ak
|
||
from http_timeout import call_with_timeout
|
||
from data_source_manager import data_source_manager
|
||
|
||
warnings.filterwarnings('ignore')
|
||
|
||
# 设置标准输出编码为UTF-8(仅在命令行环境,避免streamlit冲突)
|
||
def _setup_stdout_encoding():
|
||
"""仅在命令行环境设置标准输出编码"""
|
||
if sys.platform == 'win32' and not hasattr(sys.stdout, '_original_stream'):
|
||
try:
|
||
# 检测是否在streamlit环境中
|
||
import streamlit
|
||
# 在streamlit中不修改stdout
|
||
return
|
||
except ImportError:
|
||
# 不在streamlit环境,可以安全修改
|
||
try:
|
||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='ignore')
|
||
except:
|
||
pass
|
||
|
||
_setup_stdout_encoding()
|
||
|
||
|
||
class QuarterlyReportDataFetcher:
|
||
"""季报数据获取类(使用akshare数据源)"""
|
||
|
||
def __init__(self):
|
||
self.periods = 8 # 获取最近8期季报
|
||
self.available = True
|
||
print("✓ 季报数据获取器初始化成功(akshare数据源)")
|
||
|
||
def get_quarterly_reports(self, symbol):
|
||
"""
|
||
获取股票的季报数据
|
||
|
||
Args:
|
||
symbol: 股票代码(6位数字)
|
||
|
||
Returns:
|
||
dict: 包含季报数据的字典
|
||
"""
|
||
data = {
|
||
"symbol": symbol,
|
||
"income_statement": None, # 利润表
|
||
"balance_sheet": None, # 资产负债表
|
||
"cash_flow": None, # 现金流量表
|
||
"financial_indicators": None, # 财务指标
|
||
"data_success": False,
|
||
"source": "akshare"
|
||
}
|
||
|
||
# 只支持中国股票
|
||
if not self._is_chinese_stock(symbol):
|
||
data["error"] = "季报数据仅支持中国A股股票"
|
||
return data
|
||
|
||
try:
|
||
print(f"📊 正在获取 {symbol} 的季报数据...")
|
||
|
||
# 获取利润表(优先tushare,失败时回退akshare)
|
||
income_data = self._get_income_statement_from_tushare(symbol)
|
||
if income_data is None:
|
||
income_data = self._get_income_statement(symbol)
|
||
if income_data:
|
||
data["income_statement"] = income_data
|
||
print(f" ✓ 成功获取 {len(income_data.get('data', []))} 期利润表数据")
|
||
|
||
# 获取资产负债表(优先tushare,失败时回退akshare)
|
||
balance_data = self._get_balance_sheet_from_tushare(symbol)
|
||
if balance_data is None:
|
||
balance_data = self._get_balance_sheet(symbol)
|
||
if balance_data:
|
||
data["balance_sheet"] = balance_data
|
||
print(f" ✓ 成功获取 {len(balance_data.get('data', []))} 期资产负债表数据")
|
||
|
||
# 获取现金流量表(优先tushare,失败时回退akshare)
|
||
cash_flow_data = self._get_cash_flow_from_tushare(symbol)
|
||
if cash_flow_data is None:
|
||
cash_flow_data = self._get_cash_flow(symbol)
|
||
if cash_flow_data:
|
||
data["cash_flow"] = cash_flow_data
|
||
print(f" ✓ 成功获取 {len(cash_flow_data.get('data', []))} 期现金流量表数据")
|
||
|
||
# 获取财务指标(优先tushare,失败时回退akshare)
|
||
indicators_data = self._get_financial_indicators_from_tushare(symbol)
|
||
if indicators_data is None:
|
||
indicators_data = self._get_financial_indicators(symbol)
|
||
if indicators_data:
|
||
data["financial_indicators"] = indicators_data
|
||
print(f" ✓ 成功获取 {len(indicators_data.get('data', []))} 期财务指标数据")
|
||
|
||
# 如果至少有一个成功,则标记为成功
|
||
if income_data or balance_data or cash_flow_data or indicators_data:
|
||
data["data_success"] = True
|
||
print("✅ 季报数据获取完成")
|
||
else:
|
||
print("⚠️ 未能获取到季报数据")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 获取季报数据失败: {e}")
|
||
data["error"] = str(e)
|
||
|
||
return data
|
||
|
||
def _is_chinese_stock(self, symbol):
|
||
"""判断是否为中国股票"""
|
||
return symbol.isdigit() and len(symbol) == 6
|
||
|
||
def _convert_ts_records(self, df, field_map, periods):
|
||
"""将tushare返回的财务表转换为统一的记录结构"""
|
||
try:
|
||
data_list = []
|
||
for _, row in df.head(periods).iterrows():
|
||
item = {}
|
||
for ch_name, ts_name in field_map.items():
|
||
if ts_name in df.columns:
|
||
value = row.get(ts_name)
|
||
if value is None or (isinstance(value, float) and pd.isna(value)):
|
||
continue
|
||
try:
|
||
item[ch_name] = str(value)
|
||
except:
|
||
item[ch_name] = "N/A"
|
||
if item:
|
||
data_list.append(item)
|
||
return {
|
||
"data": data_list,
|
||
"periods": len(data_list),
|
||
"columns": list(field_map.keys()),
|
||
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
except Exception as e:
|
||
print(f" 转换tushare财务数据异常: {e}")
|
||
return None
|
||
|
||
def _get_income_statement_from_tushare(self, symbol):
|
||
"""从tushare获取利润表(优先数据源)"""
|
||
try:
|
||
if not data_source_manager.tushare_available:
|
||
return None
|
||
ts_code = data_source_manager._convert_to_ts_code(symbol)
|
||
df = data_source_manager.tushare_api.income(ts_code=ts_code)
|
||
if df is None or df.empty:
|
||
return None
|
||
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
|
||
field_map = {
|
||
'报告期': 'end_date',
|
||
'营业总收入': 'total_revenue',
|
||
'营业收入': 'revenue',
|
||
'营业总成本': 'total_operate_cost',
|
||
'营业利润': 'operate_profit',
|
||
'利润总额': 'total_profit',
|
||
'净利润': 'n_income',
|
||
'归属于母公司所有者的净利润': 'n_income_attr_p',
|
||
'基本每股收益': 'basic_eps',
|
||
'稀释每股收益': 'diluted_eps',
|
||
'销售费用': 'sell_exp',
|
||
'管理费用': 'admin_exp',
|
||
'财务费用': 'fin_exp',
|
||
'研发费用': 'rd_exp',
|
||
}
|
||
result = self._convert_ts_records(df, field_map, self.periods)
|
||
if result and result.get('periods'):
|
||
print(f" ✓ tushare成功获取 {result['periods']} 期利润表数据")
|
||
return result
|
||
except Exception as e:
|
||
print(f" tushare获取利润表异常: {e}")
|
||
return None
|
||
|
||
def _get_balance_sheet_from_tushare(self, symbol):
|
||
"""从tushare获取资产负债表(优先数据源)"""
|
||
try:
|
||
if not data_source_manager.tushare_available:
|
||
return None
|
||
ts_code = data_source_manager._convert_to_ts_code(symbol)
|
||
df = data_source_manager.tushare_api.balancesheet(ts_code=ts_code)
|
||
if df is None or df.empty:
|
||
return None
|
||
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
|
||
field_map = {
|
||
'报告期': 'end_date',
|
||
'资产总计': 'total_assets',
|
||
'流动资产合计': 'total_cur_assets',
|
||
'非流动资产合计': 'total_ncur_assets',
|
||
'负债合计': 'total_liab',
|
||
'流动负债合计': 'total_cur_liab',
|
||
'非流动负债合计': 'total_ncur_liab',
|
||
'所有者权益合计': 'total_hldr_eqy_inc_min_int',
|
||
'归属于母公司股东权益合计': 'total_hldr_eqy_exc_min_int',
|
||
}
|
||
result = self._convert_ts_records(df, field_map, self.periods)
|
||
if result and result.get('periods'):
|
||
print(f" ✓ tushare成功获取 {result['periods']} 期资产负债表数据")
|
||
return result
|
||
except Exception as e:
|
||
print(f" tushare获取资产负债表异常: {e}")
|
||
return None
|
||
|
||
def _get_cash_flow_from_tushare(self, symbol):
|
||
"""从tushare获取现金流量表(优先数据源)"""
|
||
try:
|
||
if not data_source_manager.tushare_available:
|
||
return None
|
||
ts_code = data_source_manager._convert_to_ts_code(symbol)
|
||
df = data_source_manager.tushare_api.cashflow(ts_code=ts_code)
|
||
if df is None or df.empty:
|
||
return None
|
||
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
|
||
field_map = {
|
||
'报告期': 'end_date',
|
||
'经营活动产生的现金流量净额': 'n_cashflow_act',
|
||
'投资活动产生的现金流量净额': 'n_cashflow_inv_act',
|
||
'筹资活动产生的现金流量净额': 'n_cash_flows_fnc_act',
|
||
}
|
||
result = self._convert_ts_records(df, field_map, self.periods)
|
||
if result and result.get('periods'):
|
||
print(f" ✓ tushare成功获取 {result['periods']} 期现金流量表数据")
|
||
return result
|
||
except Exception as e:
|
||
print(f" tushare获取现金流量表异常: {e}")
|
||
return None
|
||
|
||
def _get_financial_indicators_from_tushare(self, symbol):
|
||
"""从tushare获取财务指标(优先数据源)"""
|
||
try:
|
||
if not data_source_manager.tushare_available:
|
||
return None
|
||
ts_code = data_source_manager._convert_to_ts_code(symbol)
|
||
df = data_source_manager.tushare_api.fina_indicator(ts_code=ts_code)
|
||
if df is None or df.empty:
|
||
return None
|
||
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
|
||
field_map = {
|
||
'报告期': 'end_date',
|
||
'净资产收益率': 'roe',
|
||
'总资产净利率': 'roa',
|
||
'销售净利率': 'netprofit_margin',
|
||
'销售毛利率': 'grossprofit_margin',
|
||
'资产负债率': 'debt_to_assets',
|
||
'流动比率': 'current_ratio',
|
||
'速动比率': 'quick_ratio',
|
||
'应收账款周转率': 'ar_turnover',
|
||
'存货周转率': 'inventory_turnover',
|
||
'总资产周转率': 'assets_turnover',
|
||
'每股收益': 'eps',
|
||
'每股净资产': 'bps',
|
||
'每股经营现金流': 'cfps',
|
||
}
|
||
result = self._convert_ts_records(df, field_map, self.periods)
|
||
if result and result.get('periods'):
|
||
print(f" ✓ tushare成功获取 {result['periods']} 期财务指标数据")
|
||
return result
|
||
except Exception as e:
|
||
print(f" tushare获取财务指标异常: {e}")
|
||
return None
|
||
|
||
def _get_income_statement(self, symbol):
|
||
"""获取利润表数据"""
|
||
try:
|
||
# stock_financial_report_sina - 新浪财经季度利润表
|
||
df = call_with_timeout(ak.stock_financial_report_sina, timeout=25,
|
||
stock=symbol, symbol="利润表")
|
||
|
||
if df is None or df.empty:
|
||
print(f" 未找到利润表数据")
|
||
return None
|
||
|
||
# 获取最近8期
|
||
df = df.head(self.periods)
|
||
|
||
# 转换为字典列表
|
||
data_list = []
|
||
for idx, row in df.iterrows():
|
||
item = {}
|
||
for col in df.columns:
|
||
value = row.get(col)
|
||
if value is None or (isinstance(value, float) and pd.isna(value)):
|
||
continue
|
||
try:
|
||
item[col] = str(value)
|
||
except:
|
||
item[col] = "N/A"
|
||
if item:
|
||
data_list.append(item)
|
||
|
||
return {
|
||
"data": data_list,
|
||
"periods": len(data_list),
|
||
"columns": df.columns.tolist(),
|
||
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f" 获取利润表异常: {e}")
|
||
return None
|
||
|
||
def _get_balance_sheet(self, symbol):
|
||
"""获取资产负债表数据"""
|
||
try:
|
||
# stock_financial_report_sina - 新浪财经季度资产负债表
|
||
df = call_with_timeout(ak.stock_financial_report_sina, timeout=25,
|
||
stock=symbol, symbol="资产负债表")
|
||
|
||
if df is None or df.empty:
|
||
print(f" 未找到资产负债表数据")
|
||
return None
|
||
|
||
# 获取最近8期
|
||
df = df.head(self.periods)
|
||
|
||
# 转换为字典列表
|
||
data_list = []
|
||
for idx, row in df.iterrows():
|
||
item = {}
|
||
for col in df.columns:
|
||
value = row.get(col)
|
||
if value is None or (isinstance(value, float) and pd.isna(value)):
|
||
continue
|
||
try:
|
||
item[col] = str(value)
|
||
except:
|
||
item[col] = "N/A"
|
||
if item:
|
||
data_list.append(item)
|
||
|
||
return {
|
||
"data": data_list,
|
||
"periods": len(data_list),
|
||
"columns": df.columns.tolist(),
|
||
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f" 获取资产负债表异常: {e}")
|
||
return None
|
||
|
||
def _get_cash_flow(self, symbol):
|
||
"""获取现金流量表数据"""
|
||
try:
|
||
# stock_financial_report_sina - 新浪财经季度现金流量表
|
||
df = call_with_timeout(ak.stock_financial_report_sina, timeout=25,
|
||
stock=symbol, symbol="现金流量表")
|
||
|
||
if df is None or df.empty:
|
||
print(f" 未找到现金流量表数据")
|
||
return None
|
||
|
||
# 获取最近8期
|
||
df = df.head(self.periods)
|
||
|
||
# 转换为字典列表
|
||
data_list = []
|
||
for idx, row in df.iterrows():
|
||
item = {}
|
||
for col in df.columns:
|
||
value = row.get(col)
|
||
if value is None or (isinstance(value, float) and pd.isna(value)):
|
||
continue
|
||
try:
|
||
item[col] = str(value)
|
||
except:
|
||
item[col] = "N/A"
|
||
if item:
|
||
data_list.append(item)
|
||
|
||
return {
|
||
"data": data_list,
|
||
"periods": len(data_list),
|
||
"columns": df.columns.tolist(),
|
||
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f" 获取现金流量表异常: {e}")
|
||
return None
|
||
|
||
def _get_financial_indicators(self, symbol):
|
||
"""获取财务指标数据"""
|
||
try:
|
||
# 使用stock_financial_abstract替代已失效的stock_financial_analysis_indicator
|
||
df = call_with_timeout(ak.stock_financial_abstract, timeout=25, symbol=symbol)
|
||
|
||
if df is None or df.empty:
|
||
print(f" 未找到财务指标数据")
|
||
return None
|
||
|
||
# 获取最近8期
|
||
df = df.head(self.periods * 2) # 取更多数据以确保有足够的季度数据
|
||
|
||
# 提取关键财务指标
|
||
key_indicators = [
|
||
'净资产收益率(ROE)', '总资产报酬率(ROA)', '销售净利率', '销售毛利率',
|
||
'资产负债率', '流动比率', '速动比率', '应收账款周转率', '存货周转率',
|
||
'总资产周转率', '基本每股收益', '每股净资产', '每股现金流'
|
||
]
|
||
|
||
# 筛选出包含关键指标的行
|
||
indicator_rows = df[df['指标'].isin(key_indicators)]
|
||
|
||
if indicator_rows.empty:
|
||
print(f" 未找到关键财务指标数据")
|
||
return None
|
||
|
||
# 获取日期列(排除'选项'和'指标'列)
|
||
date_columns = [col for col in df.columns if col not in ['选项', '指标']]
|
||
|
||
# 转换为字典列表,每个字典代表一个时期的财务指标
|
||
data_list = []
|
||
for date_col in date_columns[:self.periods]: # 只取最近的periods期
|
||
item = {'报告期': date_col}
|
||
for _, row in indicator_rows.iterrows():
|
||
indicator_name = row['指标']
|
||
value = row.get(date_col)
|
||
if value is not None and not (isinstance(value, float) and pd.isna(value)):
|
||
try:
|
||
# 尝试转换为字符串
|
||
item[indicator_name] = str(value)
|
||
except:
|
||
item[indicator_name] = "N/A"
|
||
else:
|
||
item[indicator_name] = "N/A"
|
||
data_list.append(item)
|
||
|
||
return {
|
||
"data": data_list,
|
||
"periods": len(data_list),
|
||
"columns": ['报告期'] + key_indicators,
|
||
"query_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f" 获取财务指标异常: {e}")
|
||
return None
|
||
|
||
def format_quarterly_reports_for_ai(self, data):
|
||
"""
|
||
将季报数据格式化为适合AI阅读的文本
|
||
"""
|
||
if not data or not data.get("data_success"):
|
||
return "未能获取季报数据"
|
||
|
||
text_parts = []
|
||
text_parts.append(f"""
|
||
【季度财务报告数据 - tushare/akshare自动切换】
|
||
股票代码:{data.get('symbol', 'N/A')}
|
||
数据期数:最近{self.periods}期季报
|
||
|
||
""")
|
||
|
||
# 利润表数据
|
||
if data.get("income_statement"):
|
||
income_data = data["income_statement"]
|
||
text_parts.append(f"""
|
||
═══════════════════════════════════════
|
||
📊 利润表(最近{income_data.get('periods', 0)}期)
|
||
═══════════════════════════════════════
|
||
""")
|
||
|
||
# 提取关键指标
|
||
key_fields = ['报告期', '营业总收入', '营业收入', '营业总成本', '营业利润',
|
||
'利润总额', '净利润', '归属于母公司所有者的净利润',
|
||
'基本每股收益', '稀释每股收益']
|
||
|
||
for idx, item in enumerate(income_data.get('data', []), 1):
|
||
text_parts.append(f"\n第 {idx} 期:")
|
||
for field in key_fields:
|
||
if field in item:
|
||
text_parts.append(f" {field}: {item[field]}")
|
||
|
||
# 显示其他重要字段(如果有)
|
||
other_fields = ['销售费用', '管理费用', '财务费用', '研发费用']
|
||
for field in other_fields:
|
||
if field in item:
|
||
text_parts.append(f" {field}: {item[field]}")
|
||
|
||
# 资产负债表数据
|
||
if data.get("balance_sheet"):
|
||
balance_data = data["balance_sheet"]
|
||
text_parts.append(f"""
|
||
|
||
═══════════════════════════════════════
|
||
📊 资产负债表(最近{balance_data.get('periods', 0)}期)
|
||
═══════════════════════════════════════
|
||
""")
|
||
|
||
# 提取关键指标
|
||
key_fields = ['报告期', '资产总计', '流动资产合计', '非流动资产合计',
|
||
'负债合计', '流动负债合计', '非流动负债合计',
|
||
'所有者权益合计', '归属于母公司股东权益合计']
|
||
|
||
for idx, item in enumerate(balance_data.get('data', []), 1):
|
||
text_parts.append(f"\n第 {idx} 期:")
|
||
for field in key_fields:
|
||
if field in item:
|
||
text_parts.append(f" {field}: {item[field]}")
|
||
|
||
# 现金流量表数据
|
||
if data.get("cash_flow"):
|
||
cash_flow_data = data["cash_flow"]
|
||
text_parts.append(f"""
|
||
|
||
═══════════════════════════════════════
|
||
📊 现金流量表(最近{cash_flow_data.get('periods', 0)}期)
|
||
═══════════════════════════════════════
|
||
""")
|
||
|
||
# 提取关键指标
|
||
key_fields = ['报告期', '经营活动产生的现金流量净额',
|
||
'投资活动产生的现金流量净额', '筹资活动产生的现金流量净额',
|
||
'现金及现金等价物净增加额', '期末现金及现金等价物余额']
|
||
|
||
for idx, item in enumerate(cash_flow_data.get('data', []), 1):
|
||
text_parts.append(f"\n第 {idx} 期:")
|
||
for field in key_fields:
|
||
if field in item:
|
||
text_parts.append(f" {field}: {item[field]}")
|
||
|
||
# 财务指标数据
|
||
if data.get("financial_indicators"):
|
||
indicators_data = data["financial_indicators"]
|
||
text_parts.append(f"""
|
||
|
||
═══════════════════════════════════════
|
||
📊 关键财务指标(最近{indicators_data.get('periods', 0)}期)
|
||
═══════════════════════════════════════
|
||
""")
|
||
|
||
# 提取关键指标
|
||
key_fields = ['报告期', '净资产收益率', '总资产净利率', '销售净利率',
|
||
'销售毛利率', '资产负债率', '流动比率', '速动比率',
|
||
'应收账款周转率', '存货周转率', '总资产周转率',
|
||
'每股收益', '每股净资产', '每股经营现金流']
|
||
|
||
for idx, item in enumerate(indicators_data.get('data', []), 1):
|
||
text_parts.append(f"\n第 {idx} 期:")
|
||
for field in key_fields:
|
||
if field in item:
|
||
text_parts.append(f" {field}: {item[field]}")
|
||
|
||
return "\n".join(text_parts)
|
||
|
||
|
||
# 测试函数
|
||
if __name__ == "__main__":
|
||
print("测试季报数据获取(akshare数据源)...")
|
||
print("="*60)
|
||
|
||
fetcher = QuarterlyReportDataFetcher()
|
||
|
||
if not fetcher.available:
|
||
print("❌ 季报数据获取器不可用")
|
||
sys.exit(1)
|
||
|
||
# 测试股票
|
||
test_symbols = ["000001", "600519"] # 平安银行、贵州茅台
|
||
|
||
for symbol in test_symbols:
|
||
print(f"\n{'='*60}")
|
||
print(f"正在测试股票: {symbol}")
|
||
print(f"{'='*60}\n")
|
||
|
||
data = fetcher.get_quarterly_reports(symbol)
|
||
|
||
if data.get("data_success"):
|
||
print("\n" + "="*60)
|
||
print("季报数据获取成功!")
|
||
print("="*60)
|
||
|
||
formatted_text = fetcher.format_quarterly_reports_for_ai(data)
|
||
print(formatted_text)
|
||
else:
|
||
print(f"\n获取失败: {data.get('error', '未知错误')}")
|
||
|
||
print("\n")
|