请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  数理科学 帖子:3154395 新帖:0

量化定投,工薪族逆袭之路(附定投模型)

醒掌天下权发表于:8 月 8 日 23:20回复(1)

量化定投,工薪族的逆袭之路¶

一个人一生能积累多少钱,不是取决于他能够赚多少钱,而是取决于他如何投资理财,人找钱不如钱找钱,要知道让钱为你工作,而不是你为钱工作。——(美)沃伦●巴菲特

# 导入常用的库
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')

一、 指数格局,跌宕起伏¶

这里的指数,我们重点说一下上证指数、深圳指数。指数其实是一篮子股票,它反应的这些股票总体的表现。而上证与深圳指数更反应出当下国内的经济形式(当然不是百分百的呈现)。

相信大家都了解过经济周期,理论上,社会环境的经济会以衰退-萧条-复苏-繁荣四种形式往复呈现。不过,由于不同国家的国情不尽相同,这种周而复始的周期曲线表现得并不完美。

下图为经济周期曲线图:

image.png

那么,股市是否也会呈现一定的周期性呢?如果具有周期性,如何估算牛熊之间的时间距离呢?带着这样的疑问,接着往下探究。

在探究此问题之前,可以去查找一下相关的历史资料,看看是否有人已经给出答案,或者可以找一些重要的线索。经过百度,收集到:上证指数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))
牛顶平均间隔时间: ['3042 days', '2313 days', '2800 days']
平均时间间隔 2718.33 天,即 7.45 年
平均时间间隔偏差 303.16 天,即 0.83 年
下个牛顶时间范围 2022年1月 ~ 2023年9月
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% 预言未来!所以大家还要带着辩证的心态看待这个结果。

通过上面的统计与可视化,可得出以下结论:

  1. 一轮牛熊的平均时间间隔为7.5年左右;
  2. 4轮牛熊的时间偏差在0.8年左右;
  3. 依此估算的下个牛顶的时间范围在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)))
最初收盘价为 99.98,当前收盘价为 2978.8784
持有到目前为止的收益率为 2879.47%
平均每年收益率为 103.22%
年化收益率为 12.8%

看到这里,可能小伙伴们都张大了嘴大喊:“这不可能!我不相信!”是的,12.8% 的年化复利,的确很夸张。但这并不是表明指数收益相当可观,这其中的原因是:

  1. 指数刚上市的时候净值很低;
  2. 中国的经济已经发生了天翻地覆的变化;
  3. 持有的时间相对长;
  4. 没有考虑通货膨胀与金钱的时间价值。

现在我们已经知道,指数从长期来看是持续向上发展的,而在指数投资中,越早投资获得的收益越好。但对于大部分的式蒺族来说,大家的理财理念并没有得到较好的普及,况且投资是一项需要承担较高风险的活动,许多工薪族朋友只能看着自己的钱包在非理性消费与通货膨胀的影响下不断的缩水。有些拥有比较好的习惯的工薪族会将部分收入储蓄到银行卡中,但仍旧逃不过金钱被贬值的命运!

其实,工薪族如果了解“定投”这个概念的话,是可以将一部分的资金从银行卡里拿出来定期存到指数投资产品中的。如果去百度定投的概念,那么会出现一些关键字,比如“低估值”,“风险平摊”,“定期定额度”,“微笑曲线”等,如果对于定投不太了解,可以先去百度一下定投的理念。

接下来,我们构建一个以定期定额方式投资指数的模型,看看最终的投资效果如何。

模型描述:¶

  1. 最早日期选定在上证指数公布的那一天;
  2. 每月的第一个交易日买入上证指数1000元;
  3. 假设指数的净值已经缩小到个位数,即 1000 元可以正常交易;
  4. 持有到现在,没有卖出。

本模型不考虑交易费用与滑点,默认每次的投入本金都可以全部买进!

# 获取每个月的第一个交易日的数据
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
DatetimeIndex(['1990-12-19', '1991-01-02', '1991-02-01', '1991-03-01',
               '1991-04-01', '1991-05-02', '1991-06-03', '1991-07-01',
               '1991-08-01', '1991-09-02',
               ...
               '2018-09-03', '2018-10-08', '2018-11-01', '2018-12-03',
               '2019-01-02', '2019-02-01', '2019-03-01', '2019-04-01',
               '2019-05-06', '2019-06-03'],
              dtype='datetime64[ns]', length=343, freq=None)
# 按照模型进行定投
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)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
close pct_change save_money hold_money
1990-12-19 99.98 NaN 1000 1000.000000
1991-01-02 128.84 0.288658 2000 2288.657732
1991-02-01 129.51 0.005200 3000 3300.559320
1991-03-01 132.53 0.023319 4000 4377.523950
1991-04-01 120.73 -0.089036 5000 4987.764781
1991-05-02 113.16 -0.062702 6000 5675.022468
1991-06-03 115.97 0.024832 7000 6815.945172
1991-07-01 136.85 0.180047 8000 9043.132679
1991-08-01 145.24 0.061308 9000 10597.549071
1991-09-02 180.22 0.240843 10000 14149.891858
# 计算定投、收益曲线
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))
累计投入: 343000元
累计收益: 583956.3789768222元
最终本息累积: 926956.3789768222元
绝对收益率为: 170.24967317108522%

