构建指数权重ib weight

警告
本文最后更新于 2024-04-15,文中内容可能已过时。

中金所推出了多个股指期货合约,这些合约以相应的股票指数为基础标的。为了更好的预测股指期货波动,我们需要更准确的指数信息。而构建指数的基础数据包括:

  • 成份股
  • 成份权重
  • 基准日期的自由流通市值(中证指数)

无论对于历史数据的复原,还是实盘数据的更新,一份合理、准确的指数构建都至关重要。上述三个项目当中,尤其以指数的成份权重尤为关键。

中证指数官网有偿提供每日权重的更新数据,不过收费巨贵。为此,我们可以通过模拟中证指数的构建方法和计算规则,生成一份准确的指数数据。具体的规则,可以参考《中证指数有限公司股票指数计算与维护细则V13.1》

我们的工作主要有部分:

  1. 复原历史指数权重数据
  2. 实现每日权重数据更新
  3. 根据样本权重,利用逐笔成交数据(last_px)、快照数据(vwap)计算得到每一个点位上的指数价格

关于中证指数规则

编制规则

  • 中证指数公司负责编制指数,并在每个月月底公布下个月的权重数据(官网可下载)
  • 每年的6、12月第二个周五定期调整权重(此时不再对外公布权重信息,这也是我们自编制的指数在每年6、12月下旬出现较大偏移的原因)
  • 样本临时调整:
    • 退市:直接从样本剔除,然后从备选池纳入新成分
    • 收购合并:中证指数会发布公告,万得指数成分变动数据有记录
    • 停牌(目前无法处理)

更新维护

指数修正

  • 除息(现金分红)不予修正:
  • 除权(送股、转股、配股、拆股、缩股):需要计算除权价格
  • 其他公司事件(由于我们无法获取跟中证指数统计口径一致的数据,暂时无法处理):
    • 当样本股本发生由其他公司事件(如增发、债转股、期权行权等)引起的总股本变动累计达到或超过5%时,对其进行临时调整,在样本的股本变动生效日前修正指数。
    • 当样本股本发生由其他公司事件引起的总股本变动累计不及5%时,对其进行定期调整,在定期调整生效日前修正指数
  • 样本发生退市情况,需要删除原有样本,然后从备选池录入新样本(万得提供了历史指数成分的纳入、剔除数据)

除权除息价格计算

可以根据恒等式推算

\begin{align} 公司总资产 &: \\ &\equiv N * P_0 - N * D_{现金分红} + NR_{配股比例} R_{配股完成比例}P_{配股价格} \\ &\equiv N(1 + R_{送股比例} + R_{转股比例} + R_{配股比例} * R_{配股完成比例} ) * P_t \\ \longrightarrow P_0^{’} &= \frac{P_0 - D_{现金分红} + R_{配股比例} * R_{配股完成比例} * P_{配股价格} }{ 1 + R_{送股比例} + R_{转股比例} + R_{配股比例} * R_{配股完成比例} } \end{align}

其中

  • N 表示原始股本数量

  • $P_0$ 表示前一日价格

  • $P_t$ 表示除权除息当日价格

  • 中证指数不处理现金分红,所以 $D_{现金分红} = 0$。

数据说明

  • 中证指数使用自由流通市值,根据官方文档

    为反映市场中实际流通股份的变动情况,指数剔除了样本总股本中的限售股份,以及由于战略持股或其他原因导致的基本不流通股份,剩下的股本称为自由流通股本,也即自由流通量。上市公司公告明确的限售股份和属于下述四类股份,且股东持有股份量达到或超过 5%或具有一致行动人关系的股东合计持有股份量达到或超过 5%,被视为非自由流通股本。四类股份具体如下:

    (1)公司创建者、家族、高级管理者等长期持有的股份

    (2)国有股份

    (3)战略投资者持有的股份

    (4)员工持股计划

    计算公式为: $$ 自由流通量 = 样本总股本- 非自由流通股本 $$ 中证指数有限公司根据多种客观的信息来源估算自由流通量,包括但不限于:

    • 招股说明书、上市公告书:实际控制人,发起人,战略投资者,高管持股,员工持股等;
    • 定期报告:实际控制人,发起人,战略投资者,高管持股,员工持股等;
    • 临时公告:股东持股变化公告、收购报告书、权益变动报告书等。

