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>
223 lines
11 KiB
Python
223 lines
11 KiB
Python
import pandas as pd
|
|
import numpy as np
|
|
from datetime import datetime
|
|
|
|
try:
|
|
from .config import TS_TOKEN, START_DATE, END_DATE, PRECISION_CONFIG
|
|
from .stock_utils import *
|
|
from .smoothBrush import smooth_dataframe_brush
|
|
except (ImportError, SystemError):
|
|
from config import TS_TOKEN, START_DATE, END_DATE, PRECISION_CONFIG
|
|
from stock_utils import *
|
|
from smoothBrush import smooth_dataframe_brush
|
|
|
|
from .data_source import get_tushare_pro
|
|
|
|
pro = get_tushare_pro()
|
|
'''
|
|
给定个股代码和起止日期:
|
|
1. 调用tushare接口查询个股分红数据,接口文档:https://tushare.pro/document/2?doc_id=103 查询日期范围内的所有分红记录
|
|
2. 返回调整后的分红记录,包含以下字段:
|
|
- ts_code: 股票代码
|
|
- end_date: 分红年度
|
|
- ann_date: 公告日期
|
|
- ex_date: 除权除息日
|
|
- cash_div_tax: 每股现金分红(含税)
|
|
- div_proc: 实施进度,仅筛选 div_proc='实施'的记录
|
|
3. 调用tushare 的trade_cal接口,查询起止日期内的所有交易日
|
|
4. 建一个新的df,在起止日期内填充,规则:
|
|
- 下列运算过程中将起始时间往前推 gap_days,赋值360天。运算结束后,截取起止时间内的数据
|
|
- trade_date: 以交易日历为准,填充所有交易日。
|
|
- 填入当ex_date=trade_date的日期,填入:ex_date,cash_div_tax,其余日期ex_date留空,cash_div_tax 置 0
|
|
- cash_div_year:逐行计算填入,以trade_date往前计算过去gap_days天内的cash_div_tax之和
|
|
5. 调用tushare的个股日线行情接口:https://tushare.pro/document/2?doc_id=27,查询起止日期内的个股日线行情数据,包含以下字段:
|
|
- ts_code: 股票代码
|
|
- trade_date: 交易日期
|
|
- close: 收盘价
|
|
6. 将分红数据和日线行情数据按交易日期合并,得到最终结果,包含以下字段:
|
|
- ts_code: 股票代码
|
|
- trade_date: 交易日期
|
|
- close: 收盘价
|
|
- cach_div_tax: 每股现金分红(含税)
|
|
- cach_div_year: 每股现金TTM年度分红(含税)
|
|
- div_yield: 股息率,计算公式为 (cach_div_year / close) * 100,保留PRECISION_CONFIG位小数,如果cash_div_year为0则div_yield也为0
|
|
7. 返回最终结果的DataFrame
|
|
'''
|
|
def analyze_stock_dividend_and_price(ts_code, start_date=START_DATE, end_date=END_DATE):
|
|
# 检查日期范围是否超过当前日期,若超过则调整为当前日期
|
|
start_date=date_format_correction(start_date)
|
|
end_date=date_format_correction(end_date)
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
if end_date > today: end_date = today
|
|
if start_date > today: start_date = today
|
|
|
|
GAP_DAYS = 360 # 定义TTM计算窗口期为360天
|
|
n = 15 # 定义向前填充的最大非零值个数
|
|
"""
|
|
分析个股分红与行情数据。
|
|
|
|
参数:
|
|
ts_code (str): 股票代码,例如 '000001.SZ'。
|
|
start_date (str): 起始日期,格式 'YYYYMMDD'。
|
|
end_date (str): 结束日期,格式 'YYYYMMDD'。
|
|
|
|
返回:
|
|
pd.DataFrame: 包含合并后数据的DataFrame。
|
|
"""
|
|
|
|
# 1. 调用tushare接口查询个股分红数据
|
|
try:
|
|
df_div_raw = pro.dividend(ts_code=ts_code)
|
|
# 2. 筛选并调整分红记录
|
|
# 筛选实施进度为'实施'的记录,并在指定日期范围内
|
|
df_div_filtered = df_div_raw[
|
|
(df_div_raw['div_proc'] == '实施')
|
|
].copy()
|
|
|
|
df_div_adjusted = df_div_filtered[[
|
|
'ts_code', 'end_date', 'ann_date', 'ex_date', 'cash_div_tax'
|
|
]].reset_index(drop=True)
|
|
|
|
except Exception as e:
|
|
print(f"获取或处理分红数据时出错: {e}")
|
|
return pd.DataFrame() # 返回空DataFrame
|
|
|
|
# 3. 调用tushare 的trade_cal接口,查询交易日
|
|
try:
|
|
# 运算时起始时间往前推 GAP_DAYS
|
|
calc_start_date = pd.to_datetime(start_date) - pd.Timedelta(days=GAP_DAYS)
|
|
calc_start_date_str = calc_start_date.strftime('%Y%m%d')
|
|
|
|
df_cal = pro.trade_cal(exchange='', start_date=calc_start_date_str, end_date=end_date)
|
|
# 筛选交易日
|
|
trade_dates_all = df_cal[df_cal['is_open'] == 1]['cal_date'].sort_values().tolist()
|
|
|
|
except Exception as e:
|
|
print(f"获取交易日历时出错: {e}")
|
|
return pd.DataFrame()
|
|
|
|
# 4. 建立新df并填充
|
|
df_div_processed = pd.DataFrame({'trade_date': trade_dates_all})
|
|
df_div_processed['trade_date'] = pd.to_datetime(df_div_processed['trade_date'], format='%Y%m%d')
|
|
|
|
# 将原始分红数据的ex_date也转为datetime以便合并
|
|
df_div_adjusted['ex_date'] = pd.to_datetime(df_div_adjusted['ex_date'], format='%Y%m%d')
|
|
|
|
# 合并分红数据到交易日历
|
|
df_merged_temp = df_div_processed.merge(df_div_adjusted[['ex_date', 'cash_div_tax']],
|
|
left_on='trade_date', right_on='ex_date', how='left')
|
|
df_merged_temp.drop('ex_date', axis=1, inplace=True)
|
|
# 填充空值
|
|
df_merged_temp['cash_div_tax'] = df_merged_temp['cash_div_tax'].fillna(0.0)
|
|
|
|
# 计算 cash_div_year (TTM)
|
|
df_merged_temp = df_merged_temp.sort_values('trade_date').reset_index(drop=True)
|
|
# 使用滚动窗口计算过去 GAP_DAYS 天的总和
|
|
# rolling的window参数是基于行数的,所以我们需要先确保日期是连续的交易日
|
|
# 由于trade_date已经是交易日,我们可以直接使用rolling
|
|
# 但需要处理时间窗口,确保是360天而不是360行(因为可能有节假日)
|
|
# 更精确的方法是使用一个自定义函数来累加过去360天内的值
|
|
|
|
# 使用更精确的日期差计算
|
|
def calculate_ttm_div(row_idx):
|
|
current_date = df_merged_temp.loc[row_idx, 'trade_date']
|
|
start_window_date = current_date - pd.Timedelta(days=GAP_DAYS)
|
|
# 筛选出窗口期内的记录
|
|
mask = (df_merged_temp['trade_date'] > start_window_date) & (df_merged_temp['trade_date'] <= current_date)
|
|
return df_merged_temp.loc[mask, 'cash_div_tax'].sum()
|
|
|
|
df_merged_temp['cash_div_year'] = [calculate_ttm_div(i) for i in range(len(df_merged_temp))]
|
|
|
|
|
|
# 截取原始请求的起止时间内的数据
|
|
start_date_dt = pd.to_datetime(start_date, format='%Y%m%d')
|
|
end_date_dt = pd.to_datetime(end_date, format='%Y%m%d')
|
|
df_div_final = df_merged_temp[
|
|
(df_merged_temp['trade_date'] >= start_date_dt) &
|
|
(df_merged_temp['trade_date'] <= end_date_dt)
|
|
].copy()
|
|
df_div_final['trade_date'] = df_div_final['trade_date'].dt.strftime('%Y%m%d')
|
|
|
|
# 5. 调用tushare的个股日线行情接口
|
|
try:
|
|
df_daily = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
|
|
df_daily = df_daily[['ts_code', 'trade_date', 'close']].sort_values('trade_date').reset_index(drop=True)
|
|
except Exception as e:
|
|
print(f"获取日线行情数据时出错: {e}")
|
|
return pd.DataFrame()
|
|
|
|
# 6. 合并分红数据和日线行情数据
|
|
df_result = df_daily.merge(df_div_final[['trade_date', 'cash_div_tax', 'cash_div_year']],
|
|
on='trade_date', how='left')
|
|
|
|
# 填充因合并可能产生的NaN(例如,某日有行情但无分红记录)
|
|
df_result['cash_div_tax'] = df_result['cash_div_tax'].fillna(0.0)
|
|
df_result['cash_div_year'] = df_result['cash_div_year'].fillna(0.0)
|
|
|
|
|
|
'''
|
|
向前填充cash_div_year(最多填充最近n个非零值)
|
|
如果遇到0值,依次往下查询直到查询到非0数字为止,如果数字个数<=n个,就置last_valid_value, 否则保持不变
|
|
'''
|
|
# 确保按trade_date倒序遍历(日期从大到小)
|
|
df_result = df_result.sort_values('trade_date', ascending=False).reset_index(drop=True)
|
|
|
|
last_valid_value = None # 初始化最后一个有效值变量
|
|
for idx in range(len(df_result)): # 遍历DataFrame的每一行
|
|
current_value = df_result.loc[idx, 'cash_div_year'] # 获取当前行的TTM分红值
|
|
if current_value > 0: # 如果当前值大于0
|
|
last_valid_value = current_value # 更新最后一个有效值
|
|
else:
|
|
# 向下查找最多n个位置内的非零值
|
|
found_value = None # 初始化找到的值
|
|
search_count = 0 # 初始化搜索计数
|
|
# 从下一行开始搜索,最多搜索n行
|
|
for search_idx in range(idx + 1, min(idx + n + 1, len(df_result))):
|
|
search_value = df_result.loc[search_idx, 'cash_div_year'] # 获取搜索行的值
|
|
search_count += 1 # 增加搜索计数
|
|
if search_value > 0: # 如果找到非零值
|
|
found_value = search_value # 记录找到的值
|
|
break # 跳出搜索循环
|
|
|
|
# 如果找到非零值且在n个位置内
|
|
if found_value is not None and search_count <= n:
|
|
df_result.loc[idx, 'cash_div_year'] = found_value
|
|
last_valid_value = found_value
|
|
elif last_valid_value is not None:
|
|
df_result.loc[idx, 'cash_div_year'] = last_valid_value
|
|
|
|
# 毛刺平滑处理 cash_div_year 列
|
|
df_result=smooth_dataframe_brush(df_result, target_columns=['cash_div_year'], window_size=31, threshold_factor=0.5, max_brush_length=15 )
|
|
# 恢复原始日期顺序
|
|
df_result = df_result.sort_values('trade_date').reset_index(drop=True)
|
|
|
|
# 计算股息率
|
|
df_result['div_yield'] = 0.0
|
|
mask_non_zero_price = df_result['close'] > 0
|
|
mask_non_zero_div_year = df_result['cash_div_year'] > 0
|
|
# 只对收盘价大于0且TTM分红大于0的记录计算股息率
|
|
valid_mask = mask_non_zero_price & mask_non_zero_div_year
|
|
df_result.loc[valid_mask, 'div_yield'] = (
|
|
(df_result.loc[valid_mask, 'cash_div_year'] / df_result.loc[valid_mask, 'close']) * 100
|
|
).round(PRECISION_CONFIG)
|
|
|
|
# 7. 返回最终结果
|
|
# 重命名字段以匹配要求 (注意: 题目中'cach_div_tax'应为'cash_div_tax')
|
|
#df_result.rename(columns={'cash_div_tax': 'cach_div_tax', 'cash_div_year': 'cach_div_year'}, inplace=True)
|
|
|
|
final_columns = ['ts_code', 'trade_date', 'close', 'cash_div_tax', 'cash_div_year', 'div_yield']
|
|
df_final = df_result[final_columns]
|
|
|
|
return df_final
|
|
|
|
|
|
|
|
# --- 示例用法 ---
|
|
if __name__ == "__main__":
|
|
start_dt = '2020-01-01'
|
|
end_dt = '2025-12-31'
|
|
ts_code = '000001.SZ'
|
|
result = analyze_stock_dividend_and_price(ts_code, start_dt, end_dt)
|
|
print(f"股票 {ts_code} 的分红与行情数据分析结果:")
|
|
print(result.head(10))
|
|
print(f"\n数据总行数: {len(result)}") |