警告
本文最后更新于 2024-04-15,文中内容可能已过时。
中金所推出了多个股指期货合约,这些合约以相应的股票指数为基础标的。为了更好的预测股指期货波动,我们需要更准确的指数信息。而构建指数的基础数据包括:
成份股
成份权重
基准日期的自由流通市值(中证指数)
无论对于历史数据的复原,还是实盘数据的更新,一份合理、准确的指数构建都至关重要。上述三个项目当中,尤其以指数的成份权重尤为关键。
中证指数官网有偿 提供每日权重的更新数据,不过收费巨贵。为此,我们可以通过模拟中证指数的构建方法和计算规则,生成一份准确的指数数据。具体的规则,可以参考《中证指数有限公司股票指数计算与维护细则V13.1》
我们的工作主要有部分:
复原历史指数权重数据
实现每日权重数据更新
根据样本权重,利用逐笔成交数据(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}
其中
数据说明
中证指数使用自由流通市值 ,根据官方文档
为反映市场中实际流通股份的变动情况,指数剔除了样本总股本中的限售股份,以及由于战略持股或其他原因导致的基本不流通股份,剩下的股本称为自由流通股本,也即自由流通量。上市公司公告明确的限售股份和属于下述四类股份,且股东持有股份量达到或超过 5%或具有一致行动人关系的股东合计持有股份量达到或超过 5%,被视为非自由流通股本。四类股份具体如下:
(1)公司创建者、家族、高级管理者等长期持有的股份
(2)国有股份
(3)战略投资者持有的股份
(4)员工持股计划
计算公式为:
$$
自由流通量 = 样本总股本- 非自由流通股本
$$
中证指数有限公司根据多种客观的信息来源估算自由流通量,包括但不限于:
招股说明书、上市公告书:实际控制人,发起人,战略投资者,高管持股,员工持股等;
定期报告:实际控制人,发起人,战略投资者,高管持股,员工持股等;
临时公告:股东持股变化公告、收购报告书、权益变动报告书等。
从以上表述看,自由流通市值 这个概念其实没有一个非常清晰的界定,毕竟以上所涉及的各类公告信息,目前市面上几乎没有数据供应商具备收集与整理的能力。即使强如万得,也是没有这方面精确的、与中证指数官网完全一致的数据。
代码实现 代码实现难度不高,不过需要注意历史的部分数据有异常情况(如临时退市、纳入与剔除数量不一致等)。
流程
从万得获取一份指数的历史成份记录(wind.indexhistory
),用于更新成份列表。由于从每年6、12月第二个周五开始,中证指数没有公布成份权重数据,对于在此时剔除、纳入的样本股票,万得提供的成份权重数据是 NAN
,这个一定不能用 (罪恶!)。
在每个月最后一个交易日 从万得获取指数成份权重数据,这个将被用于当作接下来一整个月的数据基础。同时,我们最好在每个月第一个交易日,再更新一份万得的权重数据,然后跟月末的数据进行对比,看看是否有发生变化,以便做数据核对。
接下来的每一天直至月末 ,我们根据基准日期(即上个月月末)的权重数据,计算成份股的涨跌幅度 。然后判断当天盘后是否有最新的权重更新(比如wind数据源月末最后一天, 或者6月/12月指数调整披露的日期)
以上处理,实现了每日根据个股涨跌幅计算市值的波动,并以此更新权重数据。
在每年的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月第二个周五(盘后更新),需要使用万得提供的流通市值
计算成份股的权重。
具体步骤为:
读取上一个月的权重成份
获取 indexhistory
,区分纳入、剔除的成份(最好对比一下数量是否一样)
生成新的成份样本
根据前一天的收盘价计算当天的权重
此后每日更新至月底
合成新的成份
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指数更加平滑
价格指数 vs 全收益指数
全收益指数 另外,我们在对比权重数据的时候,需要区分价格指数 与全收益指数的区别 。可以参考这篇文章: 全收益指数概念详解
价格指数 vs 全收益指数
价格指数是单纯反应一篮子股票的价格变化情况。每一个价格指数背后都有一个对应的全收益指数(Total Return Index),这类指数除了反映股价波动外,还假定篮子内所有股票的现金分红用于再投资产生收益。
沪深300指数它对应“沪深300全收益指数”。当有样本股除息(分红派息),沪深300指数不予修正,任其自然回落;沪深300全收益指数考虑到分红的部分,在样本股除息日前按照除息参考价予以修正。
比如这些指数以及对应的全收益指数:wind.规模指数_全收益.csv
验证 ib weight 最好使用全收益指数
我们在每日滚动更新权重时,使用的 $\frac{Close}{PreClose}$,这里面的 PreClose
即除权除息价格。
\begin{align}
P_0^{’} &= \frac{P_0 - D_{现金分红} + R_{配股比例} * R_{配股完成比例} *P_{配股价格} }{1 + R_{送股比例} + R_{转股比例} + R_{配股比例} * R_{配股完成比例} }
\end{align}
由于中证指数对于个股现金派息不予以修正,会导致我们滚动计算的除权价格偏小,相反的当日的个股收益变大
于是,我们每日滚动计算的指数数据,实际上比中证指数要偏大一些
对此,我们需要跟踪全收益指数,这能反映现金分红进行再投资带来的增益。