从以上表述看,自由流通市值这个概念其实没有一个非常清晰的界定,毕竟以上所涉及的各类公告信息,目前市面上几乎没有数据供应商具备收集与整理的能力。即使强如万得,也是没有这方面精确的、与中证指数官网完全一致的数据。1

代码实现

代码实现难度不高,不过需要注意历史的部分数据有异常情况(如临时退市、纳入与剔除数量不一致等)。

流程

  1. 从万得获取一份指数的历史成份记录(wind.indexhistory),用于更新成份列表。由于从每年6、12月第二个周五开始,中证指数没有公布成份权重数据,对于在此时剔除、纳入的样本股票,万得提供的成份权重数据是 NAN这个一定不能用(罪恶!)。

  2. 在每个月最后一个交易日从万得获取指数成份权重数据,这个将被用于当作接下来一整个月的数据基础。同时,我们最好在每个月第一个交易日,再更新一份万得的权重数据,然后跟月末的数据进行对比,看看是否有发生变化,以便做数据核对。

  3. 接下来的每一天直至月末,我们根据基准日期(即上个月月末)的权重数据,计算成份股的涨跌幅度2。然后判断当天盘后是否有最新的权重更新(比如wind数据源月末最后一天, 或者6月/12月指数调整披露的日期)

    • 如果有,则直接同步更新至最新的指数权重。

    • 如果没有,则用当天盘后 stock.dailystock_symbolsclose/preclose 当做权重,来调整前一日的权重。

    以上处理,实现了每日根据个股涨跌幅计算市值的波动,并以此更新权重数据。

  4. 在每年的6、12月第二个周五,由于没有成份权重数据,我们可以利用万得api提供的流通市值,用来近似代表中证指数使用的自由流通市值,并逐日进行更新,这样可以尽量减少在6、12月下旬的差异。

代码解析

计算中证指数每年6、12月第二个周五的交易日

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@staticmethod
def gen_csi_index_change_days():
    """计算中证指数在每年6、12月第二个周五调整对应的交易日期
    """
    ##
    start_date = datetime(2000, 1, 1)
    end_date = datetime(2099, 1, 1)
    delta = end_date - start_date
    days = []
    for i in range(delta.days + 1):
        day = start_date + timedelta(days=i)
        days.append(day.strftime("%Y-%m-%d"))
    df = pd.DataFrame({'natural_day': days})

    df['natural_mon'] = df['natural_day'].apply(lambda x: x[:7])
    df['day_of_week'] = df['natural_day'].apply(
        lambda x: datetime.strptime(x, '%Y-%m-%d').strftime("%u"))
    df['nth_of_week'] = df.groupby(['natural_mon', "day_of_week"])["day_of_week"].transform(lambda x: range(1,len(x)+1))
    for k in ['day_of_week', 'nth_of_week']:
        df[k] = df[k].astype(str)

    ## 样本调整实施时间原则上分别为每年 6 月和 12 月的第二个星期五的下一交易日。
    df = df[(df['natural_mon'].str.contains('-06$|-12$')) &
            (df['day_of_week'] == '5') &
            (df['nth_of_week'] == '2')]
    return df
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
      natural_day natural_mon day_of_week nth_of_week
160    2000-06-09     2000-06           5           2
342    2000-12-08     2000-12           5           2
524    2001-06-08     2001-06           5           2
713    2001-12-14     2001-12           5           2
895    2002-06-14     2002-06           5           2
...           ...         ...         ...         ...
35412  2096-12-14     2096-12           5           2
35594  2097-06-14     2097-06           5           2
35776  2097-12-13     2097-12           5           2
35958  2098-06-13     2098-06           5           2
36140  2098-12-12     2098-12           5           2

[198 rows x 4 columns]

获取 wind.indexhistory

1
2
3
4
res = windx('wset', [
    "indexhistory",
    f"startdate={start_day};enddate={cal.today()};windcode={self.index_symbol}"],
    src = self.wind_src)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
                          tradedate  tradecode tradename         mv   weight tradestatus  TradingDay     Symbol