三、指数分析,知已知彼¶

从上面的模型可以看出,如果尽早的定投,并且在低估的时候开始定投,随着国家的发展,指数的不断攀升,累积的总体收益也是一直在上升的。虽然总体收益率不是很高,但在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)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
pb pe
date
2005-01-04 2.01585 26.53715
2005-01-05 2.04450 26.94350
2005-01-06 2.02165 26.61115
2005-01-07 2.02360 26.77050
2005-01-10 2.04630 26.94320
2005-01-11 2.05100 26.99430
2005-01-12 2.04245 26.78615
2005-01-13 2.06330 26.81250
2005-01-14 2.02760 26.63980
2005-01-17 1.96960 25.85190
# 可视化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()
(15.708, 21.447]     248
(21.447, 27.129]    1170
(27.129, 32.811]     523
(32.811, 38.492]     757
(38.492, 44.174]     490
(44.174, 49.856]     118
(49.856, 55.538]     100
(55.538, 61.219]     103
(61.219, 66.901]      25
(66.901, 72.583]      12
dtype: int64

上图是将 PE 的值分成了 10 个分位,对每个分位 PE 的数量进行统计,可是以发现:

  1. 第 2 个柱体是最高的,说明第 2 个 10 分位的 PE 数据量最多。
  2. 整体上来看,柱状图呈左偏形态,说明 PE 长时间处于 5 分位以下。
  3. 第 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))
比值(2698:608:240)

四、模型构思,循序渐进¶

由于PE/PB数据是从聚宽数据而来,最早的时间是2005年的数据,因此,相较于1990年的数据来说,数据量减少了不止一点。但不影响接下来的研究。

通过上面的分析,接下来提出的设想是:设定一个可参考的估值区,当小于该估值时,进行定投,反之则持续卖出。

上面我们已经计算出,PE 40 到 60 的估值范围为 32.811 ~ 49.856 之间,这里我们设定此区间为适中估值区间。

模型的描述如下:

  1. 当 PE 处于适中估值区间时,不做任何操作;当月准备的定投金归入回收资金中。
  2. 当 PE 低于适中估值区间时,持续定投。
  3. 当 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)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
pb pe close pct_change
date
2005-01-04 2.01585 26.53715 1242.77 NaN
2005-02-01 1.86520 24.02880 1188.93 -0.043323
2005-03-01 2.09300 26.67730 1303.41 0.096288
2005-04-01 1.86500 25.19810 1223.57 -0.061255
2005-05-09 1.59800 22.17390 1130.84 -0.075786
2005-06-01 1.57390 20.97550 1039.19 -0.081046
2005-07-01 1.55790 21.28340 1055.59 0.015782
2005-08-01 1.49010 20.81670 1088.95 0.031603
2005-09-01 1.71190 22.38690 1184.93 0.088140
2005-10-10 1.69390 22.21185 1138.95 -0.038804
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))
累计投入: 133000元
累计收益: 49155.86259384008元
最终本息累积: 182155.86259384008元
绝对收益率为: 36.95929518333841%
# 展示各年投入金额
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()

从上面的模型来看,整个投资区间,回收资金过少,即不能很好的在市场上涨的时候将钱落袋为安。

由于买入与卖出都是按一个基准来操作的,因此,这里设想,是否可以越跌则买的越多,而越涨越卖出得越多呢?

模拟描述:

  1. 当 PE 处于适中估值区间时,不做任何操作;当月准备的定投金归入回收资金中。
  2. 当 PE 低于适中估值区间时,持续定投;每低一个10%分位,则增加一倍倍投入。
  3. 当 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)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
pb pe close pct_change
date
2005-01-04 2.01585 26.53715 1242.77 NaN
2005-02-01 1.86520 24.02880 1188.93 -0.043323
2005-03-01 2.09300 26.67730 1303.41 0.096288
2005-04-01 1.86500 25.19810 1223.57 -0.061255
2005-05-09 1.59800 22.17390 1130.84 -0.075786
2005-06-01 1.57390 20.97550 1039.19 -0.081046
2005-07-01 1.55790 21.28340 1055.59 0.015782
2005-08-01 1.49010 20.81670 1088.95 0.031603
2005-09-01 1.71190 22.38690 1184.93 0.088140
2005-10-10 1.69390 22.21185 1138.95 -0.038804
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))
累计投入: 319000元
累计收益: 288684.23619299044元
最终本息累积: 607684.2361929904元
绝对收益率为: 90.49662576582772%
# 展示各年投入金额
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