量化定投,工薪族的逆袭之路¶
一个人一生能积累多少钱,不是取决于他能够赚多少钱,而是取决于他如何投资理财,人找钱不如钱找钱,要知道让钱为你工作,而不是你为钱工作。——(美)沃伦●巴菲特
# 导入常用的库
import numpy as np
import pandas as pd
import datetime as dt
import time
import matplotlib.pyplot as plt
import seaborn as sns
from jqdata import *
import tushare as ts
import seaborn as sns
plt.style.use('fivethirtyeight')
一、 指数格局,跌宕起伏¶
这里的指数,我们重点说一下上证指数、深圳指数。指数其实是一篮子股票,它反应的这些股票总体的表现。而上证与深圳指数更反应出当下国内的经济形式(当然不是百分百的呈现)。
相信大家都了解过经济周期,理论上,社会环境的经济会以衰退-萧条-复苏-繁荣四种形式往复呈现。不过,由于不同国家的国情不尽相同,这种周而复始的周期曲线表现得并不完美。
下图为经济周期曲线图:
那么,股市是否也会呈现一定的周期性呢?如果具有周期性,如何估算牛熊之间的时间距离呢?带着这样的疑问,接着往下探究。
在探究此问题之前,可以去查找一下相关的历史资料,看看是否有人已经给出答案,或者可以找一些重要的线索。经过百度,收集到:上证指数1990年12月19号成立,之后的经历了4次牛市,分别1993年2月、2001年6月、2007年10月、2015年6月。让我们来实际看一下上证指数全部的趋势情况。
plt.style.use('fivethirtyeight')
# 由于数据量比较多,这里打算从 tushare 获取上证指数所有的价格数据
# tushare 接口,参数为注册时生成的 token
pro = ts.pro_api('xxxxxxxxxxxxxxxx')
# tushare 要求一次最多获取 3000 条数据,所以分两次获取
# 然后将数据合并,按时间排序
df1 = pro.index_daily(ts_code='000001.SH',
start_date='19901219',
end_date='20101231')
df2 = pro.index_daily(ts_code='000001.SH',
start_date='20110101',
end_date='20190630')
df = pd.concat([df2, df1]) # 合并
df = df.sort_values(by=['trade_date']) # 排序
df['trade_date'] = pd.to_datetime(df['trade_date']) # 转换为时间类型
df.set_index(['trade_date'], inplace=True) # 设置索引列
df.index.name = None # 去掉索引列名
# 将上证指数价格曲线画出来
# 并在对应的牛市年份,画一条竖线来标记
df.close.plot(figsize=(14, 7), title='牛顶间隔展示')
for year in ['1993-02-01', '2001-06-01',
'2007-10-01', '2015-06-01']:
plt.axvline(year,color='r', alpha=0.7)
plt.show()
看完这张图,大概大家都会感慨:曾经的股市是多么的疯狂,它也像人生,起起伏伏。粗一看,感觉指数的起伏是有一定的周期性规律可寻的,但仔细看却发现,各牛市间的时间间隔并不匀称。那问题来了,各牛市顶点的时间间隔大概在什么样的取值范围呢?未来大盘的趋势是否会符合某种规律呢?接下来我们计算一下各牛顶时间节点的平均间隔时间与偏差。
# 转换成时间格式
best_years = [dt.datetime.strptime(year, '%Y-%m-%d').date()
for year in ['1993-02-01', '2001-06-01',
'2007-10-01', '2015-06-01']]
# 计算牛顶时间间隔
gap_year = [days.days for days in np.diff(best_years)]
print('牛顶平均间隔时间:', ['{} days'.format(days) for days in gap_year])
# 计算平均间隔年数,为避免盲目猜测,再计算出均值的偏差值
mean_gap = np.mean(gap_year)
print('平均时间间隔 {} 天,即 {} 年'.format(round(mean_gap, 2), round(mean_gap / 365, 2)))
# 计算平均时间间隔偏差值
std_gap = np.std(gap_year)
print('平均时间间隔偏差 {} 天,即 {} 年'.format(round(std_gap, 2), round(std_gap / 365, 2)))
# 计算下一个牛市出现的合理时间区间
early_year = (best_years[-1] + dt.timedelta(round(mean_gap - std_gap, 0)))
latest_year = (best_years[-1] + dt.timedelta(round(mean_gap + std_gap, 0)))
# 构造 title
early_y = early_year.year
early_m = early_year.month
latest_y = latest_year.year
latest_m = latest_year.month
print('下个牛顶时间范围 {}年{}月 ~ {}年{}月'.format(early_y, early_m, latest_y, latest_m))
title = '评估下个牛顶时间范围 {}年{}月 ~ {}年{}月'.format(early_y, early_m, latest_y, latest_m)
df.close.plot(figsize=(14, 7), title=title)
for year in ['1993-02-01', '2001-06-01',
'2007-10-01', '2015-06-01']:
plt.axvline(year,color='r', alpha=0.6)
# 生成 2019-02-11 ~ 2023-12-10 的时间区间
# 如果要填充一个区间,y 就给价格的最大值便好
date_span = pd.date_range(early_year, latest_year)
value_span = [df.close.max() for x in date_span]
plt.fill_between(date_span, value_span, color='orange', alpha=0.8)
plt.show()
这里需要提醒一下大家:历史数据只可用来评估一些现象,但绝不能 100% 预言未来!所以大家还要带着辩证的心态看待这个结果。
通过上面的统计与可视化,可得出以下结论:
- 一轮牛熊的平均时间间隔为7.5年左右;
- 4轮牛熊的时间偏差在0.8年左右;
- 依此估算的下个牛顶的时间范围在2022年1月 ~ 2023年9月之间。
假设这个结果是大概率可信的,那在到达牛顶之前,一定要经过一个牛市的启动阶段——所以,低估值与熊之尾巴才是最宝贵的!!!
二、定投畅想,看好国运¶
指数的价格一直在波动起伏,但从宏观看来,其底部是一直在抬高的。只要国家经济一直是向好的,那指数的从超长期来看,是总体向上发展的。也就是说,买指数产品,就是看好国运!
例如下图,通过线性回归,我们可以看出,上证指数的价格整体趋势是向上的。假如在上证指数成立之初我们就买入,并一直持有到现在,到目前为止的收益将是多少呢?我们来做个计算。
# 通过线性回归,刻画整体平均趋势
plt.figure(figsize=(14, 7))
sns.regplot(x=np.arange(0, df.shape[0]), y=df.close.values)
plt.show()
# 为了计算方便,默认都倩收盘价格为准
# 获取上市第一天的收盘价和当前收盘价
start_price = df.close[0]
end_price = df.close[-1]
print('最初收盘价为 {},当前收盘价为 {}'.format(start_price, end_price))
# 计算收益率
total_return = (end_price / start_price) - 1
print('持有到目前为止的收益率为 {}%'.format(round(total_return * 100, 2)))
# 计算年平均收益,一年以250个交易日为准
# 平均年化收益率=(投资内收益/本金)×(250/投资天数)× 100%
mean_return = total_return * (250 / df.shape[0])
print('平均每年收益率为 {}%'.format(round(mean_return * 100, 2)))
# 计算年化收益率复利
# 总收益 = 本金 * (年化利率 + 1)的 n 次方,n为交易年数
# 年化利率 = (总收益 / 本金)的 n次开根 - 1
annualized_return = pow(((end_price - start_price) / start_price),
1 / (df.shape[0] / 250)) - 1
print('年化收益率为 {}%'.format(round(annualized_return * 100, 2)))
看到这里,可能小伙伴们都张大了嘴大喊:“这不可能!我不相信!”是的,12.8% 的年化复利,的确很夸张。但这并不是表明指数收益相当可观,这其中的原因是:
- 指数刚上市的时候净值很低;
- 中国的经济已经发生了天翻地覆的变化;
- 持有的时间相对长;
- 没有考虑通货膨胀与金钱的时间价值。
现在我们已经知道,指数从长期来看是持续向上发展的,而在指数投资中,越早投资获得的收益越好。但对于大部分的式蒺族来说,大家的理财理念并没有得到较好的普及,况且投资是一项需要承担较高风险的活动,许多工薪族朋友只能看着自己的钱包在非理性消费与通货膨胀的影响下不断的缩水。有些拥有比较好的习惯的工薪族会将部分收入储蓄到银行卡中,但仍旧逃不过金钱被贬值的命运!
其实,工薪族如果了解“定投”这个概念的话,是可以将一部分的资金从银行卡里拿出来定期存到指数投资产品中的。如果去百度定投的概念,那么会出现一些关键字,比如“低估值”,“风险平摊”,“定期定额度”,“微笑曲线”等,如果对于定投不太了解,可以先去百度一下定投的理念。
接下来,我们构建一个以定期定额方式投资指数的模型,看看最终的投资效果如何。
模型描述:¶
- 最早日期选定在上证指数公布的那一天;
- 每月的第一个交易日买入上证指数1000元;
- 假设指数的净值已经缩小到个位数,即 1000 元可以正常交易;
- 持有到现在,没有卖出。
本模型不考虑交易费用与滑点,默认每次的投入本金都可以全部买进!
# 获取每个月的第一个交易日的数据
first_day = []
for i in range(len(df)):
date = df.index[i]
if i == 0:
first_day.append(date)
else:
last_date = df.index[i - 1]
if date.day < last_date.day:
first_day.append(date)
index_df = df.loc[first_day]
index_df.index
# 按照模型进行定投
month_df = index_df.copy()
month_df['pct_change'] = month_df['close'].pct_change()
month_df = month_df[['close', 'pct_change']] # 按月整合数据
save_money = []
hold_money = []
save_base = 1000
for i in range(len(month_df)):
if i == 0:
save_money.append(save_base)
hold_money.append(save_base)
else:
save_money.append(save_money[-1] + save_base)
hold_money.append(hold_money[-1] * (1 + month_df['pct_change'][i]) + save_base)
month_df['save_money'] = save_money
month_df['hold_money'] = hold_money
month_df.head(10)
# 计算定投、收益曲线
month_df['return_money'] = month_df['hold_money'] - month_df['save_money']
month_df[['save_money', 'hold_money', 'return_money']].plot(figsize=(14, 7))
plt.legend(['累积投入', '累积本息', '累积收入'])
plt.show()
print('累计投入: {}元'.format(month_df['save_money'][-1]))
print('累计收益: {}元'.format(month_df['return_money'][-1]))
print('最终本息累积: {}元'.format(month_df['hold_money'][-1]))
print('绝对收益率为: {}%'.format((month_df['return_money'][-1] / month_df['save_money'][-1]) * 100))
三、指数分析,知已知彼¶
从上面的模型可以看出,如果尽早的定投,并且在低估的时候开始定投,随着国家的发展,指数的不断攀升,累积的总体收益也是一直在上升的。虽然总体收益率不是很高,但在2015的时候,总资金曾达到160万左右。
由于此模型没有卖出,因此在牛市疯狂的时候,没有到盈利落实到口袋中,而在市场最高的位置,也在不断的投入资金,这样便将定投的平均成本摊高了。
因此,接下来我们想解决的问题是,能否在指数位置偏低的时候持续定投,而在指数位置走到某种高度以上时持续卖出呢?那用什么指标来评判指数的高低位置呢?
了解点价值投资者的朋友,都应该听说过 PE 和 PB,它们可以用来评估标的价格是处于低估还是高估位置。因此,下面我们将 PE 和 PB 运用到指数上来,看看能否带来效果。
下面使用简单的中位数方式,求取指数每天的PE与PB。
# 从聚宽获取上证指数的信息
index = '000001.XSHG' # 指数 code
index_info = get_security_info(index) # 指数信息
start_date = index_info.start_date # 指数开始时间
end_date = datetime.datetime.now().date() # 以当天为最后一天
index_name = index_info.display_name # 指数全称
# 定义一个函数,计算每天的成份股的平均pe/pb
def get_pe_pb(index_code, start_date, end_date):
def iter_pe_pb():
# 一个获取PE/PB的生成器
trade_date = get_trade_days(start_date=start_date, end_date=end_date)
for date in trade_date:
stocks = get_index_stocks(index_code, date)
q = query(valuation.pe_ratio,
valuation.pb_ratio
).filter(valuation.pe_ratio != None,
valuation.pb_ratio != None,
valuation.code.in_(stocks))
df = get_fundamentals(q, date)
# 通过分位值进行过滤异常值
# 这里并没有采用三倍标准差来去除极值,差异不大
quantile = df.quantile([0.25, 0.75])
df_pe = df.pe_ratio[(df.pe_ratio > quantile.pe_ratio.values[0]) &\
(df.pe_ratio < quantile.pe_ratio.values[1])]
df_pb = df.pb_ratio[(df.pb_ratio > quantile.pb_ratio.values[0]) &\
(df.pb_ratio < quantile.pb_ratio.values[1])]
yield date, df_pe.median(), df_pb.median()
dict_result = [{'date': value[0], 'pe': value[1], 'pb':value[2]} for value in iter_pe_pb()]
df_result = pd.DataFrame(dict_result)
df_result.set_index('date', inplace=True)
return df_result
df_pe_pb = get_pe_pb(index, start_date, end_date)
df_pe_pb.head(10)
# 可视化PE/PB曲线图
df_pe_pb.plot(figsize=(14, 7), subplots=True)
plt.show()
# 将PE/PB趋势与指数趋势一起展示,以作观察
_, axs = plt.subplots(ncols=2, figsize=(14, 5))
close = get_price(index, start_date=start_date, end_date=end_date).close
_df = pd.DataFrame()
_df['close'] = close
_df['pe'] = df_pe_pb.pe
_df['pb'] = df_pe_pb.pb
_df[['close', 'pe']].plot(secondary_y=['pe'], ax=axs[0], alpha=.8)
_df[['close', 'pb']].plot(secondary_y=['pb'], ax=axs[1], alpha=.8)
plt.show()
如上图所示,可以看出,PE 与 PB 的大小会随着市场的起伏而呈现正相关性的波动。PE 的波动区间大概在 10 到 70 倍之间,而 PB 的波动范围大概在 0 到 7 之间。
# 分析PE/PB数据分布情况
_, axs = plt.subplots(nrows=2, ncols=2, figsize=(14, 7))
sns.distplot(df_pe_pb.pe, ax=axs[0][0])
sns.boxplot(df_pe_pb.pe, ax=axs[0][1])
sns.distplot(df_pe_pb.pb, ax=axs[1][0])
sns.boxplot(df_pe_pb.pb, ax=axs[1][1])
plt.show()
这里,通过上图的正态分布图与箱线图可以看出,PE 与 PB 有两个峰值,PE 的值主要集中在 24~39 倍区间,PB 的值主要集中在 2.1~3.6 倍之间。
另外,牛市顶时对就的 PE 与 PB 值数量相当少,并且与中间区间的值的距离相对比较远,以至于在箱线图上成为了离群点。通过这一点可以说明牛市顶一闪而过,时间非常短,产生的数据量也非常少。
整体来说,上图反应了中国股市熊长牛短的特点。因此,想要抓住牛市的机会,是需要而心等待的。
# 观察PE/PB之间的关系
sns.jointplot(x='pb',y='pe', data=df_pe_pb, height=7)
plt.show()
PE 与 PB 都可以用来对指数进行估值,那到底用哪个比较好呢?
但通过上图的散点图发现,本研究对应的 PE 与 PB 数据存在线性相关的数据,也就是说这两个指标大致上是同步涨同步跌的,因此,无论用 PE 还是 PB 来进行估值,效果都差不多,因此,接下将使用 PE 进行高位位置的判断。
# 将PE分成十个分位,查看各分位PE数量
pe_array = df_pe_pb.pe.values
value_counts = pd.cut(pe_array, 10).value_counts()
print(value_counts)
plt.figure(figsize=(14, 4))
sns.barplot(x=np.arange(0, len(value_counts)),
y=value_counts.values)
plt.show()
上图是将 PE 的值分成了 10 个分位,对每个分位 PE 的数量进行统计,可是以发现:
- 第 2 个柱体是最高的,说明第 2 个 10 分位的 PE 数据量最多。
- 整体上来看,柱状图呈左偏形态,说明 PE 长时间处于 5 分位以下。
- 第 9 与第 10 个柱体代表的量少得可怜,说明高估值区的时间非常短。
# 刻画PE整体趋势的中等分位区间(40%~60%)
def show_quantile():
_df = pd.DataFrame()
df = df_pe_pb.copy()
df.index.name = None
_df['pe'] = df.pe
_df = _df
p_high = [_df.pe.quantile(i / 10.0) for i in [4, 5, 6]]
for p_h, i in zip(p_high, [4, 5, 6]):
_df[str(i / 10 * 100)+'%'] = p_h
low_p = _df[_df.pe < _df.pe.iloc[-1]]
quantile_now = low_p.shape[0] / _df.shape[0] # 当前百分位值
last_p = _df.pe[-1]
_df.plot(figsize=(14, 7))
show_quantile()
上图将当前 PE 按时间序列进行可视化,并用三条线标出了 40%、50%、60% 分位的位置。再结合上面的统计,可以得出:
- 低估区的数据数量为:2686
- 估值适中区的数据数量为:608
- 高估区的数据数量为:240
比值为 2684:608:240。低估区间的时间是高估区时间的11.19倍。
# 计算比值
low = value_counts[0:4].sum()
medin = value_counts[4:6].sum()
high = value_counts[6:10].sum()
print('比值({}:{}:{})'.format(low, medin, high))
四、模型构思,循序渐进¶
由于PE/PB数据是从聚宽数据而来,最早的时间是2005年的数据,因此,相较于1990年的数据来说,数据量减少了不止一点。但不影响接下来的研究。
通过上面的分析,接下来提出的设想是:设定一个可参考的估值区,当小于该估值时,进行定投,反之则持续卖出。
上面我们已经计算出,PE 40 到 60 的估值范围为 32.811 ~ 49.856 之间,这里我们设定此区间为适中估值区间。
模型的描述如下:
- 当 PE 处于适中估值区间时,不做任何操作;当月准备的定投金归入回收资金中。
- 当 PE 低于适中估值区间时,持续定投。
- 当 PE 高于适中估值区间时,持续卖出;卖出的金额与当月准备的定投金归入回收资金中。
本模型不考虑交易费用与滑点,默认每次的投入本金都可以全部买进!
# 获取每个月的第一个交易日
first_day = []
for i in range(len(df_pe_pb)):
date = df_pe_pb.index[i]
if i == 0:
first_day.append(date)
else:
last_date = df_pe_pb.index[i - 1]
if date.day < last_date.day:
first_day.append(date)
# 按月计算价格与涨跌幅度
close = get_price(index, start_date=df_pe_pb.index[0], end_date=df_pe_pb.index[-1])['close']
df = df_pe_pb.copy()
df['close'] = close
df = df.loc[first_day]
df['pct_change'] = df.close.pct_change()
df.head(10)
miden_estimation = (38.492, 49.856) # 中等估值的pe区间
save_money = [] # 每月定存
back_money = [] # 回收资金
hold_money = [] # 持仓资金
base_money = 1000 # 定投基准
def trade():
for i in range(len(df)):
pe = df['pe'][i] # 估值位
if i == 0: # 初始买入
# 1.计算买入金额
save_money.append(base_money)
# 2. 计算回收金额
back_money.append(0)
# 3. 计算持仓变化
hold_money.append(base_money)
continue
if pe <= miden_estimation[0]: # 执行买入计算
# 1.计算买入金额
save_money.append(base_money)
# 2. 计算回收金额
back_money.append(0)
# 3. 计算持仓变化
hold_money.append(hold_money[-1] * (1 + df['pct_change'][i]) + base_money)
elif pe >= miden_estimation[-1]: # 执行卖出计算
# 1. 计算买入金额
save_money.append(0)
# 2. 计算回收金额
back_money.append(base_money)
# 3. 计算持仓变化
hold_money.append(hold_money[-1] * (1 + df['pct_change'][i]) - base_money)
else:
# 1.计算买入金额
save_money.append(0)
# 2. 计算回收金额
back_money.append(0)
# 3. 计算持仓变化
hold_money.append(hold_money[-1] * (1 + df['pct_change'][i]))
trade()
df['save_money'] = save_money # 定投金额
df['save_money_cumsum'] = df['save_money'].cumsum() # 定投累计金额
df['hold_money'] = hold_money # 持仓金额
df['back_money'] = back_money # 回收金额
df['back_money_cumsum'] = df['back_money'].cumsum() # 累计回收金额
df['total_money'] = df['hold_money'] + df['back_money_cumsum'] # 总资金
df['return_money'] = df['total_money'] - df['save_money_cumsum'] # 持续收益
df['return_rate'] = (df['total_money'] / df['save_money_cumsum']) - 1 # 持续收益率
df[['save_money_cumsum', 'total_money', 'back_money_cumsum', 'return_money']].plot(figsize=(14, 7))
plt.legend(['累积定投', '累计本息', '回收资金', '收益曲线'])
plt.show()
print('累计投入: {}元'.format(df['save_money_cumsum'][-1]))
print('累计收益: {}元'.format(df['return_money'][-1]))
print('最终本息累积: {}元'.format(df['total_money'][-1]))
print('绝对收益率为: {}%'.format((df['return_money'][-1] / df['save_money_cumsum'][-1]) * 100))
# 展示各年投入金额
money_year = {}
for date in df.index:
year = date.year
if year in money_year.keys():
money_year[year] = money_year[year] + df.loc[date, 'save_money']
else:
money_year[year] = df.loc[date, 'save_money']
money_mean = mean(list(money_year.values()))
years_count = len(money_year) - 1
money_year = {key: [value] for key, value in money_year.items()}
df_money_year = pd.DataFrame(money_year, index=[''])
df_money_year = df_money_year.T
df_money_year.plot(figsize=(14, 4), kind='bar')
plt.hlines(money_mean, 0, years_count, color='orange')
plt.legend(['年均投入', '定投年金'])
plt.show()
# 展示各年的收益
return_year = {}
for date in df.index:
year = date.year
return_year[year] = df.loc[date, 'return_rate']
return_year = {key: [value] for key, value in return_year.items()}
return_df = pd.DataFrame(return_year, index=['return']).T
return_df['diff'] = return_df['return'].diff()
return_df['diff'].fillna(return_df['return'], inplace=True)
return_df[['diff']].plot(figsize=(14, 4), kind='bar')
plt.legend(['各年收益率'])
plt.show()
从上面的模型来看,整个投资区间,回收资金过少,即不能很好的在市场上涨的时候将钱落袋为安。
由于买入与卖出都是按一个基准来操作的,因此,这里设想,是否可以越跌则买的越多,而越涨越卖出得越多呢?
模拟描述:
- 当 PE 处于适中估值区间时,不做任何操作;当月准备的定投金归入回收资金中。
- 当 PE 低于适中估值区间时,持续定投;每低一个10%分位,则增加一倍倍投入。
- 当 PE 高于适中估值区间时,每高一个10%分位,则增加一倍卖出。在上面分析过程中发现低估值区间是高估值区间的11倍左右,因此,这里还在卖出的原倍数上乘以11.
本模型不考虑交易费用与滑点,默认每次的投入本金都可以全部买进!
# 获取每个月的第一个交易日
first_day = []
for i in range(len(df_pe_pb)):
date = df_pe_pb.index[i]
if i == 0:
first_day.append(date)
else:
last_date = df_pe_pb.index[i - 1]
if date.day < last_date.day:
first_day.append(date)
# 按月计算价格与涨跌幅度
close = get_price(index, start_date=df_pe_pb.index[0], end_date=df_pe_pb.index[-1])['close']
df = df_pe_pb.copy()
df['close'] = close
df = df.loc[first_day]
df['pct_change'] = df.close.pct_change()
df.head(10)
def get_how_value(pe):
how_value = [15.708,21.447,27.129,32.811,38.492,44.174,
49.856,55.538,61.219,66.901,72.583]
for i, value in zip(range(0, len(how_value)) , how_value):
# zip 包装了整数倍的分位值与对应的pe值区间
if how_value[i] <= pe < how_value[i + 1]:
location = i + 1
_how_value = 5 - location # 以5为中等值
return _how_value # 返回基于中位的买入或卖出倍数
miden_estimation = (38.492, 49.856) # 中等估值的pe区间
save_money = [] # 每月定存
back_money = [] # 回收资金
hold_money = [] # 持仓资金
base_money = 1000 # 定投基准
def trade():
for i in range(len(df)):
pe = df['pe'][i] # 估值位
how_value = get_how_value(pe)
if i == 0: # 初始买入
# 1.计算买入金额
save_money.append(base_money)
# 2. 计算回收金额
back_money.append(0)
# 3. 计算持仓变化
hold_money.append(base_money)
continue
if how_value > 0: # 执行买入计算
# 1.计算买入金额
save_money.append(base_money * how_value)
# 2. 计算回收金额
back_money.append(0)
# 3. 计算持仓变化
hold_money.append(hold_money[-1] * (1 + df['pct_change'][i]) + base_money * how_value)
else: # 执行卖出计算
# 1. 计算买入金额
save_money.append(0)
# 2. 计算回收金额
back_money.append(base_money * -how_value * 11)
# 3. 计算持仓变化
hold_money.append(hold_money[-1] * (1 + df['pct_change'][i]) - base_money * -how_value * 11)
trade()
df['save_money'] = save_money # 定投金额
df['save_money_cumsum'] = df['save_money'].cumsum() # 定投累计金额
df['hold_money'] = hold_money # 持仓金额
df['back_money'] = back_money # 回收金额
df['back_money_cumsum'] = df['back_money'].cumsum() # 累计回收金额
df['total_money'] = df['hold_money'] + df['back_money_cumsum'] # 总资金
df['return_money'] = df['total_money'] - df['save_money_cumsum'] # 持续收益
df['return_rate'] = (df['total_money'] / df['save_money_cumsum']) - 1 # 持续收益率
df[['save_money_cumsum', 'total_money', 'back_money_cumsum', 'return_money']].plot(figsize=(14, 7))
plt.legend(['累积定投', '累计本息', '回收资金', '收益曲线'])
plt.show()
print('累计投入: {}元'.format(df['save_money_cumsum'][-1]))
print('累计收益: {}元'.format(df['return_money'][-1]))
print('最终本息累积: {}元'.format(df['total_money'][-1]))
print('绝对收益率为: {}%'.format((df['return_money'][-1] / df['save_money_cumsum'][-1]) * 100))
# 展示各年投入金额
money_year = {}
for date in df.index:
year = date.year
if year in money_year.keys():
money_year[year] = money_year[year] + df.loc[date, 'save_money']
else:
money_year[year] = df.loc[date, 'save_money']
money_mean = mean(list(money_year.values()))
years_count = len(money_year) - 1
money_year = {key: [value] for key, value in money_year.items()}
df_money_year = pd.DataFrame(money_year, index=[''])
df_money_year = df_money_year.T
df_money_year.plot(figsize=(14, 4), kind='bar')
plt.hlines(money_mean, 0, years_count, color='orange')
plt.legend(['年均投入', '定投年金'])
plt.show()
# 展示各年的收益
return_year = {}
for date in df.index:
year = date.year
return_year[year] = df.loc[date, 'return_rate']
return_year = {key: [value] for key, value in return_year.items()}
return_df = pd.DataFrame(return_year, index=['return']).T
return_df['diff'] = return_df['return'].diff()
return_df['diff'].fillna(return_df['return'], inplace=True)
return_df[['diff']].plot(figsize=(14, 4), kind='bar')
plt.legend(['各年收益率'])
plt.show()
模型有了很大的进步,总体说来,资金的增长主要靠的是持续的定投与高位的加倍卖出举动。
但该模型在使用了从过支到当下计算的估值区间,我们期望可以使用一种动态追踪的估值数字,来指导我们做定投。
设想:将表态的pe按照近一段时间,来评估当下pe占过去历史百分位的高度,此区间随着时间的移动,一来可以发生动态变化,二来可以不受太旧历史数据的影响。
那这个历史区间设置多久呢?在上面的计算中,我们发现一个牛熊的运动大概在7.5年左右。因此,我们在这里设置这个时间区间为7.5年。
来看看依照此方式计算出的结果:
# 查看动态pe形态的分位趋势图
def get_quantile(index_code, p, n, data):
"""指数百分位展示。
Args:
index_code: 指数 code。
p: 可以是 pe,也可以是 pb。
n: 指用于计算指数估值百分位的区间,如果是5指近5年数据。
data: 包含有 pe/pb 的 DataFrame。
Returns:
计算后的DataFrame。
"""
# 这里的计算按一年244个交易日计算
windows = int(n * 244) # 将时间取整数
_df = data.copy()
_df.index.name = None
price = get_price(index_code, start_date=_df.index[0], end_date=_df.index[-1])
_df['close'] = price.close
_df['quantile'] = _df[p].rolling(windows).apply(lambda x: pd.Series(x).rank().iloc[-1] /
pd.Series(x).shape[0], raw=True)
_df.dropna(inplace=True)
_df['quantile'].plot(figsize=(14, 7))
# 画出适中估值区间
plt.fill_between(_df.index, y1=0.4, y2=0.6, color='orange', alpha=0.7)
plt.annotate('适中估值区', (_df.index[-1], 0.5))
return _df
# 展示指数百分位趋势图
df_quantile = get_quantile(index, 'pe', 7.45, df_pe_pb)
# pe动态分位图与指数高低位的比较
def show_quantile(index_code, p, n, data):
"""指数百分位展示。
Args:
index_code: 指数 code。
p: 可以是 pe,也可以是 pb。
n: 指用于计算指数估值百分位的区间,如果是5指近5年数据。
data: 包含有 pe/pb 的 DataFrame。
Returns:
None.
"""
# 这里的计算按一年244个交易日计算
windows = int(n * 244) # 将时间取整数
_df = data.copy()
_df.index.name = None
price = get_price(index_code, start_date=_df.index[0], end_date=_df.index[-1])
_df['close'] = price.close
_df['quantile'] = _df[p].rolling(windows).apply(lambda x: pd.Series(x).rank().iloc[-1] /
pd.Series(x).shape[0], raw=True)
_df.dropna(inplace=True)
_df[['quantile', 'close']].plot(figsize=(14, 10), subplots=True)
# 画出适中估值区间
# 展示指数百分位趋势图
show_quantile(index, 'pe', 7.5, df_pe_pb)
由上我们可以看出,pe近7.5年的动态分位图可以比较恰当的描述指数的高低起伏。
由此作出以下模型构思。
模型描述:
- 以近7.5年的pe分位来指导定投操作;
- 当分位值低于适中估值区间时,按倍增法买入;
- 当分位值处于适中估值区间时,不做任何操作;
- 当分位值高于适中估值区间时,按照立方指数数倍卖出。
本模型不考虑交易费用与滑点,默认每次的投入本金都可以全部买进!
# 获取每个月的第一个交易日
first_day = []
for i in range(len(df_quantile)):
date = df_quantile.index[i]
if i == 0:
first_day.append(date)
else:
last_date = df_quantile.index[i - 1]
if date.day < last_date.day:
first_day.append(date)
# 按月计算价格与涨跌幅度
close = get_price(index, start_date=df_quantile.index[0], end_date=df_quantile.index[-1])['close']
df = df_quantile.copy()
df['close'] = close
df = df.loc[first_day]
df['pct_change'] = df.close.pct_change()
df.head(10)
save_money = [] # 每月定存
back_money = [] # 回收资金
hold_money = [] # 持仓资金
base_money = 1000 # 定投基准
def trade():
for i in range(len(df)):
quantile = df['quantile'][i] # 估值位
multiple = int((0.5 - quantile) * 10) # 定投倍数计算
if i == 0: # 初始买入
# 1.计算买入金额
_save_money = base_money * multiple
save_money.append(_save_money)
# 2. 计算回收金额
_back_money = 0
back_money.append(_back_money)
# 3. 计算持仓变化
_hold_money = _save_money
hold_money.append(_hold_money)
continue
if multiple >=0: # 执行买入计算
# 1.计算买入金额
_save_money = base_money * multiple
save_money.append(_save_money)
# 2. 计算回收金额
_back_money = 0
back_money.append(_back_money)
# 3. 计算持仓变化
_hold_money = hold_money[-1] * (1 + df['pct_change'][i]) + _save_money
hold_money.append(_hold_money)
else: # 执行卖出计算
# 1. 计算买入金额
_save_money = 0
save_money.append(_save_money)
# 2. 计算回收金额
_back_money = base_money * (2 ** -multiple) # 按2的指数倍卖出
_hold_money = hold_money[-1] * (1 + df['pct_change'][i])
if _back_money > _hold_money:
_back_money = _hold_money
if quantile >= 1.0:
_back_money = _hold_money # 如果达到100%分位,清仓
back_money.append(_back_money)
# 3. 计算持仓变化
_hold_money = _hold_money - _back_money
hold_money.append(_hold_money)
trade()
df['save_money'] = save_money # 定投金额
df['save_money_cumsum'] = df['save_money'].cumsum() # 定投累计金额
df['hold_money'] = hold_money # 持仓金额
df['back_money'] = back_money # 回收金额
df['back_money_cumsum'] = df['back_money'].cumsum() # 累计回收金额
df['total_money'] = df['hold_money'] + df['back_money_cumsum'] # 总资金
df['return_money'] = df['total_money'] - df['save_money_cumsum'] # 持续收益
df['return_rate'] = (df['total_money'] / df['save_money_cumsum']) - 1 # 持续收益率
df[['save_money_cumsum', 'total_money', 'back_money_cumsum', 'return_money']].plot(figsize=(14, 7))
plt.legend(['累积定投', '累计本息', '回收资金', '收益曲线'])
plt.show()
print('累计投入: {}元'.format(df['save_money_cumsum'][-1]))
print('累计收益: {}元'.format(df['return_money'][-1]))
print('最终本息累积: {}元'.format(df['total_money'][-1]))
print('绝对收益率为: {}%'.format((df['return_money'][-1] / df['save_money_cumsum'][-1]) * 100))
# 展示各年投入金额
money_year = {}
for date in df.index:
year = date.year
if year in money_year.keys():
money_year[year] = money_year[year] + df.loc[date, 'save_money']
else:
money_year[year] = df.loc[date, 'save_money']
money_mean = mean(list(money_year.values()))
years_count = len(money_year) - 1
money_year = {key: [value] for key, value in money_year.items()}
df_money_year = pd.DataFrame(money_year, index=[''])
df_money_year = df_money_year.T
df_money_year.plot(figsize=(14, 4), kind='bar')
plt.hlines(money_mean, 0, years_count, color='orange')
plt.legend(['年均投入', '定投年金'])
plt.show()
# 展示各年的收益
return_year = {}
for date in df.index:
year = date.year
return_year[year] = df.loc[date, 'return_rate']
return_year = {key: [value] for key, value in return_year.items()}
return_df = pd.DataFrame(return_year, index=['return']).T
return_df['diff'] = return_df['return'].diff()
return_df['diff'].fillna(return_df['return'], inplace=True)
return_df[['diff']].plot(figsize=(14, 4), kind='bar')
plt.legend(['各年收益率'])
plt.show()
由于采用了动态计算pe百分位高度的方式,又牺牲掉了7.5年时间的数据,因此这个模型的数据更少。
但通过观察,发现整体的收益很可观,再结果文章开始对于下个牛市的展望,我们可以期望在2021年左右,获得一次资产翻倍的机会。
另外,在定投的过程中,可以将加收的资金买入国债,以增加收益,如果经过了多轮牛熊后,更可以将回收的资金再次投入的下一次的定投中去,以达成在低估值区间买入更多份额的目标。
由于篇幅有限,这两种情况就不作演算了。
最后说一点,由于该模型是在历史几个牛熊数据上推理优化而得,因此,这是一个过拟合模型。但为何还要去研究呢?是因为,这一切都建立在指数有效的假设上。即:我们相信,中国的运势会越来越好,指数有低谷,也终将有高潮!
接下来,我们将上面的模型尝试运用到沪深300指数上,检验一下效果。
index = '000300.XSHG' # 指数 code
index_info = get_security_info(index) # 指数信息
start_date = index_info.start_date # 指数开始时间
end_date = datetime.datetime.now().date() # 以当天为最后一天
index_name = index_info.display_name # 指数全称
df_pe_pb = get_pe_pb(index, start_date, end_date)
df_pe_pb.head(10)
# 展示指数百分位趋势图
df_quantile = get_quantile(index, 'pe', 7.45, df_pe_pb)
# 展示指数百分位趋势图
show_quantile(index, 'pe', 7.45, df_pe_pb)
# 获取每个月的第一个交易日
first_day = []
for i in range(len(df_quantile)):
date = df_quantile.index[i]
if i == 0:
first_day.append(date)
else:
last_date = df_quantile.index[i - 1]
if date.day < last_date.day:
first_day.append(date)
# 按月计算价格与涨跌幅度
close = get_price(index, start_date=df_quantile.index[0], end_date=df_quantile.index[-1])['close']
df = df_quantile.copy()
df['close'] = close
df = df.loc[first_day]
df['pct_change'] = df.close.pct_change()
df.head(10)
save_money = [] # 每月定存
back_money = [] # 回收资金
hold_money = [] # 持仓资金
base_money = 1000 # 定投基准
def trade():
for i in range(len(df)):
quantile = df['quantile'][i] # 估值位
multiple = int((0.5 - quantile) * 10) # 定投倍数计算
if i == 0: # 初始买入
# 1.计算买入金额
_save_money = base_money * multiple
save_money.append(_save_money)
# 2. 计算回收金额
_back_money = 0
back_money.append(_back_money)
# 3. 计算持仓变化
_hold_money = _save_money
hold_money.append(_hold_money)
continue
if multiple >=0: # 执行买入计算
# 1.计算买入金额
_save_money = base_money * multiple
save_money.append(_save_money)
# 2. 计算回收金额
_back_money = 0
back_money.append(_back_money)
# 3. 计算持仓变化
_hold_money = hold_money[-1] * (1 + df['pct_change'][i]) + _save_money
hold_money.append(_hold_money)
else: # 执行卖出计算
# 1. 计算买入金额
_save_money = 0
save_money.append(_save_money)
# 2. 计算回收金额
_back_money = base_money * (2 ** -multiple) # 按2的指数倍卖出
_hold_money = hold_money[-1] * (1 + df['pct_change'][i])
if _back_money > _hold_money:
_back_money = _hold_money
if quantile >= 1.0:
_back_money = _hold_money # 如果达到100%分位,清仓
back_money.append(_back_money)
# 3. 计算持仓变化
_hold_money = _hold_money - _back_money
hold_money.append(_hold_money)
trade()
df['save_money'] = save_money # 定投金额
df['save_money_cumsum'] = df['save_money'].cumsum() # 定投累计金额
df['hold_money'] = hold_money # 持仓金额
df['back_money'] = back_money # 回收金额
df['back_money_cumsum'] = df['back_money'].cumsum() # 累计回收金额
df['total_money'] = df['hold_money'] + df['back_money_cumsum'] # 总资金
df['return_money'] = df['total_money'] - df['save_money_cumsum'] # 持续收益
df['return_rate'] = (df['total_money'] / df['save_money_cumsum']) - 1 # 持续收益率
df[['save_money_cumsum', 'total_money', 'back_money_cumsum', 'return_money']].plot(figsize=(14, 7))
plt.legend(['累积定投', '累计本息', '回收资金', '收益曲线'])
plt.show()
print('累计投入: {}元'.format(df['save_money_cumsum'][-1]))
print('累计收益: {}元'.format(df['return_money'][-1]))
print('最终本息累积: {}元'.format(df['total_money'][-1]))
print('绝对收益率为: {}%'.format((df['return_money'][-1] / df['save_money_cumsum'][-1]) * 100))
# 展示各年投入金额
money_year = {}
for date in df.index:
year = date.year
if year in money_year.keys():
money_year[year] = money_year[year] + df.loc[date, 'save_money']
else:
money_year[year] = df.loc[date, 'save_money']
money_mean = mean(list(money_year.values()))
years_count = len(money_year) - 1
money_year = {key: [value] for key, value in money_year.items()}
df_money_year = pd.DataFrame(money_year, index=[''])
df_money_year = df_money_year.T
df_money_year.plot(figsize=(14, 4), kind='bar')
plt.hlines(money_mean, 0, years_count, color='orange')
plt.legend(['年均投入', '定投年金'])
plt.show()
# 展示各年的收益
return_year = {}
for date in df.index:
year = date.year
return_year[year] = df.loc[date, 'return_rate']
return_year = {key: [value] for key, value in return_year.items()}
return_df = pd.DataFrame(return_year, index=['return']).T
return_df['diff'] = return_df['return'].diff()
return_df['diff'].fillna(return_df['return'], inplace=True)
return_df[['diff']].plot(figsize=(14, 4), kind='bar')
plt.legend(['各年收益率'])
plt.show()