0     Mon, 11 Dec 2023 00:00:00 GMT  300308.SZ      中际旭创 964.595725 0.972000          剔除  2023-12-11  300308.SZ
1     Mon, 11 Dec 2023 00:00:00 GMT  688256.SH     寒武纪-U 708.168907 0.497000          剔除  2023-12-11  688256.SH
2     Mon, 11 Dec 2023 00:00:00 GMT  600372.SH      中航机载 629.056562 0.398000          剔除  2023-12-11  600372.SH
3     Mon, 11 Dec 2023 00:00:00 GMT  601136.SH      首创证券 599.966769 0.103000          剔除  2023-12-11  601136.SH
4     Mon, 11 Dec 2023 00:00:00 GMT  300832.SZ       新产业 599.896292 0.352000          剔除  2023-12-11  300832.SZ
...                             ...        ...       ...        ...      ...         ...         ...        ...
2925  Mon, 04 Jan 2010 00:00:00 GMT  601008.SH       连云港  41.879040      NaN          纳入  2010-01-04  601008.SH
2926  Mon, 04 Jan 2010 00:00:00 GMT  002212.SZ       天融信  40.611450      NaN          纳入  2010-01-04  002212.SZ
2927  Mon, 04 Jan 2010 00:00:00 GMT  002107.SZ      沃华医药  40.322682      NaN          纳入  2010-01-04  002107.SZ
2928  Mon, 04 Jan 2010 00:00:00 GMT  002109.SZ      兴化股份  39.459840      NaN          纳入  2010-01-04  002109.SZ
2929  Mon, 04 Jan 2010 00:00:00 GMT  600644.SH      乐山电力  34.280414      NaN          纳入  2010-01-04  600644.SH

计算每个月的基准日期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
self.index_symbol = '000905.SH'
self.output_path  = '/home/william/Desktop/ib'
self.trading_day  = '2023-06-09'
self.wind_src     = 'wuya'
self.base_day     = '2023-06-08'
self.this_mon     = '2023-06'
self.last_mon     = '2023-05'
self.this_mon_first_day = '2023-06-01'
self.this_mon_last_day  = '2023-06-30'
self.last_mon_last_day  = '2023-05-31'

获取万得历史的权重数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def fetch_index_weight_from_wind(self):
    """用这个月第一天的数据来填充剩下的日期
    """
    ## 1. 如果是月末,使用月末的日期,获取权重
    ## 2. 如果是非月末,则使用第一天的,这个应该跟月末数据是一致的
    wind_trading_day = (self.this_mon_last_day if self.trading_day == self.this_mon_last_day else self.this_mon_first_day)
    csvfile = f"{self.output_path}/wind.fetch_index_weight.{wind_trading_day}.{self.index_symbol}.csv"
    if not os.path.isfile(csvfile):
        try:
            # res = wind.fetch_index_weight(self.index_symbol, self.trading_day)
            ## 万得的 000300.SH 是每个月变动两次,需要修改 base day
            ## 我们终端中证指数权重不是每日更新的,是月度更新,沪深300就月初和月末这2次变动。
            ## 如果需要每日的数据,要购买中证指数的服务,成为会员,然后在万得做数据订制才行。
            ## 是的,目前只有沪深300的权重是月初和月末变动更新的。
            res = self.wind.fetch_index_weight(self.index_symbol, wind_trading_day)
            res.to_csv(csvfile, index=False)
            log.inf(f"wind.fetch_index_weight: {self.index_symbol}, {wind_trading_day} ==> {csvfile}")
        except Exception as e:
            msg = traceback.format_exc()
            log.err(msg)
            raise Exception(msg)

    res = pd.read_csv(csvfile)
    res['BenchmarkDay'] = res['BaseDay'].apply(lambda x: datetime.strptime(x[:16], "%a, %d %b %Y").strftime('%Y-%m-%d'))
    res['TradingDay'] = self.trading_day
    res['IndexName'] = ''
    res['IndexAlias'] = ''
    res['IndexSymbol'] = self.index_symbol
    res['Symbol'] = res['w_code']
    ## 标准化
    res = res[~pd.isna(res['index_weight'])]
    res['Weight'] = res['index_weight'] / res['index_weight'].sum()
    res['WeightAdjust'] = res['Weight']

    if (len(res['BenchmarkDay'].unique()) != 1 or
        (res['BenchmarkDay'].unique()[0] not in [self.base_day, wind_trading_day] and
         self.trading_day > '2020-01-01')
        ):
        raise Exception(f"""
            WindApi Error fetch_index_weight
            {self.base_day = }
            {res = }
            """)
    df = res[self.COLUMNS]
    return df
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
     TradingDay BenchmarkDay IndexName IndexAlias IndexSymbol     Symbol   Weight  WeightAdjust
