diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0d20a5b60..64963e729 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -5,7 +5,7 @@ name: Python package on: push: - branches: [ master, V0.9.57 ] + branches: [ master, V0.9.58 ] pull_request: branches: [ master ] diff --git a/README.md b/README.md index d1a6c63a2..8e8a421f8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ >为什么要去了解其他理论,就是这些理论操作者的行为模式,将构成以后我们猎杀的对象,他们操作模式的缺陷,就是以后猎杀他们的最好武器,这就如同学独孤九剑,必须学会发现所有派别招数的缺陷,这也是本ID理论学习中一个极为关键的步骤。 +>真正的预测,就是不测而测。所有预测的基础,就是分类,把所有可能的情况进行完全分类。有人可能说,分类以后,把不可能的排除,最后一个结果就是精确的。 +>这是脑子锈了的想法,任何的排除,等价于一次预测,每排除一个分类,按概率的乘法原则,就使得最后的所谓精确变得越不精确,最后还是逃不掉概率的套子。 +>对于预测分类的唯一正确原则就是不进行任何排除,而是要严格分清每种情况的边界条件。任何的分类,其实都等价于一个分段函数,就是要把这分段函数的边界条件确定清楚。 +>边界条件分段后,就要确定一旦发生哪种情况就如何操作,也就是把操作也同样给分段化了。然后,把所有情况交给市场本身,让市场自己去当下选择。 +>所有的操作,其实都是根据不同分段边界的一个结果,只是每个人的分段边界不同而已。因此,问题不是去预测什么,而是确定分段边界。 ## 知识星球 diff --git a/czsc/__init__.py b/czsc/__init__.py index fc6a164b8..a4ae2e0d0 100644 --- a/czsc/__init__.py +++ b/czsc/__init__.py @@ -202,13 +202,15 @@ from czsc.eda import ( remove_beta_effects, vwap, twap, cross_sectional_strategy, + judge_factor_direction, + monotonicity, ) -__version__ = "0.9.57" +__version__ = "0.9.58" __author__ = "zengbin93" __email__ = "zeng_bin8888@163.com" -__date__ = "20240726" +__date__ = "20240808" def welcome(): diff --git a/czsc/connectors/qmt_connector.py b/czsc/connectors/qmt_connector.py index f07f5ce55..e30364872 100644 --- a/czsc/connectors/qmt_connector.py +++ b/czsc/connectors/qmt_connector.py @@ -138,7 +138,7 @@ def start_qmt_exe(acc, pwd, qmt_exe, title, max_retry=6, **kwargs): wait_seconds = kwargs.get("wait_seconds", 6) i = 0 - while not find_exe_window(acc): + while not find_exe_window(title): if i > max_retry: logger.warning(f"QMT连续{i}次尝试依旧无法启动,请人工检查!") break diff --git a/czsc/eda.py b/czsc/eda.py index bd62e54bc..99c37c3bc 100644 --- a/czsc/eda.py +++ b/czsc/eda.py @@ -39,15 +39,17 @@ def remove_beta_effects(df, **kwargs): - factor: str, 因子列名 - betas: list, beta 列名列表 - linear_model: str, 线性模型,可选 ridge、linear 或 lasso + - linear_model_params: dict, 线性模型参数, 默认为空, 需要传入字典,根据模型不同参数不同 :return: DataFrame """ linear_model = kwargs.get("linear_model", "ridge") + linear_model_params = kwargs.get("linear_model_params", {}) linear = { - "ridge": Ridge(), - "linear": LinearRegression(), - "lasso": Lasso(), + "ridge": Ridge, + "linear": LinearRegression, + "lasso": Lasso, } assert linear_model in linear.keys(), "linear_model 参数必须为 ridge、linear 或 lasso" Model = linear[linear_model] @@ -71,7 +73,7 @@ def remove_beta_effects(df, **kwargs): x = dfg[betas].values y = dfg[factor].values - model = Model().fit(x, y) + model = Model(**linear_model_params).fit(x, y) dfg[factor] = y - model.predict(x) rows.append(dfg) @@ -113,7 +115,47 @@ def cross_sectional_strategy(df, factor, **kwargs): dfa = dfg.sort_values(factor, ascending=False).head(long_num) dfb = dfg.sort_values(factor, ascending=True).head(short_num) - df.loc[dfa.index, "weight"] = 1 / long_num - df.loc[dfb.index, "weight"] = -1 / short_num + if long_num > 0: + df.loc[dfa.index, "weight"] = 1 / long_num + if short_num > 0: + df.loc[dfb.index, "weight"] = -1 / short_num return df + + +def judge_factor_direction(df: pd.DataFrame, factor, target='n1b', by='symbol', **kwargs): + """判断因子的方向,正向还是反向 + + :param df: pd.DataFrame, 数据源,必须包含 symbol, dt, target, factor 列 + :param factor: str, 因子名称 + :param target: str, 目标名称,默认为 n1b,表示下一根K线的涨跌幅 + :param by: str, 分组字段,默认为 symbol,表示按品种分组(时序);也可以按 dt 分组,表示按时间分组(截面) + :param kwargs: dict, 其他参数 + - method: str, 相关系数计算方法,默认为 pearson,可选 pearson, kendall, spearman + :return: str, positive or negative + """ + assert by in df.columns, f"数据中不存在 {by} 字段" + assert factor in df.columns, f"数据中不存在 {factor} 字段" + assert target in df.columns, f"数据中不存在 {target} 字段" + + if by == "dt" and df['symbol'].nunique() < 2: + raise ValueError("品种数量过少,无法在时间截面上计算因子有效性方向") + + if by == "symbol" and df['dt'].nunique() < 2: + raise ValueError("时间序列数据量过少,无法在品种上计算因子有效性方向") + + method = kwargs.get("method", "pearson") + dfc = df.groupby(by)[[factor, target]].corr(method=method).unstack().iloc[:, 1].reset_index() + return "positive" if dfc[factor].mean().iloc[0] >= 0 else "negative" + + +def monotonicity(sequence): + """计算序列的单调性 + + 原理:计算序列与自然数序列的相关系数,系数越接近1,表示单调递增;系数越接近-1,表示单调递减;接近0表示无序 + + :param sequence: list, tuple 序列 + :return: float, 单调性系数 + """ + from scipy.stats import spearmanr + return spearmanr(sequence, range(len(sequence)))[0] diff --git a/czsc/fsa/__init__.py b/czsc/fsa/__init__.py index cfb253134..c3499ff10 100644 --- a/czsc/fsa/__init__.py +++ b/czsc/fsa/__init__.py @@ -3,7 +3,7 @@ author: zengbin93 email: zeng_bin8888@163.com create_dt: 2022/12/16 19:37 -describe: +describe: """ import requests @@ -36,7 +36,7 @@ def push_text(text: str, key: str) -> None: logger.error(f"推送消息失败: {e}") -def push_card(card: str, key: str) -> None: +def push_card(card: dict, key: str) -> None: """使用自定义机器人推送卡片消息到飞书群聊 如何在群组中使用机器人: @@ -150,7 +150,7 @@ def update_spreadsheet(df: pd.DataFrame, spreadsheet_token: str, sheet_id: str, 获取 sheet_id - https://open.feishu.cn/document/server-docs/docs/sheets-v3/spreadsheet-sheet/query?appId=cli_a3077015cc39500e - :param df: datafream内容 + :param df: dataframe内容 :param spreadsheet_token: 表格对应的token,url获取 :param sheet_id: 工作表的id :param kwargs: @@ -175,6 +175,6 @@ def update_spreadsheet(df: pd.DataFrame, spreadsheet_token: str, sheet_id: str, else: logger.error(b) return 0 - except Exception: - logger.exception("更新飞书表格失败") + except Exception as e: + logger.exception(f"更新飞书表格失败: {e}") return 0 diff --git a/czsc/fsa/bi_table.py b/czsc/fsa/bi_table.py index 8a16e506c..859b50b61 100644 --- a/czsc/fsa/bi_table.py +++ b/czsc/fsa/bi_table.py @@ -725,6 +725,10 @@ def read_table(self, table_id, **kwargs): rows = [] res = self.list_records(table_id, **kwargs)["data"] total = res["total"] + + if total == 0: + return pd.DataFrame() + rows.extend(res["items"]) while res["has_more"]: res = self.list_records(table_id, page_token=res["page_token"], **kwargs)["data"] diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py index 436cbffc0..f89ae8415 100644 --- a/czsc/traders/weight_backtest.py +++ b/czsc/traders/weight_backtest.py @@ -565,7 +565,13 @@ def backtest(self, n_jobs=1): dfw = self.dfw.copy() long_rate = dfw[dfw["weight"] > 0].shape[0] / dfw.shape[0] short_rate = dfw[dfw["weight"] < 0].shape[0] / dfw.shape[0] - stats.update({"多头占比": long_rate, "空头占比": short_rate}) + stats.update({"多头占比": round(long_rate, 4), "空头占比": round(short_rate, 4)}) + + alpha = self.alpha.copy() + stats["与基准相关性"] = round(alpha["策略"].corr(alpha["基准"]), 4) + alpha_short = alpha[alpha["基准"] < 0].copy() + stats["与基准空头相关性"] = round(alpha_short["策略"].corr(alpha_short["基准"]), 4) + stats["品种数量"] = len(symbols) res["绩效评价"] = stats return res diff --git a/czsc/utils/st_components.py b/czsc/utils/st_components.py index b3491c75d..ee0fe5f3c 100644 --- a/czsc/utils/st_components.py +++ b/czsc/utils/st_components.py @@ -1,3 +1,5 @@ +# 飞书文档:https://s0cqcxuy3p.feishu.cn/wiki/AATuw5vN7iN9XbkVPuwcE186n9f + import czsc import hashlib import optuna @@ -22,12 +24,15 @@ def show_daily_return(df: pd.DataFrame, **kwargs): - legend_only_cols: list,仅在图例中展示的列名 - use_st_table: bool,是否使用 st.table 展示绩效指标,默认为 False - plot_cumsum: bool,是否展示日收益累计曲线,默认为 True + - yearly_days: int,年交易天数,默认为 252 + - show_dailys: bool,是否展示日收益数据详情,默认为 False """ 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", 252) df = df.copy().fillna(0) df.sort_index(inplace=True, ascending=True) @@ -35,13 +40,13 @@ def show_daily_return(df: pd.DataFrame, **kwargs): def _stats(df_, type_="持有日"): df_ = df_.copy() stats = [] - for col in df_.columns: + for _col in df_.columns: if type_ == "持有日": - col_stats = czsc.daily_performance([x for x in df_[col] if x != 0]) + col_stats = czsc.daily_performance([x for x in df_[_col] if x != 0], yearly_days=yearly_days) else: assert type_ == "交易日", "type_ 参数必须是 持有日 或 交易日" - col_stats = czsc.daily_performance(df_[col]) - col_stats["日收益名称"] = col + col_stats = czsc.daily_performance(df_[_col], yearly_days=yearly_days) + col_stats["日收益名称"] = _col stats.append(col_stats) stats = pd.DataFrame(stats).set_index("日收益名称") @@ -51,6 +56,7 @@ def _stats(df_, type_="持有日"): 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=["非零覆盖"]) @@ -61,6 +67,7 @@ def _stats(df_, type_="持有日"): { "盈亏平衡点": "{:.2f}", "年化波动率": "{:.2%}", + "下行波动率": "{:.2%}", "最大回撤": "{:.2%}", "卡玛": "{:.2f}", "年化": "{:.2%}", @@ -80,17 +87,22 @@ def _stats(df_, type_="持有日"): with st.container(): sub_title = kwargs.get("sub_title", "") if sub_title: - st.subheader(sub_title, divider="rainbow") + st.subheader(sub_title, divider="rainbow", anchor=sub_title) + if kwargs.get("show_dailys", False): + with st.expander("日收益数据详情", expanded=False): + st.dataframe(df, use_container_width=True) with st.expander("交易日绩效指标", expanded=True): if use_st_table: st.table(_stats(df, type_="交易日")) else: st.dataframe(_stats(df, type_="交易日"), use_container_width=True) + st.caption("交易日:交易所指定的交易日,或者有收益发生变化的日期") if kwargs.get("stat_hold_days", True): with st.expander("持有日绩效指标", expanded=False): st.dataframe(_stats(df, type_="持有日"), use_container_width=True) + st.caption("持有日:在交易日的基础上,将收益率为0的日期删除") if kwargs.get("plot_cumsum", True): df = df.cumsum() @@ -127,7 +139,7 @@ def show_monthly_return(df, ret_col="total", sub_title="月度累计收益", **k df.sort_index(inplace=True, ascending=True) if sub_title: - st.subheader(sub_title, divider="rainbow") + st.subheader(sub_title, divider="rainbow", anchor=sub_title) monthly = df[[ret_col]].resample("ME").sum() monthly["year"] = monthly.index.year @@ -185,8 +197,10 @@ def show_sectional_ic(df, x_col, y_col, method="pearson", **kwargs): :param y_col: str,收益列名 :param method: str,计算IC的方法,可选 pearson 和 spearman :param kwargs: + - show_cumsum_ic: bool,是否展示累计IC曲线,默认为 True - show_factor_histgram: bool,是否展示因子数据分布图,默认为 False + """ dfc, res = czsc.cross_sectional_ic(df, x_col=x_col, y_col=y_col, dt_col="dt", method=method) @@ -315,7 +329,6 @@ def show_factor_layering(df, factor, target="n1b", **kwargs): :param kwargs: - n: 分层数量,默认为10 - """ n = kwargs.get("n", 10) df = czsc.feture_cross_layering(df, factor, n=n) @@ -325,7 +338,26 @@ def show_factor_layering(df, factor, target="n1b", **kwargs): if "第00层" in mrr.columns: mrr.drop(columns=["第00层"], inplace=True) - czsc.show_daily_return(mrr, stat_hold_days=False) + # 计算每层的累计收益率 + dfc = mrr.sum(axis=0).to_frame("绝对收益") + + dfc["text"] = dfc["绝对收益"].apply(lambda x: f"{x:.2%}") + fig = px.bar( + dfc, + y="绝对收益", + title="因子分层绝对收益 | 单调性:{:.2%}".format(czsc.monotonicity(dfc["绝对收益"])), + color="绝对收益", + color_continuous_scale="RdYlGn_r", + text="text", + ) + st.plotly_chart(fig, use_container_width=True) + + czsc.show_daily_return( + mrr, + stat_hold_days=False, + yearly_days=kwargs.get("yearly_days", 252), + show_dailys=kwargs.get("show_dailys", False), + ) def show_symbol_factor_layering(df, x_col, y_col="n1b", **kwargs): @@ -422,7 +454,7 @@ def show_weight_backtest(dfw, **kwargs): stat = wb.results["绩效评价"] st.divider() - c1, c2, c3, c4, c5, c6, c7, c8, c9, c10 = st.columns([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11 = st.columns([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) c1.metric("盈亏平衡点", f"{stat['盈亏平衡点']:.2%}") c2.metric("单笔收益(BP)", f"{stat['单笔收益']}") c3.metric("交易胜率", f"{stat['交易胜率']:.2%}") @@ -433,6 +465,7 @@ def show_weight_backtest(dfw, **kwargs): c8.metric("卡玛比率", f"{stat['卡玛']:.2f}") c9.metric("年化波动率", f"{stat['年化波动率']:.2%}") c10.metric("多头占比", f"{stat['多头占比']:.2%}") + c11.metric("空头占比", f"{stat['空头占比']:.2%}") st.divider() dret = wb.results["品种等权日收益"].copy() @@ -477,6 +510,7 @@ def show_splited_daily(df, ret_col, **kwargs): sub_title: str, 子标题 """ + yearly_days = kwargs.get("yearly_days", 252) if not df.index.dtype == "datetime64[ns]": df["dt"] = pd.to_datetime(df["dt"]) df.set_index("dt", inplace=True) @@ -487,7 +521,7 @@ def show_splited_daily(df, ret_col, **kwargs): sub_title = kwargs.get("sub_title", "") if sub_title: - st.subheader(sub_title, divider="rainbow") + st.subheader(sub_title, divider="rainbow", anchor=sub_title) last_dt = df.index[-1] sdt_map = { @@ -504,7 +538,7 @@ 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]) + 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 @@ -520,6 +554,7 @@ def show_splited_daily(df, ret_col, **kwargs): "最大回撤", "卡玛", "年化波动率", + "下行波动率", "非零覆盖", "日胜率", "盈亏平衡点", @@ -532,6 +567,7 @@ def show_splited_daily(df, ret_col, **kwargs): 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=["非零覆盖"]) @@ -539,6 +575,7 @@ def show_splited_daily(df, ret_col, **kwargs): { "盈亏平衡点": "{:.2f}", "年化波动率": "{:.2%}", + "下行波动率": "{:.2%}", "最大回撤": "{:.2%}", "卡玛": "{:.2f}", "年化": "{:.2%}", @@ -569,10 +606,11 @@ def show_yearly_stats(df, ret_col, **kwargs): df.sort_index(inplace=True, ascending=True) df["年份"] = df.index.year + yearly_days = max(len(df_) for year, df_ in df.groupby("年份")) _stats = [] for year, df_ in df.groupby("年份"): - _yst = czsc.daily_performance(df_[ret_col].to_list()) + _yst = czsc.daily_performance(df_[ret_col].to_list(), yearly_days=yearly_days) _yst["年份"] = year _stats.append(_yst) @@ -584,6 +622,7 @@ def show_yearly_stats(df, ret_col, **kwargs): 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=["非零覆盖"]) @@ -595,6 +634,7 @@ def show_yearly_stats(df, ret_col, **kwargs): { "盈亏平衡点": "{:.2f}", "年化波动率": "{:.2%}", + "下行波动率": "{:.2%}", "最大回撤": "{:.2%}", "卡玛": "{:.2f}", "年化": "{:.2%}", @@ -608,8 +648,9 @@ def show_yearly_stats(df, ret_col, **kwargs): } ) - if kwargs.get("sub_title"): - st.subheader(kwargs.get("sub_title"), divider="rainbow") + sub_title = kwargs.get("sub_title", "") + if sub_title: + st.subheader(sub_title, divider="rainbow", anchor=sub_title) st.dataframe(stats, use_container_width=True) @@ -829,11 +870,11 @@ def show_cointegration(df, col1, col2, **kwargs): ) l1, l2, l3 = st.columns(3) - coint_t, pvalue, crit_value = coint(df[col1], df[col2]) - l1.metric("协整检验统计量", str(round(coint_t, 3)), help="单位根检验的T统计量。") + t, p, crit_value = coint(df[col1], df[col2]) + l1.metric("协整检验统计量", str(round(t, 3)), help="单位根检验的T统计量。") l2.metric( "协整检验P值(不协整的概率)", - f"{pvalue:.2%}", + f"{p:.2%}", help="两个时间序列不协整的概率,低于5%则可以认为两个时间序列协整。", ) fig = px.line(df, x=df.index, y=[col1, col2]) @@ -1020,7 +1061,7 @@ def show_rolling_daily_performance(df, ret_col, **kwargs): sub_title = kwargs.get("sub_title", "滚动日收益绩效") if sub_title: - st.subheader(sub_title, divider="rainbow") + st.subheader(sub_title, divider="rainbow", anchor=sub_title) c1, c2, c3 = st.columns(3) window = c1.number_input("滚动窗口(自然日)", value=365 * 3, min_value=365, max_value=3650) diff --git a/czsc/utils/stats.py b/czsc/utils/stats.py index 5943407ea..2c5695de6 100644 --- a/czsc/utils/stats.py +++ b/czsc/utils/stats.py @@ -78,10 +78,11 @@ def daily_performance(daily_returns, **kwargs): - 卡玛比率 = 年化收益率 / 最大回撤(如果最大回撤不为零,则除以最大回撤;否则为10) - 日胜率 = 大于零的日收益率的个数 / 日收益率的总个数 - 年化波动率 = 日收益率的标准差 * 标准差的根号252 + - 下行波动率 = 日收益率中小于零的日收益率的标准差 * 标准差的根号252 - 非零覆盖 = 非零的日收益率个数 / 日收益率的总个数 - 回撤风险 = 最大回撤 / 年化波动率;一般认为 1 以下为低风险,1-2 为中风险,2 以上为高风险 - 4. 将所有指标的值存储在一个字典中,其中键为指标名称,值为相应的计算结果。 + 4. 将所有指标的值存储在字典中,其中键为指标名称,值为相应的计算结果。 :param daily_returns: 日收益率数据,样例: [0.01, 0.02, -0.01, 0.03, 0.02, -0.02, 0.01, -0.01, 0.02, 0.01] @@ -101,6 +102,7 @@ def daily_performance(daily_returns, **kwargs): "卡玛": 0, "日胜率": 0, "年化波动率": 0, + "下行波动率": 0, "非零覆盖": 0, "盈亏平衡点": 0, "新高间隔": 0, @@ -118,6 +120,8 @@ def daily_performance(daily_returns, **kwargs): annual_volatility = np.std(daily_returns) * np.sqrt(yearly_days) none_zero_cover = len(daily_returns[daily_returns != 0]) / len(daily_returns) + downside_volatility = np.std(daily_returns[daily_returns < 0]) * np.sqrt(yearly_days) + # 计算最大新高间隔 max_interval = Counter(np.maximum.accumulate(cum_returns).tolist()).most_common(1)[0][1] @@ -141,6 +145,7 @@ def __min_max(x, min_val, max_val, digits=4): "卡玛": __min_max(kama, -10, 10, 2), "日胜率": round(win_pct, 4), "年化波动率": round(annual_volatility, 4), + "下行波动率": round(downside_volatility, 4), "非零覆盖": round(none_zero_cover, 4), "盈亏平衡点": round(cal_break_even_point(daily_returns), 4), "新高间隔": max_interval, 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 49f3b98df..3783b0ef4 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" @@ -6,8 +6,5 @@ dfw = pd.read_feather(r"C:\Users\zengb\Downloads\ST组件样例数据\时序持仓权重样例数据.feather") -if __name__ == "__main__": - 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 - ) +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) 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/\345\233\240\345\255\220\345\210\206\346\236\220.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/\345\233\240\345\255\220\345\210\206\346\236\220.py" new file mode 100644 index 000000000..be007a159 --- /dev/null +++ "b/examples/Streamlit\347\273\204\344\273\266\345\272\223\344\275\277\347\224\250\346\241\210\344\276\213/\345\233\240\345\255\220\345\210\206\346\236\220.py" @@ -0,0 +1,13 @@ +import czsc +import pandas as pd +import streamlit as st + +st.set_page_config(layout="wide") + +df = pd.read_feather(r"C:\Users\zengb\Downloads\ST组件样例数据\因子数据样例.feather") +factor = [x for x in df.columns if x.startswith("F#")][0] +# czsc.show_factor_layering(df, factor=factor, target="n1b", n=10) + +# czsc.show_feature_returns(df, factor, target="n1b", fit_intercept=True) + +czsc.show_sectional_ic(df, factor, "n1b", show_factor_histgram=True, show_cumsum_ic=True) 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/\346\227\245\346\224\266\347\233\212\345\210\206\346\236\220.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/\346\227\245\346\224\266\347\233\212\345\210\206\346\236\220.py" new file mode 100644 index 000000000..64c442fa7 --- /dev/null +++ "b/examples/Streamlit\347\273\204\344\273\266\345\272\223\344\275\277\347\224\250\346\241\210\344\276\213/\346\227\245\346\224\266\347\233\212\345\210\206\346\236\220.py" @@ -0,0 +1,17 @@ +import czsc +import pandas as pd +import streamlit as st + +st.set_page_config(layout="wide") + +dfw = pd.read_feather(r"C:\Users\zengb\Downloads\ST组件样例数据\截面持仓权重样例数据.feather") +st.subheader("截面数据回测", divider="rainbow", anchor="截面数据回测") +czsc.show_holds_backtest( + dfw, + fee=2, + digits=2, + show_drawdowns=True, + show_splited_daily=True, + show_monthly_return=True, + show_yearly_stats=True, +) diff --git a/examples/gm_0701.py b/examples/dropit/gm_0701.py similarity index 100% rename from examples/gm_0701.py rename to examples/dropit/gm_0701.py diff --git a/examples/gm_backtest.py b/examples/dropit/gm_backtest.py similarity index 55% rename from examples/gm_backtest.py rename to examples/dropit/gm_backtest.py index 428cc9994..8fe3b9b89 100644 --- a/examples/gm_backtest.py +++ b/examples/dropit/gm_backtest.py @@ -32,36 +32,37 @@ os.environ['backtest_slippage_ratio'] = '0.0005' """ import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') + +sys.path.insert(0, "..") +sys.path.insert(0, "../..") from czsc.connectors.gm_connector import * from czsc.strategies import CzscStrategyExample2 -os.environ['strategy_id'] = 'b24661f5-838d-11ed-882c-988fe0675a5b' -os.environ['max_sym_pos'] = '0.5' -os.environ['path_gm_logs'] = 'C:/gm_logs' -os.environ['backtest_start_time'] = '2020-01-01 14:30:00' -os.environ['backtest_end_time'] = '2020-12-31 15:30:00' -os.environ['backtest_initial_cash'] = '100000000' -os.environ['backtest_transaction_ratio'] = '1' -os.environ['backtest_commission_ratio'] = '0.001' -os.environ['backtest_slippage_ratio'] = '0.0005' +os.environ["strategy_id"] = "b24661f5-838d-11ed-882c-988fe0675a5b" +os.environ["max_sym_pos"] = "0.5" +os.environ["path_gm_logs"] = "C:/gm_logs" +os.environ["backtest_start_time"] = "2020-01-01 14:30:00" +os.environ["backtest_end_time"] = "2020-12-31 15:30:00" +os.environ["backtest_initial_cash"] = "100000000" +os.environ["backtest_transaction_ratio"] = "1" +os.environ["backtest_commission_ratio"] = "0.001" +os.environ["backtest_slippage_ratio"] = "0.0005" def init(context): symbols = [ - 'SZSE.300014', - 'SHSE.600143', - 'SZSE.002216', - 'SZSE.300033', - 'SZSE.000795', - 'SZSE.002739', - 'SHSE.600000', - 'SHSE.600008', - 'SHSE.600006', - 'SHSE.600009', - 'SHSE.600010', - 'SHSE.600011' + "SZSE.300014", + "SHSE.600143", + "SZSE.002216", + "SZSE.300033", + "SZSE.000795", + "SZSE.002739", + "SHSE.600000", + "SHSE.600008", + "SHSE.600006", + "SHSE.600009", + "SHSE.600010", + "SHSE.600011", ] # 配置消息推送服务,支持飞书、企业微信通道 @@ -69,11 +70,11 @@ def init(context): "wx_key": "", "fs_app": { # 飞书应用的 app_id 和 app_secret - 'feishu_app_id': 'cli_a30770****39500e', - 'feishu_app_secret': 'jVoMf688Gbw2*****HhoVbZ7fiTkTkgg', + "feishu_app_id": "cli_a30770****39500e", + "feishu_app_secret": "jVoMf688Gbw2*****HhoVbZ7fiTkTkgg", # 指定消息推送给哪些飞书用户, - 'feishu_members': ['ou_6fa04b5b4d8*****fdc87d267e8f2a270'], - } + "feishu_members": ["ou_6fa04b5b4d8*****fdc87d267e8f2a270"], + }, } name = "stocks_sma5" @@ -84,14 +85,18 @@ def init(context): init_context_schedule(context) -if __name__ == '__main__': - run(filename=os.path.basename(__file__), token=gm_token, mode=MODE_BACKTEST, - strategy_id=os.environ['strategy_id'], - backtest_start_time=os.environ['backtest_start_time'], - backtest_end_time=os.environ['backtest_end_time'], - backtest_initial_cash=int(os.environ['backtest_initial_cash']), - backtest_transaction_ratio=float(os.environ['backtest_transaction_ratio']), - backtest_commission_ratio=float(os.environ['backtest_commission_ratio']), - backtest_slippage_ratio=float(os.environ['backtest_slippage_ratio']), +if __name__ == "__main__": + run( + filename=os.path.basename(__file__), + token=gm_token, + mode=MODE_BACKTEST, + strategy_id=os.environ["strategy_id"], + backtest_start_time=os.environ["backtest_start_time"], + backtest_end_time=os.environ["backtest_end_time"], + backtest_initial_cash=int(os.environ["backtest_initial_cash"]), + backtest_transaction_ratio=float(os.environ["backtest_transaction_ratio"]), + backtest_commission_ratio=float(os.environ["backtest_commission_ratio"]), + backtest_slippage_ratio=float(os.environ["backtest_slippage_ratio"]), backtest_adjust=ADJUST_PREV, - backtest_check_cache=1) + backtest_check_cache=1, + ) diff --git a/examples/gm_realtime.py b/examples/dropit/gm_realtime.py similarity index 59% rename from examples/gm_realtime.py rename to examples/dropit/gm_realtime.py index 48db60dd4..165d10b10 100644 --- a/examples/gm_realtime.py +++ b/examples/dropit/gm_realtime.py @@ -20,44 +20,45 @@ os.environ['path_gm_logs'] = 'C:/gm_logs' """ import sys -sys.path.insert(0, '.') -sys.path.insert(0, '..') + +sys.path.insert(0, "..") +sys.path.insert(0, "../..") from czsc.connectors.gm_connector import * from czsc.strategies import CzscStrategyExample2 -os.environ['strategy_id'] = '43b099b8-*****-11ed-99a6-988fe0675a5b' -os.environ['account_id'] = '613019f5-****-11ed-bdad-00163e18a8b3' -os.environ['max_sym_pos'] = '0.5' -os.environ['path_gm_logs'] = 'C:/gm_logs' +os.environ["strategy_id"] = "43b099b8-*****-11ed-99a6-988fe0675a5b" +os.environ["account_id"] = "613019f5-****-11ed-bdad-00163e18a8b3" +os.environ["max_sym_pos"] = "0.5" +os.environ["path_gm_logs"] = "C:/gm_logs" def init(context): # 股票池配置 symbols = [ - 'SZSE.300014', - 'SHSE.600143', - 'SZSE.002216', - 'SZSE.300033', - 'SZSE.000795', - 'SZSE.002739', - 'SHSE.600000', - 'SHSE.600008', - 'SHSE.600006', - 'SHSE.600009', - 'SHSE.600010', - 'SHSE.600011' + "SZSE.300014", + "SHSE.600143", + "SZSE.002216", + "SZSE.300033", + "SZSE.000795", + "SZSE.002739", + "SHSE.600000", + "SHSE.600008", + "SHSE.600006", + "SHSE.600009", + "SHSE.600010", + "SHSE.600011", ] # 配置消息推送服务,支持飞书、企业微信通道 context.push_msg_conf = { - "wx_key": os.environ.get('wx_key', None), + "wx_key": os.environ.get("wx_key", None), "fs_app": { # 飞书消息推送 - 'feishu_app_id': 'cli_*****015cc39500e', - 'feishu_app_secret': 'jV******688Gbw20fkR2HhoVbZ7fiTkTkgg', - 'feishu_members': ['ou_6fa*****d853e9fdc87d267e8f2a270'], - } + "feishu_app_id": "cli_*****015cc39500e", + "feishu_app_secret": "jV******688Gbw20fkR2HhoVbZ7fiTkTkgg", + "feishu_members": ["ou_6fa*****d853e9fdc87d267e8f2a270"], + }, } strategy = CzscStrategyExample2 init_context_universal(context, strategy.__name__) @@ -66,6 +67,5 @@ def init(context): init_context_schedule(context) -if __name__ == '__main__': - run(filename=os.path.basename(__file__), token=gm_token, mode=MODE_LIVE, strategy_id=os.environ['strategy_id']) - +if __name__ == "__main__": + run(filename=os.path.basename(__file__), token=gm_token, mode=MODE_LIVE, strategy_id=os.environ["strategy_id"]) diff --git a/requirements.txt b/requirements.txt index 2206bd379..0cfba2fac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,33 @@ requests>=2.24.0 pyecharts>=1.9.1 -tqdm +tqdm>=4.66.4 pandas>=1.1.0 numpy>=1.16.5 -tushare -python-docx>=0.8.11 +tushare>=1.4.6 +python-docx matplotlib seaborn -Deprecated>=1.2.12 -scikit-learn>=0.24.1 +Deprecated +scikit-learn dill openpyxl pyarrow -loguru>=0.6.0 +loguru click pytest -tenacity>=8.1.0 -requests-toolbelt>=0.10.1 -plotly>=5.11.0 -parse>=1.19.0 -lightgbm>=4.0.0 +tenacity +requests-toolbelt +plotly +parse +lightgbm streamlit redis oss2 statsmodels optuna -cryptography \ No newline at end of file +cryptography +tqsdk +pytz +flask +setuptools +scipy \ No newline at end of file diff --git a/test/test_utils.py b/test/test_utils.py index 3e444b110..109967e2b 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -107,6 +107,7 @@ def test_daily_performance(): # Test case 1: empty daily returns result = daily_performance([]) assert result == { + "下行波动率": 0, "绝对收益": 0, "年化": 0, "夏普": 0, @@ -124,6 +125,7 @@ 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, @@ -141,6 +143,7 @@ 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, @@ -159,6 +162,7 @@ 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, @@ -176,6 +180,7 @@ 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,