Source code for czsc.traders.performance
# -*- coding: utf-8 -*-
"""
author: zengbin93
email: zeng_bin8888@163.com
create_dt: 2022/5/10 15:19
describe: 请描述文件用途
"""
import os
import pandas as pd
from loguru import logger
from czsc.objects import cal_break_even_point
import matplotlib.pyplot as plt
from czsc.data import TsDataCache, save_symbols_to_ebk
from czsc.sensors.utils import max_draw_down, turn_over_rate
plt.style.use('ggplot')
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
[docs]def stock_holds_performance(dc: TsDataCache, dfh, res_path):
"""计算A股日线持仓组合的表现
:param dc: Tushare 数据缓存对象
:param dfh: 持仓组合,样例如下,其中【证券代码】要求是tushare的格式,成分日期当天状态为持仓
成分日期 证券代码 持仓权重 n1b
0 2020-01-03 300620.SZ 0.008403 141.861099
1 2020-01-03 300677.SZ 0.008403 767.124023
2 2020-01-03 300708.SZ 0.008403 93.029297
3 2020-01-03 002151.SZ 0.008403 -7.465500
4 2020-01-03 002156.SZ 0.008403 350.101715
:param res_path: 结果保存路径
:return:
"""
os.makedirs(res_path, exist_ok=True)
date_fmt = '%Y%m%d'
dfh['成分日期'] = pd.to_datetime(dfh['成分日期']).dt.strftime(date_fmt)
sdt = pd.to_datetime(dfh['成分日期'].min()).strftime(date_fmt)
edt = pd.to_datetime(dfh['成分日期'].max()).strftime(date_fmt)
dfh.to_feather(os.path.join(res_path, f'holds_{sdt}_{edt}.feather'))
latest = dfh[dfh['成分日期'] == dfh['成分日期'].max()]
latest.to_excel(os.path.join(res_path, "组合最新持仓.xlsx"), index=False)
save_symbols_to_ebk(latest['证券代码'].to_list(), os.path.join(res_path, "组合最新持仓.EBK"), source='ts')
# 一进一出算1倍组合换手
_, turn = turn_over_rate(dfh)
mean_counts = round(dfh.groupby('成分日期')['证券代码'].count().mean(), 2)
index_list = ['000905.SH', '000016.SH', '000300.SH']
# 计算收益曲线
dfa = pd.DataFrame({"成分日期": dc.get_dates_span(sdt, edt)})
dfa['成分日期'] = pd.to_datetime(dfa['成分日期']).apply(lambda x: x.strftime(date_fmt))
dfa = dfa.sort_values(by='成分日期')
df_ = dfh.groupby('成分日期')['n1b'].mean().reset_index(drop=False)
dfa = dfa.merge(df_[['成分日期', 'n1b']], on=['成分日期'], how='left')
dfa.rename({'n1b': '组合收益'}, axis=1, inplace=True)
for _index in index_list:
dfi = dc.pro_bar(_index, sdt, edt, freq='D', asset="I", adj='qfq', raw_bar=False)
dfi['成分日期'] = dfi['dt'].apply(lambda x: x.strftime(date_fmt))
dfa = dfa.merge(dfi[['成分日期', 'n1b']], on=['成分日期'], how='left')
dfa.rename({'n1b': _index}, axis=1, inplace=True)
dfa = dfa.fillna(0)
dfa.to_excel(os.path.join(res_path, f'收益对比_{sdt}_{edt}.xlsx'), index=False)
mdd = max_draw_down(dfa['组合收益'])
# 绘制收益曲线
plt.close()
fig = plt.figure(figsize=(13, 4 * len(index_list)))
axes = fig.subplots(len(index_list), 1, sharex=True)
for i, _index in enumerate(index_list):
ax = axes[i]
df_alpha = dfa.copy(deep=True).dropna(subset=['组合收益'])
df_alpha['超额收益'] = df_alpha['组合收益'] - df_alpha[_index]
df_alpha['组合收益'] = df_alpha['组合收益'].cumsum()
df_alpha[_index] = df_alpha[_index].cumsum()
df_alpha['超额收益'] = df_alpha['超额收益'].cumsum()
df_alpha['成分日期'] = pd.to_datetime(df_alpha['成分日期']).apply(lambda x: x.date())
ax.set_title(f"组合:日均持有{mean_counts}只股票,双边换手{round(turn, 2)}倍,最大回撤{int(mdd[2] * 10000)}BP")
ax.plot(df_alpha['成分日期'], df_alpha['超额收益'], "r-", alpha=0.4)
ax.plot(df_alpha['成分日期'], df_alpha['组合收益'], "b-", alpha=0.4)
ax.plot(df_alpha['成分日期'], df_alpha[_index], "g-", alpha=0.4)
ax.legend(['超额收益', '组合收益', f"基准:{_index}"], loc='upper left')
ax.set_ylabel("净值(单位: BP)")
plt.xticks(rotation=45)
plt.tight_layout()
file_png = os.path.join(res_path, f"alpha_plot.png")
plt.savefig(file_png, bbox_inches='tight', dpi=100)
plt.close()
[docs]class PairsPerformance:
"""交易对效果评估"""
def __init__(self, df_pairs: pd.DataFrame, ):
"""
:param df_pairs: 全部交易对,数据样例如下
标的代码 交易方向 最大仓位 开仓时间 累计开仓 平仓时间 \
0 000001.SH 多头 1 2020-02-06 09:45:00 2820.014893 2020-02-10 13:15:00
1 000001.SH 多头 1 2020-03-20 14:15:00 2733.164062 2020-03-27 14:15:00
2 000001.SH 多头 1 2020-03-30 13:30:00 2747.813965 2020-03-31 13:15:00
3 000001.SH 多头 1 2020-04-01 10:45:00 2765.350098 2020-04-02 09:45:00
4 000001.SH 多头 1 2020-04-02 14:15:00 2757.827881 2020-04-09 11:15:00
累计平仓 累计换手 持仓K线数 事件序列 持仓天数 盈亏金额 交易盈亏 \
0 2872.166992 2 40 开多@低吸 > 平多@60分钟顶背驰 4.145833 52.152100 0.0184
1 2786.754883 2 80 开多@低吸 > 平多@60分钟顶背驰 7.000000 53.590820 0.0196
2 2752.198975 2 15 开多@低吸 > 平多@持有资金 0.989583 4.385010 0.0015
3 2721.335938 2 12 开多@低吸 > 平多@持有资金 0.958333 -44.014160 -0.0159
4 2821.693115 2 58 开多@低吸 > 平多@60分钟顶背驰 6.875000 63.865234 0.0231
盈亏比例
0 0.0184
1 0.0196
2 0.0015
3 -0.0159
4 0.0231
"""
df_pairs = df_pairs.copy(deep=True)
# 将时间转换为年月日周
time_convert = lambda x: (x.strftime("%Y年"), x.strftime("%Y年%m月"), x.strftime("%Y-%m-%d"),
f"{x.year}年第{x.weekofyear}周" if x.weekofyear >= 10 else f"{x.year}年第0{x.weekofyear}周",
)
df_pairs[['开仓年', '开仓月', '开仓日', '开仓周']] = list(df_pairs['开仓时间'].apply(time_convert))
df_pairs[['平仓年', '平仓月', '平仓日', '平仓周']] = list(df_pairs['平仓时间'].apply(time_convert))
self.df_pairs = df_pairs
# 指定哪些列可以用来进行聚合分析
self.agg_columns = ['标的代码', '交易方向', '平仓年', '平仓月', '平仓周', '平仓日', '开仓年', '开仓月', '开仓日', '开仓周']
[docs] @staticmethod
def get_pairs_statistics(df_pairs: pd.DataFrame):
"""统计一组交易的基本信息
:param df_pairs:
:return:
"""
if len(df_pairs) == 0:
info = {
"开始时间": None,
"结束时间": None,
"交易标的数量": 0,
"总体交易次数": 0,
"平均持仓天数": 0,
"平均单笔收益": 0,
"单笔收益标准差": 0,
"最大单笔收益": 0,
"最小单笔收益": 0,
"交易胜率": 0,
"累计盈亏比": 0,
"单笔盈亏比": 0,
"交易得分": 0,
"赢面": 0,
"每自然日收益": 0,
"每根K线收益": 0,
"盈亏平衡点": 0,
"开仓日盈亏平衡点": 0,
}
return info
win_pct = round(len(df_pairs[df_pairs['盈亏比例'] > 0]) / len(df_pairs), 4)
df_gain = df_pairs[df_pairs['盈亏比例'] > 0]
df_loss = df_pairs[df_pairs['盈亏比例'] <= 0]
# 限制盈亏比最大有效值为 5
single_gain_loss_rate = min(round(df_gain['盈亏比例'].mean() / (abs(df_loss['盈亏比例'].mean()) + 1e-8), 2), 5)
total_gain_loss_rate = min(round(df_gain['盈亏比例'].sum() / (abs(df_loss['盈亏比例'].sum()) + 1e-8), 2), 5)
info = {
"开始时间": df_pairs['开仓时间'].min(),
"结束时间": df_pairs['平仓时间'].max(),
"交易标的数量": df_pairs['标的代码'].nunique(),
"总体交易次数": len(df_pairs),
"平均持仓天数": round(df_pairs['持仓天数'].mean(), 2),
"平均持仓K线数": round(df_pairs['持仓K线数'].mean(), 2),
"平均单笔收益": round(df_pairs['盈亏比例'].mean(), 4),
"单笔收益标准差": round(df_pairs['盈亏比例'].std(), 4),
"最大单笔收益": round(df_pairs['盈亏比例'].max(), 4),
"最小单笔收益": round(df_pairs['盈亏比例'].min(), 4),
"交易胜率": win_pct,
"单笔盈亏比": single_gain_loss_rate,
"累计盈亏比": total_gain_loss_rate,
"交易得分": round(total_gain_loss_rate * win_pct, 4),
"赢面": round(single_gain_loss_rate * win_pct - (1 - win_pct), 4),
"盈亏平衡点": round(cal_break_even_point(df_pairs['盈亏比例'].to_list()), 4),
"开仓日盈亏平衡点": round(df_pairs.groupby('开仓日')['盈亏比例'].apply(cal_break_even_point).mean(), 4),
}
info['每自然日收益'] = round(info['平均单笔收益'] / info['平均持仓天数'], 2)
info['每根K线收益'] = round(info['平均单笔收益'] / info['平均持仓K线数'], 2)
return info
[docs] def agg_statistics(self, col: str):
"""按列聚合进行交易对评价"""
df_pairs = self.df_pairs.copy()
assert col in self.agg_columns, f"{col} 不是支持聚合的列,参考:{self.agg_columns}"
results = []
for name, dfg in df_pairs.groupby(col):
if dfg.empty:
continue
res = {col: name}
res.update(self.get_pairs_statistics(dfg))
results.append(res)
df = pd.DataFrame(results)
return df
@property
def basic_info(self):
"""写入基础信息"""
df_pairs = self.df_pairs.copy()
return self.get_pairs_statistics(df_pairs)
[docs] def agg_to_excel(self, file_xlsx):
"""遍历聚合列,保存结果到 Excel 文件中"""
f = pd.ExcelWriter(file_xlsx)
for col in ['标的代码', '交易方向', '平仓年', '平仓月', '平仓周', '平仓日']:
df_ = self.agg_statistics(col)
df_.to_excel(f, sheet_name=f"{col}聚合", index=False)
f.close()
logger.info(f"交易次数:{len(self.df_pairs)}; 聚合分析结果文件:{file_xlsx}")
[docs]def combine_holds_and_pairs(holds, pairs, results_path):
"""结合股票池和择时策略开平交易进行分析
函数计算逻辑:
1. 将holds和pairs数据进行处理和准备。
- 将holds复制到dfh变量。
- 将dfh的'成分日期'列转换为日期类型。
- 将dfh的'证券代码'列赋值给'标的代码'列。
- 将pairs复制到dfp变量。
- 将dfp的'开仓时间'列转换为日期类型,并将日期部分提取出来赋值给'开仓日期'列。
2. 合并数据并筛选交易对。
- 将dfp与dfh的[['开仓日期', '标的代码', '持仓权重']]列进行左连接,得到dfp_。
- 从dfp_中选择持仓权重大于0的交易对,赋值给df_pairs。
- 从dfp中选择开仓时间在df_pairs的开仓时间范围内的数据,赋值给dfp_sub。
3. 进行评价和分析。
- 使用dfp_sub创建PairsPerformance对象tp_old。
- 使用df_pairs创建PairsPerformance对象tp_new。
4. 创建结果目录并保存评价结果和交易数据。
- 使用os.makedirs创建结果目录。
- 将tp_old的统计结果保存为Excel文件,文件名为"原始交易评价.xlsx"。
- 将tp_new的统计结果保存为Excel文件,文件名为"组合过滤评价.xlsx"。
- 将df_pairs的数据保存为Feather文件,文件名为"组合过滤交易.feather"。
5. 返回tp_old和tp_new对象。
:param holds: 组合股票池数据,样例:
成分日期 证券代码 n1b 持仓权重
0 2020-01-02 000001.SZ 183.758194 0.001232
1 2020-01-02 000002.SZ -156.633896 0.001232
2 2020-01-02 000063.SZ 310.296204 0.001232
3 2020-01-02 000066.SZ -131.824997 0.001232
4 2020-01-02 000069.SZ -38.561699 0.001232
:param pairs: 择时策略开平交易数据,数据格式如下
标的代码 交易方向 最大仓位 开仓时间 累计开仓 平仓时间 \
0 002698.SZ 多头 1 2015-01-12 13:30:00 24.02790 2015-01-13 09:45:00
1 300031.SZ 多头 1 2015-01-12 10:30:00 53.87420 2015-01-13 09:45:00
2 300046.SZ 多头 1 2015-01-12 10:15:00 41.35824 2015-01-13 09:45:00
3 300076.SZ 多头 1 2015-01-12 10:30:00 57.84800 2015-01-13 09:45:00
4 300099.SZ 多头 1 2015-01-12 10:15:00 62.57308 2015-01-13 09:45:00
累计平仓 累计换手 持仓K线数 持仓天数 盈亏金额 交易盈亏 盈亏比例
0 23.38150 2 7 0.843750 -0.64640 -0.0269 -0.0269
1 52.71284 2 13 0.968750 -1.16136 -0.0215 -0.0215
2 40.72068 2 14 0.979167 -0.63756 -0.0154 -0.0154
3 55.45144 2 13 0.968750 -2.39656 -0.0414 -0.0414
4 61.50528 2 14 0.979167 -1.06780 -0.0170 -0.0170
:param results_path: 分析结果目录
:return:
"""
dfh = holds.copy()
dfh['开仓日期'] = pd.to_datetime(dfh['成分日期'])
dfh['标的代码'] = dfh['证券代码']
dfp = pairs.copy()
dfp['开仓日期'] = pd.to_datetime(dfp['开仓时间'].apply(lambda x: x.date()))
# 合并,选择组合持仓权重大于 0 的交易对
dfp_ = dfp.merge(dfh[['开仓日期', '标的代码', '持仓权重']], on=['开仓日期', '标的代码'], how='left')
df_pairs = dfp_[dfp_['持仓权重'] > 0]
# 按筛选出的交易对时间范围过滤原始交易对
dfp_sub = dfp[(dfp['开仓时间'] >= df_pairs['开仓时间'].min()) & (dfp['开仓时间'] <= df_pairs['开仓时间'].max())]
tp_old = PairsPerformance(dfp_sub)
tp_new = PairsPerformance(df_pairs)
print(f"原始交易:{tp_old.basic_info},\n{tp_old.agg_statistics('平仓年')}\n")
print(f"组合过滤:{tp_new.basic_info},\n{tp_new.agg_statistics('平仓年')}")
os.makedirs(results_path, exist_ok=True)
tp_old.agg_to_excel(os.path.join(results_path, "原始交易评价.xlsx"))
tp_new.agg_to_excel(os.path.join(results_path, "组合过滤评价.xlsx"))
df_pairs.reset_index(drop=True, inplace=True)
df_pairs.to_feather(os.path.join(results_path, "组合过滤交易.feather"))
return tp_old, tp_new
[docs]def combine_dates_and_pairs(dates: list, pairs: pd.DataFrame, results_path):
"""结合大盘日期择时和择时策略开平交易进行分析
函数计算逻辑:
1. 将dates转换为日期类型,并赋值给变量dates。
2. 将pairs复制到dfp变量。
3. 将dfp的'开仓时间'列转换为日期类型,并将日期部分提取出来赋值给'开仓日期'列。
4. 从dfp中选择开仓日期在dates中的数据,赋值给df_pairs。
5. 从dfp中选择开仓时间在df_pairs的开仓时间范围内的数据,赋值给dfp_sub。
6. 使用dfp_sub创建PairsPerformance对象tp_old。
7. 使用df_pairs创建PairsPerformance对象tp_new。
8. 打印原始交易的基本信息和平仓年度统计。
9. 打印组合过滤后的交易的基本信息和平仓年度统计。
10. 创建结果目录并保存评价结果和交易数据。
11. 返回tp_old和tp_new对象。
:param dates: 大盘日期择时日期数据,数据样例 ['2020-01-02', ..., '2022-01-06']
:param pairs: 择时策略开平交易数据,数据格式如下
标的代码 交易方向 最大仓位 开仓时间 累计开仓 平仓时间 \
0 002698.SZ 多头 1 2015-01-12 13:30:00 24.02790 2015-01-13 09:45:00
1 300031.SZ 多头 1 2015-01-12 10:30:00 53.87420 2015-01-13 09:45:00
2 300046.SZ 多头 1 2015-01-12 10:15:00 41.35824 2015-01-13 09:45:00
3 300076.SZ 多头 1 2015-01-12 10:30:00 57.84800 2015-01-13 09:45:00
4 300099.SZ 多头 1 2015-01-12 10:15:00 62.57308 2015-01-13 09:45:00
累计平仓 累计换手 持仓K线数 持仓天数 盈亏金额 交易盈亏 盈亏比例
0 23.38150 2 7 0.843750 -0.64640 -0.0269 -0.0269
1 52.71284 2 13 0.968750 -1.16136 -0.0215 -0.0215
2 40.72068 2 14 0.979167 -0.63756 -0.0154 -0.0154
3 55.45144 2 13 0.968750 -2.39656 -0.0414 -0.0414
4 61.50528 2 14 0.979167 -1.06780 -0.0170 -0.0170
:param results_path: 分析结果目录
:return:
"""
dates = [pd.to_datetime(x) for x in dates]
dfp = pairs.copy()
dfp['开仓日期'] = pd.to_datetime(dfp['开仓时间'].apply(lambda x: x.date()))
df_pairs = dfp[dfp['开仓日期'].isin(dates)]
# 按筛选出的交易对时间范围过滤原始交易对
dfp_sub = dfp[(dfp['开仓时间'] >= df_pairs['开仓时间'].min()) & (dfp['开仓时间'] <= df_pairs['开仓时间'].max())]
tp_old = PairsPerformance(dfp_sub)
tp_new = PairsPerformance(df_pairs)
print(f"原始交易:{tp_old.basic_info},\n{tp_old.agg_statistics('平仓年')}\n")
print(f"组合过滤:{tp_new.basic_info},\n{tp_new.agg_statistics('平仓年')}")
os.makedirs(results_path, exist_ok=True)
tp_old.agg_to_excel(os.path.join(results_path, "原始交易评价.xlsx"))
tp_new.agg_to_excel(os.path.join(results_path, "组合过滤评价.xlsx"))
return tp_old, tp_new