0    2023-06-09   2023-05-31                        000905.SH  000009.SZ 0.003790      0.003790
1    2023-06-09   2023-05-31                        000905.SH  000012.SZ 0.001270      0.001270
2    2023-06-09   2023-05-31                        000905.SH  000021.SZ 0.003250      0.003250
3    2023-06-09   2023-05-31                        000905.SH  000027.SZ 0.002000      0.002000
4    2023-06-09   2023-05-31                        000905.SH  000031.SZ 0.000710      0.000710
..          ...          ...       ...        ...         ...        ...      ...           ...
495  2023-06-09   2023-05-31                        000905.SH  688772.SH 0.000320      0.000320
496  2023-06-09   2023-05-31                        000905.SH  688777.SH 0.005100      0.005100
497  2023-06-09   2023-05-31                        000905.SH  688779.SH 0.001340      0.001340
498  2023-06-09   2023-05-31                        000905.SH  688819.SH 0.000730      0.000730
499  2023-06-09   2023-05-31                        000905.SH  689009.SH 0.002120      0.002120

[500 rows x 8 columns]

使用流通市值计算权重

在每年的6、12月第二个周五(盘后更新),需要使用万得提供的流通市值计算成份股的权重。

具体步骤为:

  1. 读取上一个月的权重成份
  2. 获取 indexhistory,区分纳入、剔除的成份(最好对比一下数量是否一样)
  3. 生成新的成份样本
  4. 根据前一天的收盘价计算当天的权重
  5. 此后每日更新至月底
合成新的成份
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
## 1. 先读取上一个的成份
last_mon_day = self.ALL_DAYS[self.ALL_DAYS.mon == self.last_mon].days.values[-1]
csvfile = f"{self.output_path}/{self.index_symbol}.base_day.{last_mon_day}.csv"
if not os.path.isfile(csvfile):
    if not (re.search("SH", self.index_symbol) or self.trading_day > '2019-06-01'):
        df_last_mon = self.fetch_index_weight_from_wind()
    else:
        raise Exception(f"failed to find last mon base_day: {csvfile = }")
else:
    log.inf(f"already done, using {csvfile = }")
    df_last_mon = pd.read_csv(csvfile)

## 2. 然后根据出入 history 情况进行剔除、添加
## 注意 TradingDay 是生效日期,需要在 base_day 加上一天
res = self.index_history[
    (self.index_history.TradingDay == cal.cal_trading_day(self.base_day, +1))
    ]
if len(res) == 0:
    base_day = self.SPECIAL_DAYS[self.SPECIAL_DAYS.mon == self.this_mon]
    for day in base_day.days:
        res = self.index_history[
        (self.index_history.TradingDay == cal.cal_trading_day(day, +1))
        ]
        if len(res) != 0:
            break
if set(res.tradestatus.unique()) != set(['剔除', '纳入']) and self.trading_day > '2020-01-01':
    raise Exception(f"""
        Wind has Changed it's tradestatus,
        should be ...... {set(['剔除', '纳入']) = }
        but got {set(res.tradestatus.unique()) = }
        """)

removed = res[res.tradestatus.str.contains("剔除")]
added = res[res.tradestatus.str.contains("纳入")]
## 2017-06-01: 000804.CSI, removed:30, added:29
if abs(len(removed) - len(added)) > 1 and len(df_last_mon) < 1000:
    raise Exception(f"""
        Wind has different symbol length
        {len(removed) = }
        {len(added) = }
        """)
log.wrn(f"""
    {self.index_symbol = }
    {self.trading_day = }
    {self.base_day = }
    {removed = }
    {added = }
    """)
