Skip to content

Commit

Permalink
0.9.54 增加超额分析
Browse files Browse the repository at this point in the history
  • Loading branch information
zengbin93 committed Jun 27, 2024
1 parent 41183d9 commit e77a6b6
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 15 deletions.
81 changes: 69 additions & 12 deletions czsc/traders/weight_backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from typing import Union, AnyStr, Callable
from multiprocessing import cpu_count
from concurrent.futures import ProcessPoolExecutor

import czsc
from czsc.traders.base import CzscTrader
from czsc.utils.io import save_json
from czsc.utils.stats import daily_performance, evaluate_pairs
Expand Down Expand Up @@ -226,9 +228,13 @@ class WeightBacktest:
"""持仓权重回测
飞书文档:https://s0cqcxuy3p.feishu.cn/wiki/Pf1fw1woQi4iJikbKJmcYToznxb
更新日志:
- V240627: 增加dailys属性,品种每日的交易信息
"""

version = "V231126"
version = "V240627"

def __init__(self, dfw, digits=2, **kwargs) -> None:
"""持仓权重回测
Expand Down Expand Up @@ -278,6 +284,7 @@ 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())
default_n_jobs = min(cpu_count() // 2, len(self.symbols))
self._dailys = None
self.results = self.backtest(n_jobs=kwargs.get("n_jobs", default_n_jobs))

@property
Expand All @@ -290,6 +297,46 @@ def daily_return(self) -> pd.DataFrame:
"""品种等权费后日收益率"""
return self.results.get("品种等权日收益", pd.DataFrame())

@property
def dailys(self) -> pd.DataFrame:
"""品种每日的交易信息
columns = ['date', 'symbol', 'edge', 'return', 'cost', 'n1b', 'turnover']
其中:
date 交易日,
symbol 合约代码,
n1b 品种每日收益率,
edge 策略每日收益率,
return 策略每日收益率减去交易成本后的真实收益,
cost 交易成本
turnover 当日的单边换手率
"""
return self._dailys.copy() if self._dailys is not None else pd.DataFrame()

@property
def alpha(self) -> pd.DataFrame:
"""策略超额收益
columns = ['date', '策略', '基准', '超额']
"""
if self._dailys is None:
return pd.DataFrame()
df1 = self._dailys.groupby("date").agg({"return": "mean", "n1b": "mean"})
df1["alpha"] = df1["return"] - df1["n1b"]
df1.rename(columns={"return": "策略", "n1b": "基准", "alpha": "超额"}, inplace=True)
df1 = df1.reset_index()
return df1

@property
def alpha_stats(self):
"""策略超额收益统计"""
df = self.alpha.copy()
stats = czsc.daily_performance(df["超额"].to_list())
stats["开始日期"] = df["date"].min().strftime("%Y-%m-%d")
stats["结束日期"] = df["date"].max().strftime("%Y-%m-%d")
return stats

def get_symbol_daily(self, symbol):
"""获取某个合约的每日收益率
Expand All @@ -301,19 +348,21 @@ def get_symbol_daily(self, symbol):
4. 计算每条数据扣除手续费后的收益(edge_post_fee):收益减去手续费。
5. 根据日期进行分组,并对每组进行求和操作,得到每日的总收益、总扣除手续费后的收益和总手续费。
6. 重置索引,并将交易标的符号添加到DataFrame中。
7. 重命名列名,将'edge_post_fee'列改为'return',将'dt'列改为'date'
7. 重命名列名,将'edge_post_fee'列改为 return,将'dt'列改为 date。
8. 选择需要的列,并返回包含日期、交易标的、收益、扣除手续费后的收益和手续费的DataFrame。
:param symbol: str,合约代码
:return: pd.DataFrame,品种每日收益率,
columns = ['date', 'symbol', 'edge', 'return', 'cost']
columns = ['date', 'symbol', 'edge', 'return', 'cost', 'n1b']
其中
date 为交易日,
symbol 为合约代码,
edge 为每日收益率,
return 为每日收益率减去交易成本后的真实收益,
cost 为交易成本
date 交易日,
symbol 合约代码,
n1b 品种每日收益率,
edge 策略每日收益率,
return 策略每日收益率减去交易成本后的真实收益,
cost 交易成本
turnover 当日的单边换手率
数据样例如下:
Expand All @@ -328,13 +377,19 @@ def get_symbol_daily(self, symbol):
========== ======== ============ ============ =======
"""
dfs = self.dfw[self.dfw["symbol"] == symbol].copy()
dfs["edge"] = dfs["weight"] * (dfs["price"].shift(-1) / dfs["price"] - 1)
dfs["cost"] = abs(dfs["weight"].shift(1) - dfs["weight"]) * self.fee_rate
dfs["n1b"] = dfs["price"].shift(-1) / dfs["price"] - 1
dfs["edge"] = dfs["weight"] * dfs["n1b"]
dfs["turnover"] = abs(dfs["weight"].shift(1) - dfs["weight"])
dfs["cost"] = dfs["turnover"] * self.fee_rate
dfs["edge_post_fee"] = dfs["edge"] - dfs["cost"]
daily = dfs.groupby(dfs["dt"].dt.date).agg({"edge": "sum", "edge_post_fee": "sum", "cost": "sum"}).reset_index()
daily = (
dfs.groupby(dfs["dt"].dt.date)
.agg({"edge": "sum", "edge_post_fee": "sum", "cost": "sum", "n1b": "sum", "turnover": "sum"})
.reset_index()
)
daily["symbol"] = symbol
daily.rename(columns={"edge_post_fee": "return", "dt": "date"}, inplace=True)
daily = daily[["date", "symbol", "edge", "return", "cost"]]
daily = daily[["date", "symbol", "n1b", "edge", "return", "cost", "turnover"]].copy()
return daily

def get_symbol_pairs(self, symbol):
Expand Down Expand Up @@ -485,6 +540,8 @@ def backtest(self, n_jobs=1):
):
res[symbol] = res_symbol

self._dailys = pd.concat([v["daily"] for k, v in res.items() if k in symbols], ignore_index=True)

dret = pd.concat([v["daily"] for k, v in res.items() if k in symbols], ignore_index=True)
dret = pd.pivot_table(dret, index="date", columns="symbol", values="return").fillna(0)
dret["total"] = dret[list(res.keys())].mean(axis=1)
Expand Down
12 changes: 9 additions & 3 deletions examples/test_offline/test_weight_backtest.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import sys

sys.path.insert(0, ".")
sys.path.insert(0, "..")
import czsc
import pandas as pd

assert czsc.WeightBacktest.version == "V231126"
assert czsc.WeightBacktest.version == "V240627"


def run_by_weights():
"""从持仓权重样例数据中回测"""
dfw = pd.read_feather(r"C:\Users\zengb\Downloads\weight_example.feather")
wb = czsc.WeightBacktest(dfw, digits=1, fee_rate=0.0002, n_jobs=1)
# wb = czsc.WeightBacktest(dfw, digits=1, fee_rate=0.0002)
dailys = wb.dailys

# 计算等权组合的超额
df1 = dailys.groupby("date").agg({"return": "mean", "n1b": "mean"})
df1["alpha"] = df1["return"] - df1["n1b"]

# ------------------------------------------------------------------------------------
# 查看绩效评价
# ------------------------------------------------------------------------------------
print(wb.results['绩效评价'])
print(wb.results["绩效评价"])
# {'开始日期': '20170103',
# '结束日期': '20230731',
# '年化': 0.093, # 品种等权之后的年化收益率
Expand All @@ -41,5 +47,5 @@ def run_by_weights():
wb.report(res_path=r"C:\Users\zengb\Desktop\231005\weight_example")


if __name__ == '__main__':
if __name__ == "__main__":
run_by_weights()

0 comments on commit e77a6b6

Please sign in to comment.