摘要与声明
1:本文将蒙特卡洛模拟的理念运用在股价预测上;
2:本文所使用的行情数据通过Tushare(ID:444829)金融大数据平台接口获取;
3:笔者希望搭建出一套交易体系,原则是只做干货的分享。后续将更新更多模块,但工作学习之余的闲暇时间有限,更新速度慢还请谅解;
4:文中假设与观点是基于笔者对模型及数据的一孔之见,若有不同见解欢迎随时留言交流;
5:模型实现基于python3.8;
目录
1. 一个天才的故事
2. 捉摸不定——布朗运动
2.1 布朗运动的特点
2.2 股价背后的物理学逻辑
3. 最为致命——蒙特卡洛模拟
4. 模拟股价
5. 浅谈现实意义
5.1 布朗运动的现实意义
5.2 蒙特卡洛模拟的现实意义
6. 写在后面
老样子,对长篇大论不感兴趣的童鞋可以直接看第四部分的结论。
1. 一个天才的故事
在人类在认识的世界的道路上一直保持这一颗好奇的心,一些看似没有联系的事件却能巧妙的通过研究其中的内在共性并找到关联,不过在这条探(zuo)索(si)路上失败的案例也是屡见不鲜。相信人人都知道发现万有引力的牛顿,没错,就是那个被苹果砸到的天才艾萨克·牛顿,但是牛顿炒股的悲惨经历却鲜有耳闻。
看看度娘是怎么评价牛顿的:
这位百科全书式的全才选手早已不满足与数学,物理,力学,甚至经济学,牛顿将他的魔抓伸向了股票市场。
公元1720年,那时正值英国股市大热,当时英国的社会上,许多人借钱加杠杆炒股,股票市场的风光无两犹如15,08年国内证券交易所内的盛世光景。眼看着别人都赚了,年逾七旬的牛顿老爷爷也坐不住了,一口气拿出7000英镑的血本杀入股市(那时候这些钱在今天够买几栋别墅的)。结果很快就翻了一番,变成14000英镑。牛顿一看这还得了,股票几天顶得上一辈子的积蓄,去你的万有引力吧。
于是在1720年4月,理智被人性所侵蚀掉的牛顿杀红两眼,融资加杠杆,斥巨资再度入市,买了当时涨幅最大的股票——英国南海公司。 同年6月,英国通过股市反泡沫法案,股市陷入巨大危机。也正如A股15年、08年那样,雪崩之下没有一片雪花是无辜的。牛顿老爷爷的南海公司惨遭腰斩,因为杠杆的关系不仅没赚反欠一屁股债务,以割肉2W英镑的代价惨淡离场并留下一句话:“I can calculate the motions of heavenly bodies, but not the madness of people(我能预测天体的运行,却预测不了人性)”。
显然08年、15年的A股股民没有听牛顿老爷爷的话,事实上只要人性还在,这样的历史今后还会上演。这个故事告诉我们,术业有专攻,一次两次的成功不代表会一直成功,要时刻保持谦虚和谨慎。
还有,不懂就不要乱加杠杆,不要乱加杠杆,不要乱加杠杆!你以为撬动的是地球,实际在撬动深渊。
2. 捉摸不定——布朗运动
同样是埋头研究的,法国一位数学家Louis Bachelier将股价和花粉粒子在液面上的无规则运动联系起来。1827年,Robert Brown用显微镜观察水中花粉运动时发现了无规则运动这一特征,在此后的近半个世纪内这种无规则运动成为谜一样的存在。直到1905年(爱因斯坦奇迹年),爱因斯坦其中一篇论文,通过统计力学将之解释为水分子的撞击导致花粉无规则运动,至此无规则运动的奥义被慢慢发扬光大并以Robert Brown的名字命名为布朗运动。
图一:花粉微粒在液面的无规则运动
2.1 布朗运动的特点
水分子的无序撞击,水分子从各个方向撞击花粉粒子,且上一次撞击与下一次撞击完全没有关系,即独立增量。最后,由于布朗运动的无序,它是连续不可微的。(这里不做过多展开了,毕竟我们不是真在研究物理)
2.2 股价背后的物理学逻辑
什么最能代表股价?基本面?技术面?资金?主力?政策?各个流派都会有自己不同的见解,笔者尝试着理解各家的看法,今天站在物理学的角度强行解释股价运动(隔壁老缠师表示坐不住了)。笔者认为有其合理依据,但这不论是站在基本面,还是技术面的角度都无法解释。同样的,基本面无法解释技术面,技术面无法解释基本面,世界本就是矛盾的存在。
股价与布朗运动有着相似特征:
1):正态增量,如图二,笔者在之前的文章中曾经将股票和指数近十年单日涨跌幅做过分布,结果发现它非常接近于一个正态分布。事实上把其它股票数据拉出来,只要足够大的样本,它们都是正态分布的。笔者认为这是由于公开市场交易的缘故,即有足够多的买方与卖方,样本量一大便很容易形成正态分布,而对于许多另类投资的回报就很难用正态分布描述。
图二:上次写估值模型计算β时用的图
2):独立增量,即是今天的价格与明天的价格没有任何关系,明天的收益率是独立的。对于这条笔者认为有待考究,首先得承认市场是有效市场。但对于美股都才是未达到半强有效的状态,更遑论A股?但仔细想想市场在很多时候都处于不温不火的横盘震荡阶段,这时候的股价可以称得上是无头苍蝇一样完全的随机游走。总之笔者对此持怀疑态度,毕竟动量效应,涨停第二日必有冲高基本是大概率事件,下跌反弹收长下影线也是大概率事件,笔者对此也就不过度评述。基本面请大可不必跳出来反驳,CAPM模型那极为严苛的条件不还是照样硬着头皮拿来做估值?
3):连续不可微,股价有涨有跌,K线与无序运动的花粉一样不是光滑的曲线。正是由于K线这种独特的性质,数学处理起来十分困难,普通求导,微分等等常用数学工具完全失效。但布朗运动却具有相似特点,笔者通过在一只个股历史数据随机抽取日涨跌幅的方式无脑构建K线。
以代码复现则有:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
pro = ts.pro_api("token") # 添加自己的token
df = pro.index_daily(ts_code=mk, start_date=start, end_date=end) #设置参数
r = df["pct_chg"] / 100
ln_r = np.log(1 + r)
sns.kdeplot(ln_r)
price_start = np.log(df["close"][0])
days = 200 # 预测天数
mean = ln_r.mean()
std_div = ln_r.var()**0.5
final_dis,price_point,data = [],[],[]
adder = 0
for i in range(1, days + 2):
variable = random_num = np.random.normal(loc=mean, scale=std_div, size=1)
adder += variable[0]
price_point.append(np.exp(price_start + adder))
date.append(i)
data = {"price": price_point, "date": date}
final_dis.append(price_point[100])
sns.lineplot(x="date", y="price", data=data)
plt.show()
这次笔者就不写爬虫程序了,直接通过Tushare的数据接口即可获得行情数据,用之前需要申请token,使用代码时先实例化接口,然后.index_daily(ts_code=mk, start_date=start, end_date=end) 输入需要的相关参数请求即可,非常方便。
笔者取某A股股票,取过去10年单日涨跌幅数据,预测期间为200天,运行得:
图三:两次随机运行
上图中的蓝线为运行结果,红线是笔者后期所添加的技术分析。。。
不得不说这种系统随机游走得到的结果与真实的K线并无二异,麻雀虽小五脏俱全,你甚至可以在上面找到趋势线,整理平台,下跌中继形态,底部反转形态,没事还能数数艾略特波浪。如果把它画成传统的蜡烛图,笔者丝毫不怀疑能在最高点找到并列阴阳线,能在最低点找到一根长长的下影线金针探底。而这一切的一切在现实中都是不存在的,别忘了这仅仅是电脑所意淫出的,并不存在于真实世界。
3. 最为致命——蒙特卡洛模拟
为什么将它命名为“最为致命”呢?原因就在于其极限逼近的思想将事件中的不确定性转化为一个概率分布,通过区间估计可以大大提高预测结果的无偏性和可验证性。接下来笔者通过一个经典的求解圆周率的案例来展示蒙特卡洛模拟“极限逼近”思想:
这是个简单又非常巧妙的案例,假设我们有一个正方形和一个四分之一圆,它们的半径和边长均为1,那么阴影部分的面积是?
图四:圆周率问题
这不是小学数学题吗?1-2π*r^2/4,如果我们不知道这个π呢?
要知道人类在计算π值上花了上千年的功夫,在1500年前祖冲之通过割圆法计算圆周率,那时的割出了24576边形,无限接近于一个圆,将π精确到六位小数,在没有计算器的年代已经是个非常伟大的成就了。
与割圆不同,我们通过在这个正方形内随机打出一个个点的方式也可以将圆周率求出来,如图三,打到半圆内的点记为红点,半圆外的点记为黑点:
图五:圆周率的面积解法
想象一下我们打出了无穷多个点,可以把正方形面积全部覆盖住,那么这时有:
1/4圆面积=红点个数/(红点个数+黑点个数)
因为圆半径是1,此时所求得的面积乘以4即是整圆面积,也是圆周率π。
将上述实验以代码复现则有:
import random
times = [10,100, 1000, 10000, 100000,5000000,10000000] #实验次数
for i in times:
counter=0 #成功次数
for a in range(i):
x = random.random()
y = random.random()
len = (x**2+y**2)**0.5
if len <=1: #半径小于1,在圆内
counter += 1
print("{}次实验,圆周率:".format(i), counter*4/i)
运行得:
图六:圆周率的极限逼近
实验次数从10次增加到1千万次,我们所求得的圆面积,或者说圆周率从3.2不断向精确的3.1415926逼近着(笔者不禁感慨如今的计算机强大,敲敲键盘便执行上千万次实验,祖冲之那个年代连算盘都没有,凭着几根竹签子竟然将圆周率精确到小数点后六位,实在佩服古人的智慧)。类似这种的实验还有著名的浦丰针投,但无论实验如何设计,其根本的思维都在于极限逼近的思想。现在,即使是用笔者性能拉跨的老爷机,多花些时间执行它个几十亿次后圆周率也会精确到祖冲之老爷爷也不敢想象的地步。
事实上我们刚刚已经不知不觉的完成了一次简单的蒙特卡洛模拟——在纸上不断打出点,随着实验次数足够多后概率会无限接近与真实值附近。那么这样极限逼近的思维能否用于股价预测呢?
笔者的答案是:人有多大胆,地有多大产。隔壁的老缠师日夜研究缠论,笔者作为基本面技术面双修的分析湿自然要走走不寻常路线。
4. 模拟股价
有了上面布朗运动与蒙特卡洛模拟的理论基础,我们不妨将两者结合起来。
只进行一次布朗运动,未来股价按这样走的概率几乎为0,那么如同计算圆周率那样,不断提高实验次数,最终得到的结果将会稳定在某个值附近,而那个值便是在这种理论框架下的未来最有可能发生的真实股价。与圆周率打点不同的是,股价是时间序列数据,我们不能像求圆面积那样无脑的处理数据。
将每日回报率记为r,将预测区间记为0~T个交易日, 则我们有:r ~ N (
,
) T个交易日后的股价
为:
由于单个交易日回报服从r ~ N (
,
), 由分布特性我们可知,N期后股价是服从log Normal分布。
将上述过程以代码复现有:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
pro = ts.pro_api("token")
df = pro.index_daily(ts_code=mk, start_date=start, end_date=end)
def dis_fun(df):
r = df["pct_chg"]/100
ln_r = np.log(1+r)
sns.kdeplot(ln_r)
plt.axvline(ln_r.mean(),label = 'mean {:.3f}%'.format(ln_r.mean()*100),
linestyle = '-.',color = 'r')
plt.axvline(ln_r.median(),label = "median {:.3f}%"
.format(ln_r.median()*100), linestyle = ':',color = 'g')
print("对数下:","偏度:",ln_r.skew(),"峰度:",ln_r.kurt(), "均值:", ln_r.mean(), "中位数:",ln_r.median())
plt.title("ln_distribution of return")
plt.legend()
plt.show()
return ln_r.mean(), ln_r.var()**0.5
def random_generator(mean, std_div):
random_num = np.random.normal(loc=mean, scale=std_div, size=1)
return random_num
def price(times, price_start, daies, mean, var):
final_dis = []
for i in range(times):
price_point = []
date = []
adder = 0
for i in range(1, daies+2):
variable = random_generator(mean,var)
adder += variable[0]
price_point.append(np.exp(price_start + adder))
date.append(i)
data = {"price":price_point, "date":date}
final_dis.append(price_point[100])
sns.lineplot(x ="date", y = "price", data = data)
plt.show()
final_dis = np.array(final_dis)
sns.kdeplot(final_dis)
plt.axvline(np.average(final_dis),label = 'mean'
'{:.3f}'.format(np.average(final_dis)), linestyle = '-.',color = 'r')
plt.axvline(np.median(final_dis),label = "median{:.3f}".format(np.median(final_dis)), linestyle = ':',color = 'g')
plt.legend()
plt.show()
if __name__ == '__main__':
times = 1000000 # 实验次数
price_start = np.log(df["close"][0]) # 起始股价
daies = 100 # 预测天数
mean = dis_fun(df)[0]
var = dis_fun(df)[1]
price(times, price_start, daies, mean, var)
笔者以A股某公司为标的,选定数据区间为过去十年,预测区间为200交易日,蒙特卡洛模拟一百万次200交易日区间的布朗运动,偷个懒不写多线程了,让笔者的老爷机多跑一跑,笔者一觉醒来运行得:
图七:该证券单日涨跌幅分布
如图七所示,好一颗根正苗红的正态分布
图八:蒙特卡洛模拟结果——时间序列
图九:蒙特卡洛模拟结果——截面数据(T日)
5. 浅谈现实意义
5.1 布朗运动的现实意义
对于这点仁者见仁智者见智。笔者认为股价在超短期内存在随机游走的倾向,但通过布朗运动模拟出的股价几乎没有任何参考意义,也很难投入到实务中帮助我们预测。
5.2 蒙特卡洛模拟的现实意义
比起布朗运动,笔者更想谈谈这个。蒙特卡洛模拟是个非常不错的研究方法论,它并不是一种模型或是结果,而是一种研究方法或者说框架。在这个框架内可以有很大的想象空间,我们所构建的模拟方式是基于布朗运动,谁知道哪天会不会有更好的解决方案嵌入这个框架中?
其次,蒙特卡洛模拟对参数的输入敏感,笔者从风险管理的角度认为它是个很好的压力测试工具,尤其对于银行业,保险,或者债券还款压力。只要在最开始设置好不同的压力条件,观察造成的结果可以很直观的通过最后的分布得到,并且很直接的量化出一个置信区间和显著度水平。对于我们广大投资者而言,完全可以使用自己所构建的模拟方案为自己的投资组合做压力测试,在市场走熊时投资组合预期表现将会如何,成分证券财报不及预期,超预期时影响又是什么。其关键在于模型的设计,因为不管什么模拟方案结果都是输入敏感的,错误的输入信息将得到完全错误的结论。
6. 写在后面
本期的文章笔者将名字命名为预测模型,之前笔者写过一期估值模型的文章,并不是笔者搞错名字了,这是两个不同的系列,在笔者看来估值更多是基于基本面分析,而预测更多在于研究量价时空的数理关系上。
笔者其实是基本面分析的支持者,但当涉猎其它不同研究方法。后面笔者还会时不时用到蒙特卡洛模拟作为分析方法,本期算是个铺垫吧,更多内容敬请期待。