## 剔除
x = df_last_mon[~(df_last_mon.Symbol.isin(removed.Symbol))]
if len(x) != (len(df_last_mon) - len(removed)) and len(df_last_mon) < 1000:
    raise Exception(f"""
        Wind has different symbol length
        {len(df_last_mon) = }
        {len(removed)     = }
        {len(x)           = }
        {((len(df_last_mon) - len(removed))) = }
        """)
x = x[['TradingDay', 'Symbol']]
y = added[['TradingDay', 'Symbol']]
df = pd.concat([x, y], ignore_index=True)
if abs(len(df) - len(df_last_mon)) > 1 and len(df_last_mon) < 1000:
    raise Exception(f"""
        {self.index_symbol= } has different symbol length
        {self.trading_day = }
        {self.base_day    = }
        {len(df_last_mon) = }
        {len(df)          = }
        """)
获取流通市值

这里需要注意基准日期是前一天,需要每日滚动更新。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
## 3. 根据 FreeShared 计算市值权重
df_freeshares = pd.DataFrame()
while True:
    csvfile = f"{self.output_path}/wind._fetch_stock_daily_by_symbol_freeshares.{self.base_day}.csv"
    if len(df_freeshares) == 0:
        if not os.path.isfile(csvfile):
            symbol_list = list(set(df.Symbol.to_list()))
            res = wind._fetch_stock_daily_by_symbol_freeshares(
                trading_day = self.base_day,
                symbol = symbol_list
                )
            res.to_csv(csvfile, index=False)
        log.inf(f"already done, using {csvfile = }")
        df_freeshares = pd.read_csv(csvfile)
        # exchange_listing = get_exchange_listing()
        # exchange_listing = pd.merge(exchange_listing, df_freeshares, on = 'Symbol', how = 'right', suffixes = ['_exchange', ''])
        missing_symbols_list = df[~(df.Symbol.isin(df_freeshares.Symbol.values))].Symbol.to_list() + \
                               df_freeshares[(pd.isna(df_freeshares.FreeShare))].Symbol.to_list()
        if len(set(missing_symbols_list)) != 0:
            log.wrn(f"less symbol, do qry WindAPI again")
            continue
        else:
            break
    else:
        symbol_list = df[~df.Symbol.isin(df_freeshares.Symbol.values)].Symbol.to_list() + \
                      df_freeshares[pd.isna(df_freeshares.FreeShare)].Symbol.to_list()
        res = wind._fetch_stock_daily_by_symbol_freeshares(
            trading_day = self.base_day,
            symbol = list(set(symbol_list)))
        res = pd.concat([df_freeshares, res])
        res.drop_duplicates(inplace=True)
        res.to_csv(csvfile, index=False)
        df_freeshares = pd.read_csv(csvfile)
        break
df_freeshares = df_freeshares[df_freeshares.Symbol.isin(df.Symbol.values)]
df_freeshares.drop_duplicates(['Symbol'], inplace=True)
df_totalshares = ch_idc.read(f"""
    select TradingDay, Symbol, TotalShare, FloatAShare
    from stock.daily
    where TradingDay = '{self.base_day}'
    and TotalShare > 0
    """)
df_freeshares = pd.merge(
    df_freeshares, df_totalshares,
    on = 'Symbol', how = 'left',
    suffixes = ['', '_total'])
