7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
288 lines
12 KiB
Python
288 lines
12 KiB
Python
import pandas as pd
|
|
import numpy as np
|
|
|
|
try:
|
|
from .config import TS_TOKEN, START_DATE, END_DATE, PRECISION_CONFIG
|
|
from .stock_utils import *
|
|
except (ImportError, SystemError):
|
|
from config import TS_TOKEN, START_DATE, END_DATE, PRECISION_CONFIG
|
|
from stock_utils import *
|
|
|
|
from .data_source import get_tushare_pro
|
|
|
|
|
|
class FinanceData:
|
|
def __init__(self, token=None):
|
|
"""
|
|
初始化 Tushare 接口
|
|
:param token: 可选,不再使用(保留兼容)
|
|
"""
|
|
self.pro = get_tushare_pro()
|
|
self.ts_code = None
|
|
self.fin_date = None
|
|
self.unit_factor = 100000000 # 单位换算因子 (元 -> 亿)
|
|
|
|
def get_finance_data(self):
|
|
"""
|
|
获取完整的财务数据并计算指标
|
|
:param ts_code: 股票代码 (如 '002273.SZ')
|
|
:param fin_date: 财报日期 (如 '20220331')
|
|
:return: 包含财务指标的字典
|
|
"""
|
|
|
|
# 获取各类财务数据 (返回 DataFrame)
|
|
balance = self.get_balance()
|
|
pre_balance = self.get_pre_balance()
|
|
income = self.get_income()
|
|
cash = self.get_cashflow()
|
|
balance = balance.where(balance.notna(), 0) # NaN替换为0
|
|
pre_balance = pre_balance.where(pre_balance.notna(), 0) # NaN替换为0
|
|
income = income.where(income.notna(), 0) # NaN替换为0
|
|
cash = cash.where(cash.notna(), 0) # NaN替换为0
|
|
|
|
# 初始化结果字典
|
|
data = {
|
|
'ts_code': self.ts_code,
|
|
'period': self.fin_date,
|
|
'--运营数据--': ''
|
|
}
|
|
|
|
# 运营数据计算
|
|
cash_equ = cash['c_cash_equ_end_period'].iloc[0]
|
|
inventories = balance['inventories'].iloc[0]
|
|
total_assets = balance['total_assets'].iloc[0]
|
|
|
|
# 应收账票 = 应收账款 + 应收票据
|
|
accounts_receiv = float(balance['accounts_receiv'].iloc[0]) + float(balance['notes_receiv'].iloc[0])
|
|
prepayment = float(balance['prepayment'].iloc[0])
|
|
|
|
data.update({
|
|
'现金额-亿': cash_equ / self.unit_factor,
|
|
'现金占比率': cash_equ / total_assets,
|
|
'存货-亿': inventories / self.unit_factor,
|
|
'存货占比率': inventories / total_assets,
|
|
'应收账票-亿': accounts_receiv / self.unit_factor,
|
|
'应收账票占比率': accounts_receiv / total_assets,
|
|
'预付款-亿': prepayment / self.unit_factor,
|
|
'预付款占比率': prepayment / total_assets,
|
|
'运营占比率': (cash_equ + inventories + accounts_receiv + prepayment) / total_assets
|
|
})
|
|
|
|
# 资产分布计算
|
|
fix_assets = float(balance['fix_assets'].iloc[0]) if 'fix_assets' in balance else 0
|
|
intan_assets = float(balance['intan_assets'].iloc[0]) if 'intan_assets' in balance else 0
|
|
lt_eqt_invest = float(balance['lt_eqt_invest'].iloc[0]) if 'lt_eqt_invest' in balance else 0
|
|
|
|
data.update({
|
|
'--资产分布--': '',
|
|
'固定资产-亿': fix_assets / self.unit_factor,
|
|
'固定资产占比率': fix_assets / total_assets,
|
|
'无形资产-亿': intan_assets / self.unit_factor,
|
|
'无形资产占率': intan_assets / total_assets,
|
|
'股权投资-亿': lt_eqt_invest / self.unit_factor,
|
|
'股权投资占比率': lt_eqt_invest / total_assets,
|
|
'投资占比率': (fix_assets + intan_assets + lt_eqt_invest) / total_assets
|
|
})
|
|
|
|
# 负债分布计算
|
|
acct_payable = float(balance['acct_payable'].iloc[0]) if 'acct_payable' in balance else 0
|
|
notes_payable = float(balance['notes_payable'].iloc[0]) if 'notes_payable' in balance else 0
|
|
adv_receipts = float(balance['adv_receipts'].iloc[0]) if 'adv_receipts' in balance else 0
|
|
st_borr = float(balance['st_borr'].iloc[0]) if 'st_borr' in balance else 0
|
|
lt_borr = float(balance['lt_borr'].iloc[0]) if 'lt_borr' in balance else 0
|
|
bond_payable = float(balance['bond_payable'].iloc[0]) if 'bond_payable' in balance else 0
|
|
|
|
biz_liab = acct_payable + notes_payable + adv_receipts
|
|
fin_liab = st_borr + lt_borr + bond_payable
|
|
zcfz = (float(balance['total_cur_liab'].iloc[0]) + float(balance['total_ncl'].iloc[0])) / total_assets
|
|
|
|
data.update({
|
|
'--负债分布--': '',
|
|
'经营负债-亿': biz_liab / self.unit_factor,
|
|
'经营负债占比率': biz_liab / total_assets,
|
|
'金融负债-亿': fin_liab / self.unit_factor,
|
|
'金融负债占比率': fin_liab / total_assets,
|
|
'资产负债率': zcfz
|
|
})
|
|
|
|
# 运营能力计算
|
|
total_days = self.get_total_days()
|
|
oper_cost = income['oper_cost'].iloc[0]
|
|
revenue = income['revenue'].iloc[0]
|
|
|
|
# 存货周转天数 (防除零)
|
|
#avg_inventories = (float(pre_balance.get('inventories', 0)) + inventories) / 2
|
|
avg_inventories = (float(pre_balance['inventories'].iloc[0]) + inventories) / 2
|
|
days_1 = total_days / (oper_cost / avg_inventories) if avg_inventories > 0 else 0
|
|
# 应收周转天数 (防除零)
|
|
avg_receiv = (float(pre_balance['accounts_receiv'].iloc[0]) + accounts_receiv) / 2
|
|
days_2 = total_days / (revenue / avg_receiv) if avg_receiv > 0 else 0
|
|
|
|
data.update({
|
|
'--运营能力--': '',
|
|
'存货周转天数': days_1,
|
|
'应收周转天数': days_2,
|
|
'营业周期': days_1 + days_2
|
|
})
|
|
|
|
# 管理费分布计算
|
|
gross_profit = revenue - oper_cost
|
|
gross_margin = gross_profit / revenue if revenue > 0 else 0
|
|
|
|
data.update({
|
|
'--管理费分布--': '',
|
|
'毛利额': gross_profit / self.unit_factor,
|
|
'毛利率': gross_margin,
|
|
'营业税金率': float(income['biz_tax_surchg'].iloc[0]) / float(revenue) if revenue > 0 else 0,
|
|
'销售费用率': float(income['sell_exp'].iloc[0]) / float(revenue) if revenue > 0 else 0,
|
|
'研发费用率': float(income['rd_exp'].iloc[0]) / float(revenue) if revenue > 0 else 0,
|
|
'管理费用率': float(income['admin_exp'].iloc[0]) / float(revenue) if revenue > 0 else 0,
|
|
'净利润': float(income['n_income'].iloc[0]) / self.unit_factor,
|
|
'净利润率': float(income['n_income'].iloc[0]) / float(revenue) if revenue > 0 else 0
|
|
})
|
|
|
|
# 权益及回报率计算
|
|
data.update({
|
|
'--权益及回报率--': '',
|
|
'总资产-亿': total_assets / self.unit_factor,
|
|
'销售收入-亿': revenue / self.unit_factor,
|
|
'总资产周转率': revenue / total_assets if total_assets > 0 else 0,
|
|
'总资产回报率': float(income['n_income'].iloc[0]) / total_assets if total_assets > 0 else 0,
|
|
'权益乘数': 1 / (1 - zcfz) if zcfz < 1 else 0,
|
|
})
|
|
|
|
# 计算ROE (净资产回报率)
|
|
roa = data['总资产回报率']
|
|
data['净资产回报率'] = roa * data['权益乘数']
|
|
|
|
# 格式化比率数据
|
|
for key in list(data.keys()):
|
|
if isinstance(data[key], float):
|
|
if key.endswith('率'):
|
|
data[key] = f"{data[key] * 100:.2f}%"
|
|
else:
|
|
data[key] = round(data[key], 2)
|
|
|
|
return pd.Series(data)
|
|
|
|
def get_balance(self):
|
|
"""获取资产负债表数据"""
|
|
fields = [
|
|
'ts_code', 'end_date', 'total_assets', 'fix_assets', 'intan_assets',
|
|
'lt_eqt_invest', 'inventories', 'accounts_receiv', 'notes_receiv',
|
|
'prepayment', 'acct_payable', 'notes_payable', 'adv_receipts',
|
|
'st_borr', 'lt_borr', 'bond_payable', 'total_cur_liab', 'total_ncl'
|
|
]
|
|
return self.pro.balancesheet(
|
|
ts_code=self.ts_code,
|
|
period=self.fin_date,
|
|
fields=fields
|
|
)
|
|
|
|
def get_pre_balance(self):
|
|
"""获取上年度资产负债表数据"""
|
|
pre_date = f"{int(self.fin_date[:4]) - 1}1231" # 上年末日期
|
|
fields = ['inventories', 'accounts_receiv', 'notes_receiv']
|
|
return self.pro.balancesheet(
|
|
ts_code=self.ts_code,
|
|
period=pre_date,
|
|
fields=fields
|
|
) # 直接返回Series
|
|
|
|
def get_income(self):
|
|
"""获取利润表数据"""
|
|
fields = [
|
|
'revenue', 'oper_cost', 'biz_tax_surchg', 'sell_exp', 'fin_exp',
|
|
'admin_exp', 'n_income', 'rd_exp'
|
|
]
|
|
return self.pro.income(
|
|
ts_code=self.ts_code,
|
|
period=self.fin_date,
|
|
fields=fields
|
|
)
|
|
|
|
def get_cashflow(self):
|
|
"""获取现金流量表数据"""
|
|
#c_cash_equ_end_period 期末现金及现金等价物余额
|
|
#end_bal_cash 现金的期末余额
|
|
fields = ['c_cash_equ_end_period', 'end_bal_cash']
|
|
return self.pro.cashflow(
|
|
ts_code=self.ts_code,
|
|
period=self.fin_date,
|
|
fields=fields
|
|
)
|
|
|
|
def get_total_days(self):
|
|
"""根据财报类型返回计算周转率的天数"""
|
|
quarter = self.fin_date[4:6]
|
|
return {
|
|
'03': 90, # Q1
|
|
'06': 180, # H1
|
|
'09': 270, # Q1-Q3
|
|
'12': 360 # 全年
|
|
}.get(quarter, 360)
|
|
|
|
#循环调用类
|
|
def get_finance_data_range( ts_code, start_date=START_DATE, end_date=END_DATE,TS_TOKEN=TS_TOKEN):
|
|
"""
|
|
获取指定日期范围内的所有财报数据
|
|
:param ts_code: 股票代码
|
|
:param start_date: 开始日期 (yyyyMMdd)
|
|
:param end_date: 结束日期 (yyyyMMdd)
|
|
:return: 合并后的财报数据列表
|
|
"""
|
|
results = pd.DataFrame()
|
|
|
|
# 生成所有可能的财报日期 (季度末)
|
|
years = range(int(start_date[:4]), int(end_date[:4]) + 1)
|
|
report_dates = []
|
|
for year in years:
|
|
report_dates.extend([
|
|
f"{year}0331", # Q1
|
|
f"{year}0630", # Q2
|
|
f"{year}0930", # Q3
|
|
f"{year}1231" # Q4
|
|
])
|
|
|
|
# 筛选在指定日期范围内的财报日期
|
|
report_dates = [
|
|
date for date in report_dates
|
|
if start_date <= date <= end_date
|
|
]
|
|
report_dates=get_released_report_dates(start_date,end_date)
|
|
|
|
# 按日期顺序获取财报数据
|
|
analyzer = FinanceData(TS_TOKEN)
|
|
analyzer.ts_code=tscodeCheck(ts_code)
|
|
for fin_date in sorted(report_dates):
|
|
analyzer.fin_date = fin_date
|
|
try:
|
|
data = analyzer.get_finance_data()
|
|
results = pd.concat([pd.DataFrame(results), data.to_frame().T], ignore_index=True) if len(results) > 0 else data.to_frame().T
|
|
except Exception as e:
|
|
print(f"获取 {ts_code} {fin_date} 财报数据失败: {str(e)}")
|
|
continue
|
|
|
|
return results
|
|
# 使用示例
|
|
if __name__ == "__main__":
|
|
# 导入当站目录的config文件
|
|
try:
|
|
# 尝试相对导入(作为包的一部分)
|
|
from .config import TS_TOKEN, START_DATE, END_DATE, PRECISION_CONFIG
|
|
except (ImportError, SystemError):
|
|
# 失败则使用绝对导入(直接运行脚本)
|
|
from config import TS_TOKEN, START_DATE, END_DATE, PRECISION_CONFIG
|
|
#token = "your_tushare_token" # 替换为实际token
|
|
#analyzer = FinanceData(TS_TOKEN)
|
|
|
|
# 获取002273.SZ在2022Q1的财务数据
|
|
#analyzer.ts_code = '300316.SZ'
|
|
#analyzer.fin_date = '20240630'
|
|
|
|
|
|
#result = analyzer.get_finance_data()
|
|
|
|
result=get_finance_data_range("300316",start_date='20200101',end_date='2025060')
|
|
print(result)
|
|
#print(pd.Series(result)) |