From 348ca55910a9c78379e02487ddfcc2ce8ee58773 Mon Sep 17 00:00:00 2001 From: zengbin93 Date: Thu, 19 Sep 2024 12:31:49 +0800 Subject: [PATCH] =?UTF-8?q?V0.9.59=20=E6=9B=B4=E6=96=B0=E4=B8=80=E6=89=B9?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=20(#213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 0.9.59 start coding * 0.9.59 fix dt type error * 0.9.59 fix dt type error * 0.9.59 update * 0.9.59 新增 min_max_limit * 0.9.59 新增 rolling layers * 0.9.59 新增 rolling layers * 0.9.59 update * 0.9.59 update * 0.9.59 daily_performance 优化 * 0.9.59 daily_performance 优化 --- .github/workflows/pythonpackage.yml | 2 +- czsc/__init__.py | 9 +- czsc/eda.py | 63 +++++ czsc/traders/rwc.py | 3 +- czsc/traders/weight_backtest.py | 4 +- czsc/utils/__init__.py | 27 ++ czsc/utils/bar_generator.py | 1 + czsc/utils/cross.py | 114 ++++---- czsc/utils/st_components.py | 248 ++++++++++-------- czsc/utils/stats.py | 7 + czsc/utils/ta.py | 207 ++++++++++++++- .../weight_backtest.py" | 11 +- ...ib\347\232\204\351\200\237\345\272\246.py" | 19 ++ requirements.txt | 6 +- test/test_utils.py | 20 +- 15 files changed, 558 insertions(+), 183 deletions(-) create mode 100644 "examples/develop/\345\257\271\346\257\224numpy\344\270\216talib\347\232\204\351\200\237\345\272\246.py" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 64963e729..2046a4e79 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -5,7 +5,7 @@ name: Python package on: push: - branches: [ master, V0.9.58 ] + branches: [ master, V0.9.59 ] pull_request: branches: [ master ] diff --git a/czsc/__init__.py b/czsc/__init__.py index a4ae2e0d0..4395bec0a 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -47,6 +47,7 @@ from czsc.utils import ( mac_address, overlap, + to_arrow, format_standard_kline, @@ -65,6 +66,7 @@ save_json, get_sub_elements, get_py_namespace, + code_namespace, freqs_sorted, x_round, import_by_name, @@ -155,6 +157,7 @@ show_strategies_recent, show_factor_value, show_code_editor, + show_classify, ) from czsc.utils.bi_info import ( @@ -204,13 +207,15 @@ cross_sectional_strategy, judge_factor_direction, monotonicity, + min_max_limit, + rolling_layers, ) -__version__ = "0.9.58" +__version__ = "0.9.59" __author__ = "zengbin93" __email__ = "zeng_bin8888@163.com" -__date__ = "20240808" +__date__ = "20240901" def welcome(): diff --git a/czsc/eda.py b/czsc/eda.py index 99c37c3bc..ae4a9fd09 100644 --- a/czsc/eda.py +++ b/czsc/eda.py @@ -159,3 +159,66 @@ def monotonicity(sequence): """ from scipy.stats import spearmanr return spearmanr(sequence, range(len(sequence)))[0] + + +def min_max_limit(x, min_val, max_val, digits=4): + """限制 x 的取值范围在 min_val 和 max_val 之间 + + :param x: float, 输入值 + :param min_val: float, 最小值 + :param max_val: float, 最大值 + :param digits: int, 保留小数位数 + :return: float + """ + return round(max(min_val, min(max_val, x)), digits) + + +def rolling_layers(df, factor, n=5, **kwargs): + """对时间序列数据进行分层 + + :param df: 因子数据,必须包含 dt, factor 列,其中 dt 为日期,factor 为因子值 + :param factor: 因子列名 + :param n: 分层数量,默认为10 + :param kwargs: + + - window: 窗口大小,默认为2000 + - min_periods: 最小样本数量,默认为300 + - mode: str, {'loose', 'strict'}, 分层模式,默认为 'loose'; + loose 表示使用 rolling + rank 的方式分层,有一点点未来信息,存在一定的数据穿越问题; + strict 表示使用 rolling + qcut 的方式分层,无未来信息,但是执行速度较慢。 + + :return: df, 添加了 factor分层 列 + """ + assert df[factor].nunique() > n * 2, "因子值的取值数量必须大于分层数量" + assert df[factor].isna().sum() == 0, "因子有缺失值,缺失数量为:{}".format(df[factor].isna().sum()) + assert df['dt'].duplicated().sum() == 0, f"dt 列不能有重复值,存在重复值数量:{df['dt'].duplicated().sum()}" + + window = kwargs.get("window", 600) + min_periods = kwargs.get("min_periods", 300) + + # 不能有 inf 和 -inf + if df.loc[df[factor].isin([float("inf"), float("-inf")]), factor].shape[0] > 0: + raise ValueError(f"存在 {factor} 为 inf / -inf 的数据") + + if kwargs.get('mode', 'loose') == 'loose': + # loose 模式,可能存在一点点未来信息 + df['pct_rank'] = df[factor].rolling(window=window, min_periods=min_periods).rank(pct=True, ascending=True) + bins = [i/n for i in range(n+1)] + df['pct_rank_cut'] = pd.cut(df['pct_rank'], bins=bins, labels=False) + df['pct_rank_cut'] = df['pct_rank_cut'].fillna(-1) + # 第00层表示缺失值 + df[f"{factor}分层"] = df['pct_rank_cut'].apply(lambda x: f"第{str(int(x+1)).zfill(2)}层") + df.drop(['pct_rank', 'pct_rank_cut'], axis=1, inplace=True) + + else: + assert kwargs.get('mode', 'strict') == 'strict' + df[f"{factor}_qcut"] = ( + df[factor].rolling(window=window, min_periods=min_periods) + .apply(lambda x: pd.qcut(x, q=n, labels=False, duplicates="drop", retbins=False).values[-1], raw=False) + ) + df[f"{factor}_qcut"] = df[f"{factor}_qcut"].fillna(-1) + # 第00层表示缺失值 + df[f"{factor}分层"] = df[f"{factor}_qcut"].apply(lambda x: f"第{str(int(x+1)).zfill(2)}层") + df.drop([f"{factor}_qcut"], axis=1, inplace=True) + + return df diff --git a/czsc/traders/rwc.py b/czsc/traders/rwc.py index daf5cda21..df9b04b00 100644 --- a/czsc/traders/rwc.py +++ b/czsc/traders/rwc.py @@ -176,6 +176,7 @@ def publish_dataframe(self, df, overwrite=False, batch_size=10000): :param df: pandas.DataFrame, 必需包含['symbol', 'dt', 'weight']列, 可选['price', 'ref']列, 如没有price则写0, dtype同publish方法 :param overwrite: boolean, 是否覆盖已有记录 + :param batch_size: int, 每次发布的最大数量 :return: 成功发布信号的条数 """ df = df.copy() @@ -392,7 +393,7 @@ def get_hist_weights(self, symbol, sdt, edt) -> pd.DataFrame: price = price if price is None else float(price) try: ref = json.loads(ref) - except Exception: + except Exception as e: ref = ref weights.append((self.strategy_name, symbol, dt, weight, price, ref)) diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py index f89ae8415..d0d65ab34 100644 --- a/czsc/traders/weight_backtest.py +++ b/czsc/traders/weight_backtest.py @@ -277,6 +277,7 @@ def __init__(self, dfw, digits=2, **kwargs) -> None: """ self.kwargs = kwargs self.dfw = dfw.copy() + self.dfw["dt"] = pd.to_datetime(self.dfw["dt"]) if self.dfw.isnull().sum().sum() > 0: raise ValueError("dfw 中存在空值, 请先处理") self.digits = digits @@ -553,9 +554,10 @@ def backtest(self, n_jobs=1): 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) + dret = dret.round(4).reset_index() res["品种等权日收益"] = dret - stats = {"开始日期": dret.index.min().strftime("%Y%m%d"), "结束日期": dret.index.max().strftime("%Y%m%d")} + stats = {"开始日期": dret["date"].min().strftime("%Y%m%d"), "结束日期": dret["date"].max().strftime("%Y%m%d")} stats.update(daily_performance(dret["total"])) dfp = pd.concat([v["pairs"] for k, v in res.items() if k in symbols], ignore_index=True) pairs_stats = evaluate_pairs(dfp) diff --git a/czsc/utils/__init__.py b/czsc/utils/__init__.py index c991f5b0c..4d93a3000 100644 --- a/czsc/utils/__init__.py +++ b/czsc/utils/__init__.py @@ -1,5 +1,6 @@ # coding: utf-8 import os +import pandas as pd from typing import List, Union from . import qywx @@ -95,6 +96,20 @@ def get_py_namespace(file_py: str, keys: list = []) -> dict: return namespace +def code_namespace(code: str, keys: list = []) -> dict: + """获取 python 代码中的 namespace + + :param code: python 代码 + :param keys: 指定需要的对象名称 + :return: namespace + """ + namespace = {"code": code} + exec(code, namespace) + if keys: + namespace = {k: v for k, v in namespace.items() if k in keys} + return namespace + + def import_by_name(name): """通过字符串导入模块、类、函数 @@ -199,3 +214,15 @@ def mac_address(): x = uuid.UUID(int=uuid.getnode()).hex[-12:].upper() x = "-".join([x[i : i + 2] for i in range(0, 11, 2)]) return x + + +def to_arrow(df: pd.DataFrame): + """将 pandas.DataFrame 转换为 pyarrow.Table""" + import io + import pyarrow as pa + + table = pa.Table.from_pandas(df) + with io.BytesIO() as sink: + with pa.ipc.new_file(sink, table.schema) as writer: + writer.write_table(table) + return sink.getvalue() diff --git a/czsc/utils/bar_generator.py b/czsc/utils/bar_generator.py index b5c63dda1..558361988 100644 --- a/czsc/utils/bar_generator.py +++ b/czsc/utils/bar_generator.py @@ -31,6 +31,7 @@ def is_trading_time(dt: datetime = datetime.now(), market="A股"): def get_intraday_times(freq="1分钟", market="A股"): """获取指定市场的交易时间段 + :param freq: K线周期,如 1分钟、5分钟、15分钟、30分钟、60分钟 :param market: 市场名称,可选值:A股、期货、默认 :return: 交易时间段列表 """ diff --git a/czsc/utils/cross.py b/czsc/utils/cross.py index 481eabe2e..67c938b14 100644 --- a/czsc/utils/cross.py +++ b/czsc/utils/cross.py @@ -13,8 +13,10 @@ from czsc.utils import WordWriter from czsc.utils.stats import net_value_stats from czsc.utils.plt_plot import plot_net_value +from deprecated import deprecated +@deprecated(version="1.0.0", reason="不需要这个类,截面策略的绩效分析可以直接使用 czsc.utils.cross") class CrossSectionalPerformance: """根据截面持仓信息,计算截面绩效""" @@ -42,24 +44,24 @@ def __init__(self, dfh: pd.DataFrame, **kwargs): :param kwargs: 其他参数 """ - self.version = 'V230528' + self.version = "V230528" dfh = dfh.copy() - dfh['dt'] = pd.to_datetime(dfh['dt']) - dfh['date'] = dfh['dt'].apply(lambda x: x.date()) + dfh["dt"] = pd.to_datetime(dfh["dt"]) + dfh["date"] = dfh["dt"].apply(lambda x: x.date()) self.dfh = dfh # self.dfh = self.__add_count(dfh) - self.dfh = self.__add_equal_weight(self.dfh, max_total_weight=kwargs.get('max_total_weight', 1)) - self.dfh['edge'] = self.dfh['n1b'] * self.dfh['weight'] + self.dfh = self.__add_equal_weight(self.dfh, max_total_weight=kwargs.get("max_total_weight", 1)) + self.dfh["edge"] = self.dfh["n1b"] * self.dfh["weight"] self.kwargs = kwargs @staticmethod def __add_count(dfh): """添加连续持仓计数""" res = [] - for symbol, dfs in dfh.groupby('symbol'): - dfs = dfs.sort_values('dt', ascending=True) - dfs['count'] = dfs['pos'].groupby((dfs['pos'] != dfs['pos'].shift()).cumsum()).cumcount() + 1 - dfs['count'] = np.where(dfs['pos'] == 0, 0, dfs['count']) + for symbol, dfs in dfh.groupby("symbol"): + dfs = dfs.sort_values("dt", ascending=True) + dfs["count"] = dfs["pos"].groupby((dfs["pos"] != dfs["pos"].shift()).cumsum()).cumcount() + 1 + dfs["count"] = np.where(dfs["pos"] == 0, 0, dfs["count"]) res.append(dfs) return pd.concat(res, ignore_index=True) @@ -72,15 +74,15 @@ def __add_equal_weight(dfh, max_total_weight=1): 1 表示 100% 权重,即 1 倍杠杆,2 表示 200% 权重,即 2 倍杠杆 :return: """ - if 'weight' in dfh.columns: + if "weight" in dfh.columns: return dfh results = [] - for dt, dfg in dfh.groupby('dt'): - dfg['weight'] = 0 - if dfg['pos'].abs().sum() != 0: - symbol_weight = max_total_weight / dfg['pos'].abs().sum() - dfg['weight'] = symbol_weight * dfg['pos'] + for dt, dfg in dfh.groupby("dt"): + dfg["weight"] = 0 + if dfg["pos"].abs().sum() != 0: + symbol_weight = max_total_weight / dfg["pos"].abs().sum() + dfg["weight"] = symbol_weight * dfg["pos"] results.append(dfg) dfh = pd.concat(results, ignore_index=True) return dfh @@ -88,19 +90,19 @@ def __add_equal_weight(dfh, max_total_weight=1): def cal_turnover(self): """计算换手率""" dfh = self.dfh.copy() - dft = pd.pivot_table(dfh, index='dt', columns='symbol', values='weight', aggfunc='sum') + dft = pd.pivot_table(dfh, index="dt", columns="symbol", values="weight", aggfunc="sum") dft = dft.fillna(0) dft1 = dft.diff().abs().sum(axis=1) # 由于是 diff 计算,第一个时刻的仓位变化被忽视了,修改一下 - dft1.iloc[0] = dfh[dfh['dt'] == dfh['dt'].min()]['pos'].sum() + dft1.iloc[0] = dfh[dfh["dt"] == dfh["dt"].min()]["pos"].sum() dft2 = dft.apply(lambda x: x[x != 0].count(), axis=1).fillna(0) dft = pd.concat([dft1, dft2], axis=1) - dft.columns = ['换手率', '持仓数量'] + dft.columns = ["换手率", "持仓数量"] return dft - def cross_net_value(self, by='dt', values='edge'): + def cross_net_value(self, by="dt", values="edge"): """计算截面等权净值 :param by: 按什么字段计算截面等权净值,默认按交易时间 @@ -109,18 +111,18 @@ def cross_net_value(self, by='dt', values='edge'): 输入 n1b,计算基准收益 :return: """ - assert values in ['edge', 'n1b'] + assert values in ["edge", "n1b"] dfh = self.dfh.copy() - dfe = pd.pivot_table(dfh, index=by, columns='symbol', values=values, aggfunc='sum') - if values == 'edge': - dfe['截面收益'] = dfe.sum(axis=1).fillna(0) + dfe = pd.pivot_table(dfh, index=by, columns="symbol", values=values, aggfunc="sum") + if values == "edge": + dfe["截面收益"] = dfe.sum(axis=1).fillna(0) else: - dfe['截面收益'] = dfe.mean(axis=1).fillna(0) + dfe["截面收益"] = dfe.mean(axis=1).fillna(0) - dfe['累计净值'] = dfe['截面收益'].cumsum() - dfe['动态回撤'] = ((dfe['累计净值'] + 10000) / (dfe['累计净值'] + 10000).cummax() - 1) * 10000 + 10000 - dfe['dt'] = dfe.index.values - return dfe[['dt', '截面收益', '累计净值', '动态回撤']].fillna(0).reset_index(drop=True) + dfe["累计净值"] = dfe["截面收益"].cumsum() + dfe["动态回撤"] = ((dfe["累计净值"] + 10000) / (dfe["累计净值"] + 10000).cummax() - 1) * 10000 + 10000 + dfe["dt"] = dfe.index.values + return dfe[["dt", "截面收益", "累计净值", "动态回撤"]].fillna(0).reset_index(drop=True) def report(self, file_docx): if os.path.exists(file_docx): @@ -129,48 +131,54 @@ def report(self, file_docx): writer = WordWriter(file_docx) writer.add_title("截面绩效分析报告") - writer.add_paragraph("本文档由 czsc 编写,用于分析截面绩效。" - "截面绩效分析,是指在某个时间点,对所有标的收益进行等权汇总,计算出截面等权收益。") + writer.add_paragraph( + "本文档由 czsc 编写,用于分析截面绩效。" + "截面绩效分析,是指在某个时间点,对所有标的收益进行等权汇总,计算出截面等权收益。" + ) dft = self.cal_turnover() writer.add_heading("换手率", level=1) writer.add_paragraph("换手率,是指在某个时间点,所有标的权重变化的绝对值之和。", first_line_indent=0) - writer.add_paragraph(f"平均换手率:{round(dft['换手率'].mean(), 4)}; " - f"累计换手率:{round(dft['换手率'].sum(), 4)}", first_line_indent=0) + writer.add_paragraph( + f"平均换手率:{round(dft['换手率'].mean(), 4)}; " f"累计换手率:{round(dft['换手率'].sum(), 4)}", + first_line_indent=0, + ) - for dt_col in ['dt', 'date']: + for dt_col in ["dt", "date"]: writer.add_heading(f"按 {dt_col} 截面进行评价", level=1) dfe = self.cross_net_value(by=dt_col) - nv = dfe[['dt', '截面收益']].copy() - nv.columns = ['dt', 'edge'] + nv = dfe[["dt", "截面收益"]].copy() + nv.columns = ["dt", "edge"] stats = net_value_stats(nv, sub_cost=False) stats_info = "\n".join([f"{k}: {v}" for k, v in stats.items()]) - writer.add_paragraph(f"截面等权净值,按 {dt_col} 截面进行评价,统计结果如下:\n{stats_info}", first_line_indent=0) + writer.add_paragraph( + f"截面等权净值,按 {dt_col} 截面进行评价,统计结果如下:\n{stats_info}", first_line_indent=0 + ) # 绘制净值曲线 writer.add_paragraph(f"按 {dt_col} 截面的策略净值曲线如下:", first_line_indent=0) - dfe = self.cross_net_value(by=dt_col, values='edge') + dfe = self.cross_net_value(by=dt_col, values="edge") file_png = f"{time.time_ns()}.png" plot_net_value(dfe, file_png=file_png, figsize=(9, 5)) writer.add_picture(file_png, width=15, height=9) os.remove(file_png) writer.add_paragraph(f"按 {dt_col} 截面的基准净值曲线如下:", first_line_indent=0) - dfe = self.cross_net_value(by=dt_col, values='n1b') + dfe = self.cross_net_value(by=dt_col, values="n1b") file_png = f"{time.time_ns()}.png" plot_net_value(dfe, file_png=file_png, figsize=(9, 5)) writer.add_picture(file_png, width=15, height=9) os.remove(file_png) # 计算每个月的累计收益 - nv['month'] = nv['dt'].apply(lambda x: x.strftime("%Y-%m")) - nvm = nv.groupby('month')['edge'].apply(sum).reset_index(drop=False) - nvm[['year', 'month']] = nvm['month'].apply(lambda x: (x[:4], x[-2:])).values.tolist() - ymr = pd.pivot_table(nvm, index='year', columns='month', values='edge', aggfunc='sum').fillna(0).round(1) + nv["month"] = nv["dt"].apply(lambda x: x.strftime("%Y-%m")) + nvm = nv.groupby("month")["edge"].apply(sum).reset_index(drop=False) + nvm[["year", "month"]] = nvm["month"].apply(lambda x: (x[:4], x[-2:])).values.tolist() + ymr = pd.pivot_table(nvm, index="year", columns="month", values="edge", aggfunc="sum").fillna(0).round(1) writer.add_heading(f"按月进行收益汇总:月胜率 = {len(nvm[nvm['edge'] > 0]) / len(nvm) * 100:.2f}%", level=2) - writer.add_df_table(ymr.reset_index(drop=False), style='Medium List 1 Accent 2', font_size=8) + writer.add_df_table(ymr.reset_index(drop=False), style="Medium List 1 Accent 2", font_size=8) writer.save() logger.info(f"报告生成成功:{file_docx}") @@ -196,29 +204,29 @@ def cross_sectional_ranker(df, x_cols, y_col, **kwargs): assert "symbol" in df.columns, "df must have column 'symbol'" assert "dt" in df.columns, "df must have column 'dt'" - if kwargs.get('copy', True): + if kwargs.get("copy", True): df = df.copy() - df['dt'] = pd.to_datetime(df['dt']) - df = df.sort_values(['dt', y_col], ascending=[True, False]) + df["dt"] = pd.to_datetime(df["dt"]) + df = df.sort_values(["dt", y_col], ascending=[True, False]) - model_params = kwargs.get('model_params', {'n_estimators': 40, 'learning_rate': 0.01}) + model_params = kwargs.get("model_params", {"n_estimators": 40, "learning_rate": 0.01}) model = LGBMRanker(**model_params) - dfd = pd.DataFrame({'dt': sorted(df['dt'].unique())}).values - tss = TimeSeriesSplit(n_splits=kwargs.get('n_splits', 5)) + dfd = pd.DataFrame({"dt": sorted(df["dt"].unique())}).values + tss = TimeSeriesSplit(n_splits=kwargs.get("n_splits", 5)) for train_index, test_index in tss.split(dfd): train_dts = dfd[train_index][:, 0] test_dts = dfd[test_index][:, 0] # 拆分训练集和测试集 - train, test = df[df['dt'].isin(train_dts)], df[df['dt'].isin(test_dts)] + train, test = df[df["dt"].isin(train_dts)], df[df["dt"].isin(test_dts)] X_train, X_test, y_train = train[x_cols], test[x_cols], train[y_col] - query_train = train.groupby('dt')['symbol'].count().values + query_train = train.groupby("dt")["symbol"].count().values # 训练模型 & 预测 model.fit(X_train, y_train, group=query_train) - df.loc[X_test.index, 'score'] = model.predict(X_test) + df.loc[X_test.index, "score"] = model.predict(X_test) - df['rank'] = df.groupby('dt')['score'].rank(ascending=kwargs.get('rank_ascending', False)) + df["rank"] = df.groupby("dt")["score"].rank(ascending=kwargs.get("rank_ascending", False)) return df diff --git a/czsc/utils/st_components.py b/czsc/utils/st_components.py index ee0fe5f3c..f5c0c6c5b 100644 --- a/czsc/utils/st_components.py +++ b/czsc/utils/st_components.py @@ -13,6 +13,44 @@ from sklearn.linear_model import LinearRegression +def __stats_style(stats): + stats = stats.style.background_gradient(cmap="RdYlGn_r", axis=None, subset=["年化"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["绝对收益"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["夏普"]) + stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["最大回撤"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["卡玛"]) + stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["年化波动率"]) + stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["下行波动率"]) + stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["盈亏平衡点"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日胜率"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日盈亏比"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日赢面"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["非零覆盖"]) + stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["新高间隔"]) + stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["回撤风险"]) + stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["新高占比"]) + stats = stats.format( + { + "盈亏平衡点": "{:.2f}", + "年化波动率": "{:.2%}", + "下行波动率": "{:.2%}", + "最大回撤": "{:.2%}", + "卡玛": "{:.2f}", + "年化": "{:.2%}", + "夏普": "{:.2f}", + "非零覆盖": "{:.2%}", + "绝对收益": "{:.2%}", + "日胜率": "{:.2%}", + "日盈亏比": "{:.2f}", + "日赢面": "{:.2%}", + "新高间隔": "{:.2f}", + "回撤风险": "{:.2f}", + "新高占比": "{:.2%}", + } + ) + return stats + + def show_daily_return(df: pd.DataFrame, **kwargs): """用 streamlit 展示日收益 @@ -50,36 +88,41 @@ def _stats(df_, type_="持有日"): stats.append(col_stats) stats = pd.DataFrame(stats).set_index("日收益名称") - stats = stats.style.background_gradient(cmap="RdYlGn_r", axis=None, subset=["年化"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["绝对收益"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["夏普"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["最大回撤"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["卡玛"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["年化波动率"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["下行波动率"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["盈亏平衡点"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日胜率"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["非零覆盖"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["新高间隔"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["回撤风险"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["新高占比"]) - stats = stats.format( - { - "盈亏平衡点": "{:.2f}", - "年化波动率": "{:.2%}", - "下行波动率": "{:.2%}", - "最大回撤": "{:.2%}", - "卡玛": "{:.2f}", - "年化": "{:.2%}", - "夏普": "{:.2f}", - "非零覆盖": "{:.2%}", - "绝对收益": "{:.2%}", - "日胜率": "{:.2%}", - "新高间隔": "{:.2f}", - "回撤风险": "{:.2f}", - "新高占比": "{:.2%}", - } - ) + stats = __stats_style(stats) + # stats = stats.style.background_gradient(cmap="RdYlGn_r", axis=None, subset=["年化"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["绝对收益"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["夏普"]) + # stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["最大回撤"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["卡玛"]) + # stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["年化波动率"]) + # stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["下行波动率"]) + # stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["盈亏平衡点"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日胜率"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日盈亏比"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日赢面"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["非零覆盖"]) + # stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["新高间隔"]) + # stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["回撤风险"]) + # stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["新高占比"]) + # stats = stats.format( + # { + # "盈亏平衡点": "{:.2f}", + # "年化波动率": "{:.2%}", + # "下行波动率": "{:.2%}", + # "最大回撤": "{:.2%}", + # "卡玛": "{:.2f}", + # "年化": "{:.2%}", + # "夏普": "{:.2f}", + # "非零覆盖": "{:.2%}", + # "绝对收益": "{:.2%}", + # "日胜率": "{:.2%}", + # "日盈亏比": "{:.2f}", + # "日赢面": "{:.2%}", + # "新高间隔": "{:.2f}", + # "回撤风险": "{:.2f}", + # "新高占比": "{:.2%}", + # } + # ) return stats use_st_table = kwargs.get("use_st_table", False) @@ -469,7 +512,9 @@ def show_weight_backtest(dfw, **kwargs): st.divider() dret = wb.results["品种等权日收益"].copy() - dret.index = pd.to_datetime(dret.index) + 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) if kwargs.get("show_drawdowns", False): @@ -538,53 +583,16 @@ def show_splited_daily(df, ret_col, **kwargs): rows = [] for name, sdt in sdt_map.items(): df1 = df.loc[sdt:last_dt].copy() - row = czsc.daily_performance(df1[ret_col], yearly_days=yearly_days) - row["开始日期"] = sdt.strftime("%Y-%m-%d") - row["结束日期"] = last_dt.strftime("%Y-%m-%d") - row["收益名称"] = name - # row['绝对收益'] = df1[ret_col].sum() + row = { + "收益名称": name, + "开始日期": sdt.strftime("%Y-%m-%d"), + "结束日期": last_dt.strftime("%Y-%m-%d"), + } + row_ = czsc.daily_performance(df1[ret_col], yearly_days=yearly_days) + row.update(row_) rows.append(row) dfv = pd.DataFrame(rows).set_index("收益名称") - cols = [ - "开始日期", - "结束日期", - "绝对收益", - "年化", - "夏普", - "最大回撤", - "卡玛", - "年化波动率", - "下行波动率", - "非零覆盖", - "日胜率", - "盈亏平衡点", - ] - dfv = dfv[cols].copy() - - dfv = dfv.style.background_gradient(cmap="RdYlGn_r", subset=["绝对收益"]) - dfv = dfv.background_gradient(cmap="RdYlGn_r", subset=["年化"]) - dfv = dfv.background_gradient(cmap="RdYlGn_r", subset=["夏普"]) - dfv = dfv.background_gradient(cmap="RdYlGn", subset=["最大回撤"]) - dfv = dfv.background_gradient(cmap="RdYlGn_r", subset=["卡玛"]) - dfv = dfv.background_gradient(cmap="RdYlGn", subset=["年化波动率"]) - dfv = dfv.background_gradient(cmap="RdYlGn", subset=["下行波动率"]) - dfv = dfv.background_gradient(cmap="RdYlGn", subset=["盈亏平衡点"]) - dfv = dfv.background_gradient(cmap="RdYlGn_r", subset=["日胜率"]) - dfv = dfv.background_gradient(cmap="RdYlGn_r", subset=["非零覆盖"]) - dfv = dfv.format( - { - "盈亏平衡点": "{:.2f}", - "年化波动率": "{:.2%}", - "下行波动率": "{:.2%}", - "最大回撤": "{:.2%}", - "卡玛": "{:.2f}", - "年化": "{:.2%}", - "夏普": "{:.2f}", - "非零覆盖": "{:.2%}", - "日胜率": "{:.2%}", - "绝对收益": "{:.2%}", - } - ) + dfv = __stats_style(dfv) st.dataframe(dfv, use_container_width=True) @@ -615,38 +623,7 @@ def show_yearly_stats(df, ret_col, **kwargs): _stats.append(_yst) stats = pd.DataFrame(_stats).set_index("年份") - - stats = stats.style.background_gradient(cmap="RdYlGn_r", axis=None, subset=["年化"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["夏普"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["绝对收益"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["最大回撤"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["卡玛"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["年化波动率"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["下行波动率"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["盈亏平衡点"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["日胜率"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["非零覆盖"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["新高间隔"]) - stats = stats.background_gradient(cmap="RdYlGn", axis=None, subset=["回撤风险"]) - stats = stats.background_gradient(cmap="RdYlGn_r", axis=None, subset=["新高占比"]) - - stats = stats.format( - { - "盈亏平衡点": "{:.2f}", - "年化波动率": "{:.2%}", - "下行波动率": "{:.2%}", - "最大回撤": "{:.2%}", - "卡玛": "{:.2f}", - "年化": "{:.2%}", - "夏普": "{:.2f}", - "非零覆盖": "{:.2%}", - "绝对收益": "{:.2%}", - "日胜率": "{:.2%}", - "新高间隔": "{:.2f}", - "回撤风险": "{:.2f}", - "新高占比": "{:.2%}", - } - ) + stats = __stats_style(stats) sub_title = kwargs.get("sub_title", "") if sub_title: @@ -1688,3 +1665,58 @@ def __editor(): with st.expander(expander_title, expanded=True): code = __editor() return code + + +def show_classify(df, col1, col2, n=10, method="cut", **kwargs): + """显示 col1 对 col2 的分类作用 + + :param df: 数据,pd.DataFrame + :param col1: 分层列 + :param col2: 统计列 + :param n: 分层数量 + :param method: 分层方法,cut 或 qcut + :param kwargs: + + - show_bar: bool, 是否展示柱状图,默认为 False + + """ + df = df[[col1, col2]].copy() + if method == "cut": + df[f"{col1}_分层"] = pd.cut(df[col1], bins=n, duplicates="drop") + elif method == "qcut": + df[f"{col1}_分层"] = pd.qcut(df[col1], q=n, duplicates="drop") + else: + raise ValueError("method must be 'cut' or 'qcut'") + + dfg = df.groupby(f"{col1}_分层", observed=True)[col2].describe().reset_index() + dfx = dfg.copy() + info = ( + f"{col1} 分层对应 {col2} 的均值单调性::red[{czsc.monotonicity(dfx['mean']):.2%}]; " + f"最后一层的均值::red[{dfx['mean'].iloc[-1]:.4f}];" + f"第一层的均值::red[{dfx['mean'].iloc[0]:.4f}]" + ) + st.markdown(info) + + if kwargs.get("show_bar", False): + dfx["标记"] = dfx[f"{col1}_分层"].astype(str) + dfx["text"] = dfx["mean"].apply(lambda x: f"{x:.4f}") + fig = px.bar(dfx, x="标记", y="mean", text="text", color="mean", color_continuous_scale="RdYlGn_r") + fig.update_xaxes(title=None) + fig.update_layout(margin=dict(l=0, r=0, t=0, b=0)) + st.plotly_chart(fig, use_container_width=True) + + dfg = dfg.style.background_gradient(cmap="RdYlGn_r", axis=None, subset=["count"]) + dfg = dfg.background_gradient(cmap="RdYlGn_r", axis=None, subset=["mean", "std", "min", "25%", "50%", "75%", "max"]) + dfg = dfg.format( + { + "count": "{:.0f}", + "mean": "{:.4f}", + "std": "{:.2%}", + "min": "{:.4f}", + "25%": "{:.4f}", + "50%": "{:.4f}", + "75%": "{:.4f}", + "max": "{:.4f}", + } + ) + st.dataframe(dfg, use_container_width=True) diff --git a/czsc/utils/stats.py b/czsc/utils/stats.py index 2c5695de6..0b3ad1032 100644 --- a/czsc/utils/stats.py +++ b/czsc/utils/stats.py @@ -101,6 +101,8 @@ def daily_performance(daily_returns, **kwargs): "最大回撤": 0, "卡玛": 0, "日胜率": 0, + "日盈亏比": 0, + "日赢面": 0, "年化波动率": 0, "下行波动率": 0, "非零覆盖": 0, @@ -117,6 +119,9 @@ def daily_performance(daily_returns, **kwargs): max_drawdown = np.max(dd) kama = annual_returns / max_drawdown if max_drawdown != 0 else 10 win_pct = len(daily_returns[daily_returns >= 0]) / len(daily_returns) + daily_mean_loss = np.mean(daily_returns[daily_returns < 0]) if len(daily_returns[daily_returns < 0]) > 0 else 0 + daily_ykb = np.mean(daily_returns[daily_returns >= 0]) / abs(daily_mean_loss) if daily_mean_loss != 0 else 5 + annual_volatility = np.std(daily_returns) * np.sqrt(yearly_days) none_zero_cover = len(daily_returns[daily_returns != 0]) / len(daily_returns) @@ -144,6 +149,8 @@ def __min_max(x, min_val, max_val, digits=4): "最大回撤": round(max_drawdown, 4), "卡玛": __min_max(kama, -10, 10, 2), "日胜率": round(win_pct, 4), + "日盈亏比": round(daily_ykb, 4), + "日赢面": round(win_pct * daily_ykb - (1 - win_pct), 4), "年化波动率": round(annual_volatility, 4), "下行波动率": round(downside_volatility, 4), "非零覆盖": round(none_zero_cover, 4), diff --git a/czsc/utils/ta.py b/czsc/utils/ta.py index 958d8a931..4430eb1de 100644 --- a/czsc/utils/ta.py +++ b/czsc/utils/ta.py @@ -6,6 +6,7 @@ describe: 常用技术分析指标 """ import numpy as np +import pandas as pd def SMA(close: np.array, timeperiod=5): @@ -22,9 +23,9 @@ def SMA(close: np.array, timeperiod=5): res = [] for i in range(len(close)): if i < timeperiod: - seq = close[0: i + 1] + seq = close[0 : i + 1] else: - seq = close[i - timeperiod + 1: i + 1] + seq = close[i - timeperiod + 1 : i + 1] res.append(seq.mean()) return np.array(res, dtype=np.double).round(4) @@ -85,11 +86,11 @@ def KDJ(close: np.array, high: np.array, low: np.array): lv = [] for i in range(len(close)): if i < n: - h_ = high[0: i + 1] - l_ = low[0: i + 1] + h_ = high[0 : i + 1] + l_ = low[0 : i + 1] else: - h_ = high[i - n + 1: i + 1] - l_ = low[i - n + 1: i + 1] + h_ = high[i - n + 1 : i + 1] + l_ = low[i - n + 1 : i + 1] hv.append(max(h_)) lv.append(min(l_)) @@ -143,3 +144,197 @@ def RSQ(close: [np.array, list]) -> float: rsq = 1 - ss_err / ss_tot return round(rsq, 4) + + +def plus_di(high, low, close, timeperiod=14): + """ + Calculate Plus Directional Indicator (PLUS_DI) manually. + + Parameters: + high (pd.Series): High price series. + low (pd.Series): Low price series. + close (pd.Series): Closing price series. + timeperiod (int): Number of periods to consider for the calculation. + + Returns: + pd.Series: Plus Directional Indicator values. + """ + # Calculate the +DM (Directional Movement) + dm_plus = high - high.shift(1) + dm_plus[dm_plus < 0] = 0 # Only positive differences are considered + + # Calculate the True Range (TR) + tr = pd.concat([high - low, (high - close.shift(1)).abs(), (low - close.shift(1)).abs()], axis=1).max(axis=1) + + # Smooth the +DM and TR with Wilder's smoothing method + smooth_dm_plus = dm_plus.rolling(window=timeperiod).sum() + smooth_tr = tr.rolling(window=timeperiod).sum() + + # Avoid division by zero + smooth_tr[smooth_tr == 0] = np.nan + + # Calculate the Directional Indicator + plus_di_ = 100 * (smooth_dm_plus / smooth_tr) + + return plus_di_ + + +def minus_di(high, low, close, timeperiod=14): + """ + Calculate Minus Directional Indicator (MINUS_DI) manually. + + Parameters: + high (pd.Series): High price series. + low (pd.Series): Low price series. + close (pd.Series): Closing price series. + timeperiod (int): Number of periods to consider for the calculation. + + Returns: + pd.Series: Minus Directional Indicator values. + """ + # Calculate the -DM (Directional Movement) + dm_minus = (low.shift(1) - low).where((low.shift(1) - low) > (high - low.shift(1)), 0) + + # Smooth the -DM with Wilder's smoothing method + smooth_dm_minus = dm_minus.rolling(window=timeperiod).sum() + + # Calculate the True Range (TR) + tr = pd.concat([high - low, (high - close.shift(1)).abs(), (low - close.shift(1)).abs()], axis=1).max(axis=1) + + # Smooth the TR with Wilder's smoothing method + smooth_tr = tr.rolling(window=timeperiod).sum() + + # Avoid division by zero + smooth_tr[smooth_tr == 0] = pd.NA + + # Calculate the Directional Indicator + minus_di_ = 100 * (smooth_dm_minus / smooth_tr.fillna(method="ffill")) + + return minus_di_ + + +def atr(high, low, close, timeperiod=14): + """ + Calculate Average True Range (ATR). + + Parameters: + high (pd.Series): High price series. + low (pd.Series): Low price series. + close (pd.Series): Closing price series. + timeperiod (int): Number of periods to consider for the calculation. + + Returns: + pd.Series: Average True Range values. + """ + # Calculate True Range (TR) + tr1 = high - low + tr2 = (high - close.shift()).abs() + tr3 = (close.shift() - low).abs() + tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) + + # Calculate ATR + atr_ = tr.rolling(window=timeperiod).mean() + + return atr_ + + +def MFI(high, low, close, volume, timeperiod=14): + """ + Calculate Money Flow Index (MFI). + + Parameters: + high (np.array): Array of high prices. + low (np.array): Array of low prices. + close (np.array): Array of closing prices. + volume (np.array): Array of trading volumes. + timeperiod (int): Number of periods to consider for the calculation. + + Returns: + np.array: Array of Money Flow Index values. + """ + # Calculate Typical Price + typical_price = (high + low + close) / 3 + + # Calculate Raw Money Flow + raw_money_flow = typical_price * volume + + # Calculate Positive and Negative Money Flow + positive_money_flow = np.where(typical_price > typical_price.shift(1), raw_money_flow, 0) + negative_money_flow = np.where(typical_price < typical_price.shift(1), raw_money_flow, 0) + + # Calculate Money Ratio + money_ratio = ( + positive_money_flow.rolling(window=timeperiod).sum() / negative_money_flow.rolling(window=timeperiod).sum() + ) + + # Calculate Money Flow Index + mfi = 100 - (100 / (1 + money_ratio)) + + return mfi + + +def CCI(high, low, close, timeperiod=14): + """ + Calculate Commodity Channel Index (CCI). + + Parameters: + high (np.array): Array of high prices. + low (np.array): Array of low prices. + close (np.array): Array of closing prices. + timeperiod (int): Number of periods to consider for the calculation. + + Returns: + np.array: Array of Commodity Channel Index values. + """ + # Typical Price + typical_price = (high + low + close) / 3 + + # Mean Deviation + mean_typical_price = np.mean(typical_price, axis=0) + mean_deviation = np.mean(np.abs(typical_price - mean_typical_price), axis=0) + + # Constant + constant = 1 / (0.015 * timeperiod) + + # CCI Calculation + cci = (typical_price - mean_typical_price) / (constant * mean_deviation) + return cci + + +def LINEARREG_ANGLE(real, timeperiod=14): + """ + Calculate the Linear Regression Angle for a given time period. + + https://github.com/TA-Lib/ta-lib/blob/main/src/ta_func/ta_LINEARREG_ANGLE.c + + :param real: NumPy ndarray of input data points. + :param timeperiod: The number of periods to use for the regression (default is 14). + :return: NumPy ndarray of angles in degrees. + """ + # Validate input parameters + if not isinstance(real, np.ndarray) or not isinstance(timeperiod, int): + raise ValueError("Invalid input parameters.") + if timeperiod < 2 or timeperiod > 100000: + raise ValueError("timeperiod must be between 2 and 100000.") + if len(real) < timeperiod: + raise ValueError("Input data must have at least timeperiod elements.") + + # Initialize output array + angles = np.zeros(len(real)) + + # Calculate the total sum and sum of squares for the given time period + SumX = timeperiod * (timeperiod - 1) * 0.5 + SumXSqr = timeperiod * (timeperiod - 1) * (2 * timeperiod - 1) / 6 + Divisor = SumX * SumX - timeperiod * SumXSqr + + # Calculate the angle for each point in the input array + for today in range(timeperiod - 1, len(real)): + SumXY = 0 + SumY = 0 + for i in range(timeperiod): + SumY += real[today - i] + SumXY += i * real[today - i] + m = (timeperiod * SumXY - SumX * SumY) / Divisor + angles[today] = np.arctan(m) * (180.0 / np.pi) + + return angles diff --git "a/examples/Streamlit\347\273\204\344\273\266\345\272\223\344\275\277\347\224\250\346\241\210\344\276\213/weight_backtest.py" "b/examples/Streamlit\347\273\204\344\273\266\345\272\223\344\275\277\347\224\250\346\241\210\344\276\213/weight_backtest.py" index 3783b0ef4..763bdf213 100644 --- "a/examples/Streamlit\347\273\204\344\273\266\345\272\223\344\275\277\347\224\250\346\241\210\344\276\213/weight_backtest.py" +++ "b/examples/Streamlit\347\273\204\344\273\266\345\272\223\344\275\277\347\224\250\346\241\210\344\276\213/weight_backtest.py" @@ -1,10 +1,17 @@ +import sys + +sys.path.insert(0, ".") +sys.path.insert(0, "..") + import czsc import pandas as pd import streamlit as st st.set_page_config(layout="wide") - +st.write(czsc.__version__) dfw = pd.read_feather(r"C:\Users\zengb\Downloads\ST组件样例数据\时序持仓权重样例数据.feather") st.subheader("时序持仓策略回测样例", anchor="时序持仓策略回测样例", divider="rainbow") -czsc.show_weight_backtest(dfw, fee=2, digits=2, show_drawdowns=True, show_monthly_return=True, show_yearly_stats=True) +czsc.show_weight_backtest( + dfw, fee=2, digits=2, show_drawdowns=True, show_monthly_return=True, show_yearly_stats=True, show_splited_daily=True +) diff --git "a/examples/develop/\345\257\271\346\257\224numpy\344\270\216talib\347\232\204\351\200\237\345\272\246.py" "b/examples/develop/\345\257\271\346\257\224numpy\344\270\216talib\347\232\204\351\200\237\345\272\246.py" new file mode 100644 index 000000000..75a47430b --- /dev/null +++ "b/examples/develop/\345\257\271\346\257\224numpy\344\270\216talib\347\232\204\351\200\237\345\272\246.py" @@ -0,0 +1,19 @@ +import sys + +sys.path.insert(0, r"A:\ZB\git_repo\waditu\czsc") +import talib +from czsc.utils import ta +from czsc.connectors import cooperation as coo + + +df = coo.get_raw_bars(symbol="SFIC9001", freq="30分钟", fq="后复权", sdt="20100101", edt="20210301", raw_bars=False) + + +def test_with_numpy(): + df1 = df.copy() + df1["x"] = ta.LINEARREG_ANGLE(df["close"].values, 10) + + +def test_with_talib(): + df1 = df.copy() + df1["x"] = talib.LINEARREG_ANGLE(df["close"].values, 10) diff --git a/requirements.txt b/requirements.txt index 0cfba2fac..0aa9f37bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,6 @@ loguru click pytest tenacity -requests-toolbelt plotly parse lightgbm @@ -26,8 +25,7 @@ oss2 statsmodels optuna cryptography -tqsdk pytz flask -setuptools -scipy \ No newline at end of file +scipy +requests_toolbelt \ No newline at end of file diff --git a/test/test_utils.py b/test/test_utils.py index 109967e2b..9c984dd1e 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -107,14 +107,16 @@ def test_daily_performance(): # Test case 1: empty daily returns result = daily_performance([]) assert result == { - "下行波动率": 0, "绝对收益": 0, "年化": 0, "夏普": 0, "最大回撤": 0, "卡玛": 0, "日胜率": 0, + "日盈亏比": 0, + "日赢面": 0, "年化波动率": 0, + "下行波动率": 0, "非零覆盖": 0, "盈亏平衡点": 0, "新高间隔": 0, @@ -125,14 +127,16 @@ def test_daily_performance(): # Test case 2: daily returns with zero standard deviation result = daily_performance([1, 1, 1, 1, 1]) assert result == { - "下行波动率": 0, "绝对收益": 0, "年化": 0, "夏普": 0, "最大回撤": 0, "卡玛": 0, "日胜率": 0, + "日盈亏比": 0, + "日赢面": 0, "年化波动率": 0, + "下行波动率": 0, "非零覆盖": 0, "盈亏平衡点": 0, "新高间隔": 0, @@ -143,14 +147,16 @@ def test_daily_performance(): # Test case 3: daily returns with all zeros result = daily_performance([0, 0, 0, 0, 0]) assert result == { - "下行波动率": 0, "绝对收益": 0, "年化": 0, "夏普": 0, "最大回撤": 0, "卡玛": 0, "日胜率": 0, + "日盈亏比": 0, + "日赢面": 0, "年化波动率": 0, + "下行波动率": 0, "非零覆盖": 0, "盈亏平衡点": 0, "新高间隔": 0, @@ -162,14 +168,16 @@ def test_daily_performance(): daily_returns = np.array([0.01, 0.02, -0.01, 0.03, 0.02, -0.02, 0.01, -0.01, 0.02, 0.01]) result = daily_performance(daily_returns) assert result == { - "下行波动率": 0.0748, "绝对收益": 0.08, "年化": 2.016, "夏普": 5, "最大回撤": 0.02, "卡玛": 10, "日胜率": 0.7, + "日盈亏比": 1.2857, + "日赢面": 0.6, "年化波动率": 0.2439, + "下行波动率": 0.0748, "非零覆盖": 1.0, "盈亏平衡点": 0.7, "新高间隔": 5, @@ -180,14 +188,16 @@ def test_daily_performance(): # Test case 5: normal daily returns with different input type result = daily_performance([0.01, 0.02, -0.01, 0.03, 0.02, -0.02, 0.01, -0.01, 0.02, 0.01]) assert result == { - "下行波动率": 0.0748, "绝对收益": 0.08, "年化": 2.016, "夏普": 5, "最大回撤": 0.02, "卡玛": 10, "日胜率": 0.7, + "日盈亏比": 1.2857, + "日赢面": 0.6, "年化波动率": 0.2439, + "下行波动率": 0.0748, "非零覆盖": 1.0, "盈亏平衡点": 0.7, "新高间隔": 5,