## 中证指数:分级靠档: http://www.sse.com.cn/market/sseindex/calculation/c/5726306.pdf
## 除非特别说明,中证指数有限公司在计算指数时,采用分级靠档的方法,即根据自由流通量所占样本总股本的比例(即自由流通比例)赋予样本总股本一定的加权比例,
## 以确计算指数的股本保持相对稳定。
## 自由流通比例=自由流通量/A股总股本
## 调整股本数=A股总股本×加权比例
def handle_shares(symbol, free_shares, total_shares):
    if pd.isna(free_shares) or free_shares <= 1:
        log.inf(f"""
            {symbol = }
            {free_shares = }
            {total_shares = }
            """)
        return 0
    if pd.isna(total_shares):
        total_shares = free_shares
    if pd.isna(free_shares) and pd.isna(total_shares):
        # 2015-01-26: 000166.SZ 申万宏源新上市,需要用下一天的数据
        tmp = ch_idc.read(f"""
            select * from stock.daily
            where TradingDay > '{self.base_day}'
            and Symbol = '{symbol}'
            and TotalShare > 0
            order by TradingDay ASC
            limit 1
            """)
        if len(tmp) == 0:
            #      TradingDay Exchange     Symbol  FreeShare TradingDay_total  TotalShare  FloatAShare
            # 324  2019-03-08     szse  000043.SZ        NaN              NaN         NaN          NaN
            return 0
        else:
            return tmp.FloatAShare.values[0]
    ## '2015-06-15': 万得会修改历史分红数据,导致 shares 不太对
    r = min(free_shares / total_shares *100,100)
    assert r < 100.01, f"error shares amount: {symbol}"
    if r < 15:
        r = int(r) + 1
    elif r > 80:
        r = 100
    else:
        r = (int(r/10) +1 ) *10
    return r/100.0 * total_shares
df_freeshares['FreeShareAdjust'] = df_freeshares[['Symbol', 'FreeShare', 'TotalShare']].apply(
    lambda x: handle_shares(x[0], x[1], x[2]), axis = 1)
cond = (pd.isna(df_freeshares['TotalShare']) & pd.isna(df_freeshares['FreeShare']))
df_freeshares.loc[cond, 'TotalShare'] = df_freeshares.loc[cond, 'FreeShareAdjust']
df_freeshares.loc[cond, 'FreeShare'] = df_freeshares.loc[cond, 'FreeShareAdjust']

if len(df_freeshares[df_freeshares['FreeShareAdjust'] > df_freeshares['TotalShare']]) > 0:
    raise Exception(f"""
        Error shares for
        {df_freeshares = }
        """)
计算权重
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
## cal weight
daily_base_day = ch.read(f"""
    select TradingDay, Symbol, Close,
           TotalShare, FloatAShare
    from stock.daily
    where TradingDay = '{self.base_day}'
    """)
df = pd.merge(daily_base_day, df_freeshares, on = 'Symbol', how = 'right',
              suffixes = ['', '_wind_freeshares'])
## 防止 Wind 复权导致的股本异常
df['FreeShare'] = df[['TotalShare', 'FreeShare']].apply(lambda x: min(x[0], x[1]), axis = 1)
df['FreeValue'] = df['FreeShare'] * df['Close']
df['FreeValueAdjust'] = df['FreeShareAdjust'] * df['Close']
df['Weight'] = df['FreeValue'] / df['FreeValue'].sum()
df['WeightAdjust'] = df['FreeValueAdjust'] / df['FreeValueAdjust'].sum()
if len(added) == len(removed) == 1:
    for k in ['Weight', 'WeightAdjust']:
        df.loc[pd.isna(df[k]), k] = removed.weight.values[0]/100.0
for k in ['Weight', 'WeightAdjust']:
    df.loc[df[k] <= self.EPSILON, k] = self.EPSILON
df['BenchmarkDay'] = self.base_day
df['TradingDay'] = self.trading_day
df['IndexName'] = ''
df['IndexAlias'] = ''
df['IndexSymbol'] = self.index_symbol

