本文仅用于学习研究与技术交流,不构成任何投资建议。

之前在聚宽上看到不少大佬分享的 行业宽度展示 脚本,觉得特别酷。每天跑一下脚本,就能立刻知道最近资金在往哪些行业集中,哪些行业正在悄悄升温。

聚宽链接:https://www.joinquant.com/view/community/detail/55fabfea977bddeaf91f3e728c3e68a1

这种从“全市场视角”快速把握热点的感觉,就特别爽、特别酷炫。

聚宽行业宽度展示

不过,作为一名 本地 Python 量化 的强迫症患者,对于不能在本地 Python 环境运行的功能总觉得缺少点什么,总觉得不够带劲。

于是,我开始琢磨:如何将该功能在本地 Python 环境中实现

数据

行业宽度的计算主要需要两方面的数据:

  1. 全市场的历史行情数据
  2. 股票所属的行业分类数据

幸运的是,这两样数据都可以从 Tushare 中取到。而 Tushare 数据的获取,对个人用户而言门槛也不高。

全市场行情

其中一个方法就是用 Tushare 的 pro.daily() 接口。该接口可以获取多只股票在某一段时间的行情,包括收盘价 close 等。

然而,这里有几个问题:

  • pro.daily() 单次最多只能提取 6000 条数据;
  • 全市场股票有 5000 多只,如果一次性取长区间,很容易超过限制;
  • 原始行情没有复权,直接用未复权价格做计算并不科学。

全市场股票数量很多,所以可以选择 分批获取,然后将获取的数据拼接起来。

# pro.daily 单次只能提取 6000 条数据,因此需要控制好每次提取数据的股票数
# https://tushare.pro/document/2?doc_id=27
price_all = []
stk_num = int(6000 / (count_ + 20) * 0.99)

# 每次提取一批股票的历史行情
for i in range(0, len(stock_list), stk_num):
    idx_end = np.min([len(stock_list), i + stk_num])
    joined_str = ",".join(stock_list[i:idx_end])
    df = pro.daily(ts_code=joined_str, start_date=since, end_date=until)
    price_all.append(df)

price_all = pd.concat(price_all)
price_all = price_all.set_index(["ts_code", "trade_date"])

直接用未复权的数据搞计算肯定是不科学的。好在 pro.daily() 也可以返回昨收价(除权价)pre_close。有了 closepre_close,我们就可以自己对数据进行复权,前复权、后复权都可以。

这就是自己在本地处理数据的好处:自由度大,想要什么样的处理方式就用什么样的处理方式。

def adjust_price(df: pd.DataFrame, label: str = "front") -> pd.DataFrame:
    df_adj = df.copy()
    adj_factor = df["close"] / df["pre_close"]
    adj_factor = adj_factor.cumprod()

    if label == "front":
        df_adj["close"] = adj_factor / adj_factor.iloc[-1] * df["close"].iloc[-1]
    elif label == "back":
        df_adj["close"] = adj_factor / adj_factor.iloc[0] * df["close"].iloc[0]

    df_adj["open"] = df["open"] / df["close"] * df_adj["close"]
    df_adj["high"] = df["high"] / df["close"] * df_adj["close"]
    df_adj["low"] = df["low"] / df["close"] * df_adj["close"]
    return df_adj


# pro.daily 获取的行情是没有复权的,需要对数据进行复权
adjusted_list = []

for i, stk in enumerate(stock_list):
    df = price_all.loc[stk]
    df = df.sort_index()
    df = adjust_price(df, "front")
    df["ts_code"] = stk
    df["date"] = df.index
    df = df[["ts_code", "close", "date"]]
    adjusted_list.append(df)

adjusted_list_all = pd.concat(adjusted_list)

行业分类

我们首先可以用 Tushare 的 pro.index_classify() 接口获取申万一级行业分类列表,总共有 31 个分类。

# 获取申万一级行业列表
# https://tushare.pro/document/2?doc_id=181
df = pro.index_classify(level="L1", src="SW2021")
s_industry = df[["index_code", "industry_name"]]
s_industry.index = s_industry["index_code"]
s_industry = s_industry.drop(columns="index_code")
s_industry = s_industry.squeeze()

listcode_industry_all = s_industry.index.tolist()

然后,可以用 pro.index_member_all() 接口获取每个行业所包含的票。有了这些信息,我们就可以映射每只票所属的行业。

# 获取所有一级分类信息及其所含股票
# https://tushare.pro/document/2?doc_id=335
df_all = []

for code in code_industry_all:
    df = pro.index_member_all(l1_code=code)
    df_all.append(df)

df_all = pd.concat(df_all)

# 获取股票所在的行业
dict_stk_2_ind = {}

for stk in stock_list:
    df = df_all[df_all["ts_code"] == stk]
    if len(df) > 0:
        dict_stk_2_ind[stk] = df.iloc[0]["l1_code"]

s_stk_2_ind = pd.Series(dict_stk_2_ind)

宽度计算

到这里,最主要的问题、最难啃的步骤都已经啃完了。后面就是基于上面两类数据计算各个票的 乖乘率,以及每日 各个行业乖乘率大于 0 的票的比例

# 计算乖乘率
df_close = adjusted_list_all.pivot(
    index="ts_code",
    columns="date",
    values="close",
).dropna(axis=0)

df_ma20 = df_close.rolling(window=20, axis=1).mean().iloc[:, -count_:]
df_bias = df_close.iloc[:, -count_:] > df_ma20

# 每个交易日全市场的总体状况:close 在 MA20 之上的比例
s_mkt_ratio = ((100.0 * df_bias.sum()) / df_bias.count()).round()

df_bias["industry_code"] = s_stk_2_ind
df_ratio = (
    (df_bias.groupby("industry_code").sum() * 100.0)
    / df_bias.groupby("industry_code").count()
).round()

我们将本地 Python 代码运行的结果,与聚宽上大佬们写的聚宽代码运行结果进行对比,结果基本一致,证明以上本地化过程是正确的。

本地行业宽度展示

行业宽度对比

我们也可以比较一下 全市场宽度 的运行结果,也基本是一样的。

全市场宽度对比

小结

整个流程走下来,最大的感受还是那句话:

本地 Python 量化虽然麻烦一点,但一旦工具链搭起来了,你就拥有了真正的自由。

  • 不依赖第三方平台
  • 数据、逻辑、代码全在自己手里
  • 想怎么跑就怎么跑,想怎么改就怎么改

如果你也和我一样,对“可控性”和“自由度”有执念,那本地化这条路,真的很值得折腾。

希望这篇思路拆解,能对你有所启发。如果你也对 Python 本地量化感兴趣,欢迎加入知识星球,一起交流学习探索。

知识星球:不定期更新可本地运行的 Python 量化工具等代码,方便个人学习研究。代码持续更新,让 Bug 远离我们。同时也有交流群,大家一起探讨 Python 本地量化。

知识星球二维码

本文仅用于学习研究与技术交流,不构成任何投资建议、证券投资咨询服务或收益承诺。
Discussion

评论与交流

当前主要通过知识星球和社交媒体交流文章相关问题。

交流入口

如果你想讨论文章里的代码、数据接口或本地运行问题,可以通过知识星球或页脚社交媒体联系我。