From 1f3be8e1823a08cb73a5a80a553ad1a549861116 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Mon, 25 Nov 2024 17:39:49 +0800 Subject: [PATCH] =?UTF-8?q?0.9.61=20=E6=96=B0=E5=A2=9E=20pyd=20=E6=89=93?= =?UTF-8?q?=E5=8C=85=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- czsc/traders/weight_backtest.py | 16 ++++++++++------ czsc/utils/st_components.py | 12 ++++++++++-- czsc/utils/stats.py | 8 ++++++-- examples/develop/weight_backtest.py | 8 ++++---- ...3\240\345\255\220\346\240\267\344\276\213.py" | 3 +++ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py index 2052507ee..7ed17f41d 100644 --- a/czsc/traders/weight_backtest.py +++ b/czsc/traders/weight_backtest.py @@ -231,10 +231,13 @@ class WeightBacktest: 更新日志: - - V240627: 增加dailys属性,品种每日的交易信息 + #### 20241125 + + 1. 新增 yearly_days 参数,用于指定每年的交易日天数,默认为 252。 + """ - version = "V240627" + version = "V241125" def __init__(self, dfw, digits=2, **kwargs) -> None: """持仓权重回测 @@ -285,7 +288,8 @@ def __init__(self, dfw, digits=2, **kwargs) -> None: self.dfw["weight"] = self.dfw["weight"].astype("float").round(digits) self.symbols = list(self.dfw["symbol"].unique().tolist()) self._dailys = None - self.results = self.backtest(n_jobs=kwargs.get("n_jobs", 1)) + self.yearly_days = kwargs.pop("yearly_days", 252) + self.results = self.backtest(n_jobs=kwargs.pop("n_jobs", 1)) @property def stats(self): @@ -332,7 +336,7 @@ def alpha(self) -> pd.DataFrame: def alpha_stats(self): """策略超额收益统计""" df = self.alpha.copy() - stats = czsc.daily_performance(df["超额"].to_list()) + stats = czsc.daily_performance(df["超额"].to_list(), yearly_days=self.yearly_days) stats["开始日期"] = df["date"].min().strftime("%Y-%m-%d") stats["结束日期"] = df["date"].max().strftime("%Y-%m-%d") return stats @@ -341,7 +345,7 @@ def alpha_stats(self): def bench_stats(self): """基准收益统计""" df = self.alpha.copy() - stats = czsc.daily_performance(df["基准"].to_list()) + stats = czsc.daily_performance(df["基准"].to_list(), yearly_days=self.yearly_days) stats["开始日期"] = df["date"].min().strftime("%Y-%m-%d") stats["结束日期"] = df["date"].max().strftime("%Y-%m-%d") return stats @@ -559,7 +563,7 @@ def backtest(self, n_jobs=1): res["品种等权日收益"] = dret stats = {"开始日期": dret["date"].min().strftime("%Y%m%d"), "结束日期": dret["date"].max().strftime("%Y%m%d")} - stats.update(daily_performance(dret["total"])) + stats.update(daily_performance(dret["total"], yearly_days=self.yearly_days)) dfp = pd.concat([v["pairs"] for k, v in res.items() if k in symbols], ignore_index=True) pairs_stats = evaluate_pairs(dfp) pairs_stats = {k: v for k, v in pairs_stats.items() if k in ["单笔收益", "持仓K线数", "交易胜率", "持仓天数"]} diff --git a/czsc/utils/st_components.py b/czsc/utils/st_components.py index 004677c5b..f7728d3f5 100644 --- a/czsc/utils/st_components.py +++ b/czsc/utils/st_components.py @@ -486,8 +486,15 @@ def show_weight_backtest(dfw, **kwargs): - n_jobs: int, 并行计算的进程数,默认为 1 """ + from czsc.eda import cal_yearly_days + fee = kwargs.get("fee", 2) digits = kwargs.get("digits", 2) + yearly_days = kwargs.pop("yearly_days", None) + + if not yearly_days: + yearly_days = cal_yearly_days(dts=dfw["dt"].unique()) + if (dfw.isnull().sum().sum() > 0) or (dfw.isna().sum().sum() > 0): st.warning("show_weight_backtest :: 持仓权重数据中存在空值,请检查数据后再试;空值数据如下:") st.dataframe(dfw[dfw.isnull().sum(axis=1) > 0], use_container_width=True) @@ -509,13 +516,14 @@ def show_weight_backtest(dfw, **kwargs): c9.metric("年化波动率", f"{stat['年化波动率']:.2%}") c10.metric("多头占比", f"{stat['多头占比']:.2%}") c11.metric("空头占比", f"{stat['空头占比']:.2%}") + st.caption(f"回测参数:单边手续费 {fee} BP,权重小数位数 {digits} ,年交易天数 {yearly_days}") st.divider() dret = wb.results["品种等权日收益"].copy() dret["dt"] = pd.to_datetime(dret["date"]) dret = dret.set_index("dt").drop(columns=["date"]) # dret.index = pd.to_datetime(dret.index) - show_daily_return(dret, legend_only_cols=dfw["symbol"].unique().tolist(), **kwargs) + show_daily_return(dret, legend_only_cols=dfw["symbol"].unique().tolist(), yearly_days=yearly_days, **kwargs) if kwargs.get("show_drawdowns", False): show_drawdowns(dret, ret_col="total", sub_title="") @@ -532,7 +540,7 @@ def show_weight_backtest(dfw, **kwargs): if kwargs.get("show_splited_daily", False): with st.expander("品种等权日收益分段表现", expanded=False): - show_splited_daily(dret[["total"]].copy(), ret_col="total") + show_splited_daily(dret[["total"]].copy(), ret_col="total", yearly_days=yearly_days) if kwargs.get("show_yearly_stats", False): with st.expander("年度绩效指标", expanded=False): diff --git a/czsc/utils/stats.py b/czsc/utils/stats.py index 0b3ad1032..cf80c6a02 100644 --- a/czsc/utils/stats.py +++ b/czsc/utils/stats.py @@ -163,7 +163,7 @@ def __min_max(x, min_val, max_val, digits=4): def rolling_daily_performance(df: pd.DataFrame, ret_col, window=252, min_periods=100, **kwargs): - """计算滚动日收益 + """计算滚动日收益的各项指标 :param df: pd.DataFrame, 日收益数据,columns=['dt', ret_col] :param ret_col: str, 收益列名 @@ -173,11 +173,15 @@ def rolling_daily_performance(df: pd.DataFrame, ret_col, window=252, min_periods - yearly_days: int, 252, 一年的交易日数 """ + from czsc.eda import cal_yearly_days + if not df.index.dtype == "datetime64[ns]": df["dt"] = pd.to_datetime(df["dt"]) df.set_index("dt", inplace=True) assert df.index.dtype == "datetime64[ns]", "index必须是datetime64[ns]类型, 请先使用 pd.to_datetime 进行转换" + yearly_days = kwargs.get("yearly_days", cal_yearly_days(df.index)) + df = df[[ret_col]].copy().fillna(0) df.sort_index(inplace=True, ascending=True) dts = sorted(df.index.to_list()) @@ -185,7 +189,7 @@ def rolling_daily_performance(df: pd.DataFrame, ret_col, window=252, min_periods for edt in dts[min_periods:]: sdt = edt - pd.Timedelta(days=window) dfg = df[(df.index >= sdt) & (df.index <= edt)].copy() - s = daily_performance(dfg[ret_col].to_list(), yearly_days=kwargs.get("yearly_days", 252)) + s = daily_performance(dfg[ret_col].to_list(), yearly_days=yearly_days) s["sdt"] = sdt s["edt"] = edt res.append(s) diff --git a/examples/develop/weight_backtest.py b/examples/develop/weight_backtest.py index 42cbee70e..f3f66e84a 100644 --- a/examples/develop/weight_backtest.py +++ b/examples/develop/weight_backtest.py @@ -13,7 +13,7 @@ def test_ensemble_weight(): from czsc import WeightBacktest dfw = pd.read_feather(r"C:\Users\zengb\Downloads\weight_example.feather") - wb = WeightBacktest(dfw, digits=1, fee_rate=0.0002) + wb = WeightBacktest(dfw, digits=2, fee_rate=0.0002, n_jobs=1) ss = sorted(wb.stats.items()) print(ss) @@ -24,7 +24,7 @@ def test_rust_weight_backtest(): dfw = pd.read_feather(r"C:\Users\zengb\Downloads\weight_example.feather") - wb = WeightBacktest(czsc.to_arrow(dfw), digits=1, fee_rate=0.0002, n_jobs=1) + wb = WeightBacktest(czsc.to_arrow(dfw), digits=2, fee_rate=0.0002, n_jobs=1) - ss = sorted(wb.stats.items()) - print(ss) + # ss = sorted(wb.stats.items()) + # print(ss) diff --git "a/examples/\346\234\237\350\264\247\345\245\227\345\210\251\345\233\240\345\255\220\346\240\267\344\276\213.py" "b/examples/\346\234\237\350\264\247\345\245\227\345\210\251\345\233\240\345\255\220\346\240\267\344\276\213.py" index b4f3814cb..d17e405ed 100644 --- "a/examples/\346\234\237\350\264\247\345\245\227\345\210\251\345\233\240\345\255\220\346\240\267\344\276\213.py" +++ "b/examples/\346\234\237\350\264\247\345\245\227\345\210\251\345\233\240\345\255\220\346\240\267\344\276\213.py" @@ -67,6 +67,7 @@ def main(): import czsc from czsc.connectors import cooperation as coo + # 构建策略 df1 = coo.get_raw_bars(symbol="DLy9001", freq="日线", sdt="20170101", edt="20221231", raw_bars=False, fq="后复权") df2 = coo.get_raw_bars(symbol="DLp9001", freq="日线", sdt="20170101", edt="20221231", raw_bars=False, fq="后复权") df = pd.concat([df1, df2], axis=0) @@ -76,6 +77,8 @@ def main(): df["price"] = df["close"] dfw = df[["dt", "symbol", "price", "weight"]].copy() + + # 执行回测 st.title("期货套利研究") czsc.show_weight_backtest( dfw, fee_rate=0.0002, show_drawdowns=True, show_yearly_stats=True, show_monthly_return=True