每日根据股票涨跌幅更新权重

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def adjust_weight_on_daily(self, df):
    """
    http://192.168.1.191:20080/wiki/#/team/M9rVLNSS/share/LdejewHw/page/9DNzQcim

    判断当天盘后是否有最新的权重更新(比如wind数据源月末最后一天, 或者6月/12月指数调整披露的日期)
    * 如果有,则直接同步更新至最新的指数权重。
    * 如果没有,则用当天盘后stock.daily中stock_symbols的close/preclose当做权重,来调整前一日的权重。
    """
    while True:
        special = self.SPECIAL_DAYS.groupby(['mon']).agg("first")
        if self.trading_day in special.days.values:
            break
        if df.BenchmarkDay.values[0] == self.trading_day:
            break
        csvfile = f"{self.output_path}/{cal.cal_trading_day(self.trading_day, -1)}.{self.index_symbol}.ib_weight.csv"
        if not os.path.isfile(csvfile):
             break
        log.inf(f"now try to update ib_weight by pre_close: {self.trading_day}")
        df_pre = pd.read_csv(csvfile)

        daily = get_stock_daily_from_cache(self.trading_day)
        if daily is None or len(daily) == 0:
            daily = ch_idc.read(f"""
                select * from stock.daily
                where TradingDay = '{self.trading_day}'
                """)
        daily = pd.merge(daily, df_pre, on = 'Symbol', how = 'right', suffixes = ['', '_ib'])
        if len(daily) != len(df) and self.trading_day > '2020-01-01':
            raise Exception(f"""
                {len(daily) = }
                {len(df)    = }
                """)

        daily['x'] = daily['Close'] / daily['PreClose']
        for k in ['Weight', 'WeightAdjust']:
            daily[f'{k}Origin'] = daily[f'{k}']
            daily[f'{k}Update'] = daily[f'{k}Origin'] * (daily['x'] / daily['x'].sum())
            daily[f'{k}Update'] = daily[f'{k}Update'] / daily[f'{k}Update'].sum()

        daily = daily[self.COLUMNS + ['WeightOrigin', 'WeightUpdate', 'WeightAdjustOrigin', 'WeightAdjustUpdate']]
        daily['BenchmarkDay'] = self.trading_day
        ## 2015-05-20: 000016.SH
        if len(daily[pd.isna(daily['WeightUpdate'])]) != 0:
            break
        ## ---------------------------------------
        ## use update version
        for k in ['Weight', 'WeightAdjust']:
            daily[f'{k}'] = daily[f'{k}Update'] df = daily[self.COLUMNS]
        break
        ## ---------------------------------------

    return df

run

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def run(self):
    df = pd.DataFrame([], columns = self.COLUMNS)
    if self.index_symbol.endswith('SH'):
        info = csi_index_symbol_basic_info(self.index_symbol)
        if self.trading_day < info['data'].get('publishDate'):
            log.wrn(f"not yet published")
            return df

    ## ====================
    df = self.gen_index_weight()
    self.df = self.adjust_weight_on_daily(df)
    if self.check():
        self.save()
    return self.df
    ## ====================

结果验证

这个版本在每年6、12月存在较大偏离,其他时间则较好贴近

一次更新
一次更新

根据每日涨跌幅滚动更新后,指数更加平滑了

ib指数更加平滑
ib指数更加平滑

价格指数 vs 全收益指数

全收益指数

另外,我们在对比权重数据的时候,需要区分价格指数全收益指数的区别。可以参考这篇文章: 全收益指数概念详解

价格指数 vs 全收益指数

价格指数是单纯反应一篮子股票的价格变化情况。每一个价格指数背后都有一个对应的全收益指数(Total Return Index),这类指数除了反映股价波动外,还假定篮子内所有股票的现金分红用于再投资产生收益。  

沪深300指数它对应“沪深300全收益指数”。当有样本股除息(分红派息),沪深300指数不予修正,任其自然回落;沪深300全收益指数考虑到分红的部分,在样本股除息日前按照除息参考价予以修正。  

比如这些指数以及对应的全收益指数:wind.规模指数_全收益.csv

验证 ib weight 最好使用全收益指数

  1. 我们在每日滚动更新权重时,使用的 $\frac{Close}{PreClose}$,这里面的 PreClose 即除权除息价格。

    \begin{align} P_0^{’} &= \frac{P_0 - D_{现金分红} + R_{配股比例} * R_{配股完成比例} *P_{配股价格} }{1 + R_{送股比例} + R_{转股比例} + R_{配股比例} * R_{配股完成比例} } \end{align}

  2. 由于中证指数对于个股现金派息不予以修正,会导致我们滚动计算的除权价格偏小,相反的当日的个股收益变大

  3. 于是,我们每日滚动计算的指数数据,实际上比中证指数要偏大一些

  4. 对此,我们需要跟踪全收益指数,这能反映现金分红进行再投资带来的增益。


  1. 万得提供了数据 float_a_shares, 这个可以近似当作自由流通市值,但不敢保证数据精确。 ↩︎

  2. 需要处理除权除息,可以根据以上除权公式;或者交易所提供的 PreClose 数据,这个数据已经过除权处理 ↩︎

william 支付宝支付宝
william 微信微信
0%