【蒙特卡洛模拟】如何使用蒙特卡洛预测股价

一、蒙特卡洛方法概述

1.基本思想

蒙特卡洛模拟法通过选择或建立适当的随机模型模拟风险因子的未来变化路径,并利用估值公示计算出对应路径的资产组合价值。然后反复重复上述模拟过程,可以最大限度获得风险因子的未来变化路径及其对应的资产组合价值在未来的可能取值,以更加准确的描述出资产组合的未来损益分布。

蒙特卡罗方法的基本思想即在大数定理的保证下,利用事件发生的“频率”作为事件发生的“概率”的近似值。只要设计一个随机试验,使一个事件的概率与某未知数有关,然后通过重复试验,以频率近似值表示概率,即可求得该未知数的近似值。在此方法中利用随机试验求近似解,需要相当多的试验次数,且次数越多近似效果也越好。

2.主要步骤

蒙特卡罗方法主要有三个步骤:

  • 1.描述或构造概率过程:对于不具有随机性质的确定性问题,需要人为地构造一个概率过程。
  • 2.利用概率分布抽样:通过计算机产生已知概率分布的随机变量,如均匀分布,正态分布、指数分布、泊松分布等。
  • 3.建立各种估计量:构造了随机概率模型并从中抽样后,就要确定一个随机变量,作为所要求问题的解。一般是把次随机抽样结果的算术平均值作为解的近似值。

3.数据选取

本项目选取上证500指数和个股的数据进行Monte Carlo模拟研究拟合效果。考虑到银行业股价具有较强的可预测性,因此选取平安银行(股票代码:SZ000001)进行个股股价预测。

数据来源:从wind和tushare数据库拉取了2018年1月2日至2024年4月12日间每个交易日的上证指数和平安银行股价数据。

二、模型搭建

1.基本假设

  • 1.股价服从几何布朗运动。

几何布朗运动:布朗运动的一种变体,它描述了一个资产价格的随机运动,其中价格的对数服从布朗运动。这个模型假设资产价格的波动率是恒定的,并且价格的变动是连续的。
假设股价$S(t)$的变化服从以下形式的随机微分方程:

dS(t) = \mu S(t) dt + \sigma S(t) \xi

其中:\mu是股价的预期收益率(即股价的无风险增长率),\sigma是股价的波动率,\xi是布朗运动的微分项,表示时间 tt+dt之间的随机变化。

  • 2.资产收益率服从正态分布。
  • 3.不同资产价格变化之间是独立的,或者具有某种已知的相关性。

2.参数估计

利用历史数据估计几何布朗运动模型中的参数,代码如下:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

data = pro.daily(ts_code='000001.SZ', start_date='20180101', end_date='20240411')
#这里计算的是一年的数据
historical_prices = data.loc['2023-01-01':'2023-12-29','close']
test = data.loc['2024-01-01':'2024-04-30','close']
train = data.loc[:,'close']

# 计算对数收益率
log_returns = np.log(np.array(historical_prices[1:]) / np.array(historical_prices[:-1]))

# 估计漂移率和波动率
drift = np.mean(log_returns)
volatility = np.std(log_returns)

print("漂移率 (drift):", drift)
print("波动率 (volatility):", volatility)

得到计算结果如下所示:

5年预期收益率 (\mu): -0.0002368310 
5年波动率 (\sigma):0.0206163004 
1年预期收益率 (\mu):-0.0015885768 
1年波动率 (\sigma): 0.0133056153

3.开始模拟

首先考虑到,模型训练数据较为久远的话,对于整个市场变动情况可以有较为充分的反映,因此选用2018年1月1日至2023年12月29日的股票价格数据来预测2024年1月1日至2024年4月11日的股票价格,绘制股价预测值和股价预测区间。

具体做法如下:进行N次模拟,预测区间即这N条股价路径所覆盖的区域,而股价预测值则为这N条股价路径的均值。

预测区间先采用5条股价路径进行预测。对于股价预测值,分别进行一次模拟和五次模拟的方法,分别绘制预测股价曲线与实际股价曲线。其中,灰色区域为五次模拟产生的五条股价路径所覆盖的区域,橙色趋势线为预测值,蓝色趋势线为股价实际走势。

代码实现如下:

def brownian_motion(start_price, drift, volatility, dt, steps):
    prices = [start_price]
    for i in range(steps):
        shock = np.random.normal(drift * dt, volatility * np.sqrt(dt))
        price = prices[-1] * np.exp(shock)
        prices+=[price]
    return prices

# 设置模拟参数
test = data.loc['2024-01-01':'2024-04-15','close']
start_price = historical_prices[-1]  # 使用最后一个历史价格作为起始价格
dt = 1  # 时间间隔,假设为1天
steps = len(test)  # 模拟步数
simulations = 5 # 模拟次数,假设为5次

# 进行蒙特卡洛模拟
simulated_prices = []
for _ in range(simulations):
    prices = brownian_motion(start_price, drift, volatility,dt, steps)
    simulated_prices.append(prices)
sp=pd.DataFrame(simulated_prices).T
sp.index=test.index
test=pd.merge(test,sp,left_index=True,right_index=True)

#置信区间
test['lower']=test[[0,1,2,3,4]].min(axis=1)
test['higher']=test[[0,1,2,3,4]].max(axis=1)
test['simulation']=test[[0,1,2,3,4]].mean(axis=1)

# 绘制模拟结果
plt.plot(test.index, test['close'], label='Actual Price', marker='o')
plt.plot(test.index, test['simulation'], label='Predicted Price', marker='o')
plt.fill_between(test.index, test['lower'], 
                 test['higher'], color='gray', alpha=0.2, label='Predicted Stock Price Interval')
plt.xlabel('Days')
plt.ylabel('Stock Price')
plt.title('Monte Carlo Simulation of Stock Price (Using Brownian Process)')
plt.xticks(rotation=45)
plt.legend()
plt.show()

一次模拟的预测结果如下:

五次模拟的预测结果如下:

可以看出,与单次模拟相比,采用多次模拟取平均值的预测结果与实际结果更加贴合。单次蒙特卡洛模拟的预测结果显示出较强的随机性,而采用五次模拟取平均值的方法能够在一定程度上抵消随机性,提高了预测的准确性和稳定性。因此在接下来的模拟研究中,我们均采用多次模拟取平均值的方式,同时展现范围区间,以获得更准确更全面的结果。 

但是注意到,模拟在二月份时出现了明显偏移,如何解释这种偏移呢?为此提出如下三个假设:

  • 1.历史数据的期限影响模拟效果
  • 2.模拟次数影响模拟效果
  • 3.股价波动还由别的因素(强有力地)驱动

4.分析过程和解释

  • 1.历史数据的期限不是影响模拟效果的主要因素。

假设预测准确度可能与所选历史数据时期长短相关——使用过长时期的历史数据进行模拟可能会导致过时信息的权重过大,而使用过短的历史数据时期可能导致信息不足,难以全面捕捉到股价变动的特征,影响预测的准确性和稳定性。
为了验证上述猜想,选取2018年1月2日至2023年12月29日的五年数据作为长期数据,2023年1月3日至2023年12月29日的一年数据作为短期数据,采用五次模拟取平均值的形式对2024年1月2日至2024年4月11日的股价数据进行预测。

预测结果如下:

这是选取长期数据进行模拟时的结果:

模拟路径数量:5条

历史区间:5年

模拟时间: 2024-01-01至2024-04-15

这是选取短期数据进行模拟时的结果:

 

模拟路径数量:5条

历史区间:1年

模拟时间: 2024-01-01至2024-04-15

从模拟结果图中可以看出,在选取长期数据和短期数据的预测中,预测股价与实际股价均在2月份出现了明显的偏离。历史期限较长时,预测股价区间更可能覆盖股价真值,但影响效果不大,因此历史数据的期限长短不是影响模拟效果的主要因素。

在进一步的研究中,本文采用5年作为历史期间。但我们仍需要考虑加入更多的影响因素,并对模型进行进一步改进,以提高其对突发事件的适应能力。

  • 2.模拟次数越多,逼近效果越好

假设预测准确度与模拟次数相关。一定范围内模拟多次可以减弱随机性,提高预测对突发事件的适应能力。
为了验证上述猜想,选取2018年1月2日至2023年12月29日的数据,分别采用五次模拟取平均值和十次模拟取平均值的方法,对2024年1月2日至2024年4月11日的股价数据进行预测。

预测结果如下:

这是五条模拟路径下的结果:

模拟路径数量:5条

历史区间:5年

模拟时间: 2024-01-01至2024-04-15

 

这是十条模拟路径下的结果: 

模拟路径数量:10条

历史区间:5年

模拟时间: 2024-01-01至2024-04-15

 从模拟结果中可以观察到,与进行五次模拟取平均值相比,进行十次模拟时,股价预测区间基本可以覆盖股价真值。这表明适当增加模拟次数可以提高模型的稳定性,减少预测结果的随机性。经过实验,再增加预测股价路径对预测准确性提升程度不高,在综合权衡成本效益之后,本文之后均采用10次模拟法。

  • 3.股价的波动由事件因素和随机游走共同驱动

实际上,仔细观察产生偏离的时间,容易想到2024年2月份发生了小微盘股流动性危机、国家队出手救市等事件。这样的非市场力量极有可能是模拟预测在2月与股价真值产生较大偏离背后的本质原因。

为了观察在正常市场条件下模型的预测效果是否会有所改善,调整预测期与历史数据时期,选取2018年1月2日至2023年10月30日的股价数据作为长期,2023年1月1日至2023年10月30日的股价数据作为短期,分别对2023年11月1日至2023年11月30日的股票数据进行预测,并在新的预测期内重新进行模拟和分析。

选择2023年11月1日-2023年11月30日的作为模拟预测的区间的原因在于,2024年开年后股市遇到了很多重大事件,而2023年12月作为本年最后一月,也有部分非市场调控因素存在,因此选择较为空白的11月作为预测时间段。

预测结果简直好的惊人,有些天的股价预测值竟然几乎重合!

显然,突发事件对股价的影响不容忽视。股票价格的形成既受到事件驱动因素的影响,也受到随机游走因素的影响。非市场力量(诸如事件驱动因素——公司新闻、市场预期和宏观经济变动等)能够迅速改变市场情绪和投资者的行为决策。而随机游走因素则反映了市场的随机性和不确定性,导致股票价格在短期内可能表现出难以预测的波动。

因此,理解和分析这两种影响因素对股票价格的形成至关重要,有助于我们更好地应对市场风险和机会。在股价预测和分析中,需要综合考虑这两类因素,并采用合适的模型和方法来准确捕捉股价变动的规律。 

三、模型改进与拓展

1.引入Markov过程

(1)模型介绍

蒙特卡洛模拟作为一种静态方法,在小区间内变化处理会产生相应偏差,并且分割数过多会大大的加剧偏差。因此,我们尝试引入Markov过程的蒙特卡洛模拟(简称MCMC模型)实现动态模拟的目的,即抽样分布能够随着模拟的进行而不断改变。

我们通过Metropolis-Hastings算法构造了一个马尔可夫链以近似复杂概率分布的方法,设定转移矩阵为[[0.2,0.5,0,3],[0.5,0.2,0.3],[0.5,0.3,0.2]]。

为对模型拟合效果进行比较,我们设定MSE作为标准衡量拟合效果,即计算模型的预测值 Ŷ 与真实值 Y 的接近程度,MSE越小说明拟合效果越好。

\text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2

(2)拟合结果

使用不同的转移矩阵进行调整,代码如下:

df=pd.read_csv("stock.csv")
df=df.rename(columns={'trade_date':'date','close':'close'})
df['date']=pd.to_datetime(df['date'])
df=df.set_index('date')   #以'date'列的值作为索引
historical_prices = df.loc['2023/1/3':'2024/2/29','close']
test = df.loc['2024/3/1':'2024/4/11','close']

log_returns = np.log(np.array(historical_prices[1:]) / np.array(historical_prices[:-1]))

drift = np.mean(log_returns)
volatility = np.std(log_returns)
print("漂移率 (drift):", drift)
print("波动率 (volatility):", volatility)

def brownian_motion(start_price, drift, volatility, dt, steps):
    prices = [start_price]
    for i in range(steps):
        shock = np.random.normal(drift * dt, volatility * np.sqrt(dt))
        price = prices[-1] * np.exp(shock)
        prices.append(price)
    return prices

def markov_transition(current_state, transition_matrix):
    # 根据当前状态和转移矩阵,确定下一个状态
    return np.random.choice(len(transition_matrix), p=transition_matrix[current_state])

def generate_transition_matrix():
    return np.array([[0.2, 0.5, 0.3],
                     [0.5, 0.2, 0.3],
                     [0.5, 0.3, 0.2]])

# 设置模拟参数
start_price = historical_prices[-1]  # 使用最后一个历史价格作为起始价格
dt = 1  # 时间间隔,假设为1天
steps = 27  # 模拟步数,即天数,step=num-1
simulations = 100 # 模拟次数,假设为100次

# 定义马尔可夫转移矩阵
transition_matrix = generate_transition_matrix()

simulated_prices = []
for _ in range(simulations):
    current_state = 0  # 初始状态为上涨
    prices = [start_price]
    for _ in range(steps):
        # 根据当前状态选择下一个状态
        current_state = markov_transition(current_state, transition_matrix)
        # 根据新状态生成价格
        shock = np.random.normal(drift * dt, volatility * np.sqrt(dt))
        price = prices[-1] * np.exp(shock)
        prices.append(price)
    simulated_prices.append(prices)

# 将模拟结果转换为DataFrame
sp = pd.DataFrame(simulated_prices).T
sp.index = test.index
test = pd.merge(test, sp, left_index=True, right_index=True)

test['lower']=test[list(range(simulations))].min(axis=1)
test['higher']=test[list(range(simulations))].max(axis=1)
test['simulation']=test[list(range(simulations))].mean(axis=1)

# 绘制模拟结果
plt.plot(test.index, test['close'], label='Actual Price', marker='o')
plt.plot(test.index, test['simulation'], label='Predicted Price', marker='o')
plt.fill_between(test.index, test['lower'], test['higher'], color='gray', alpha=0.2, label='Predicted Stock Price Interval')
plt.xlabel('Days')
plt.ylabel('Stock Price')
plt.title('Monte Carlo Simulation of Stock Price (Using Brownian Process)')
plt.xticks(rotation=45)
plt.legend()
plt.show()

# 预测结果
history=np.array(test['close'])
predict=np.array(test['simulation'])
mse=np.mean((predict - history) ** 2)
print(mse)

我们得到以下MCMC模拟结果。

MCMC对该股预测MSE为0.0168,而使用蒙特卡洛法模拟的MSE=0.0167,可见MCMC没有显著提升预测效率。

究其原因,我认为可能是有两个因素影响。第一,MCMC对于参数选择的要求较高,而我们选择的参数与现实情况有所差距,需要进一步精细调整。第二,MCMC收敛速度较慢,马尔可夫链可能并未达到充分收敛的状态。因此我们考虑采取其他的方式对蒙特卡洛模拟进行改进。

2.引入GARCH模型

(1)模型介绍

GARCH模型用于描述时间序列数据中的波动性(方差)变化,假设t时刻的方差与过去若干期的方差与残差相关,是条件异方差的一种形式。采用简单的GARCH(1,1)模型来预测股票价格的方差。
模型的表达式如下:

\sigma_t^2 = \omega + \alpha \epsilon_{t-1}^2 + \beta \sigma_{t-1}^2

其中:\sigma_t^2是时间t时刻的方差;\sigma_{t-1}^2t-1时刻的残差(误差项)的平方;\omega, \alpha, \beta是模型的参数,分别表示常数项、ARCH 效应和 GARCH 效应。

将garch模型预测得到的方差与蒙特卡洛模拟得到的随机数相乘,来修正模型的波动性,即:

\xi' = \xi * \sigma_t

dS(t) = \mu S(t) dt + \sigma S(t) \xi'

(2)拟合结果

为探究预测方差项的加入能否提高正常市场条件下和突发事件下模型的预测效果,使用2018年1月2日-2023年12月29日的历史数据分别对2024年1月1日-2024年1月31日的数据与2024年1月1日-2024年4月11日的数据进行预测。

代码如下:

import arch

data = pd.read_csv('000001SZ.csv')  

# 用NaN替换数据中的无穷值
data.replace([float('inf'), float('-inf')], pd.NA, inplace=True)
print(data.head())
print(data['trade_date'][0])
data.set_index('trade_date', inplace=True)
historical_prices = data.loc['2024-03-01':'2024-04-11','close']
log_returns = np.log(np.array(historical_prices[1:]) / np.array(historical_prices[:-1]))

drift = np.mean(log_returns)
volatility = np.std(log_returns)
model = arch.arch_model(historical_prices, vol='GARCH', p=1, q=1)
results = model.fit(disp='off')
print(results.summary())
forecasts = results.forecast(horizon=21)
print(forecasts.variance)

import math
def brownian_motion(start_price, drift, volatility, dt, steps):
    prices = [start_price]
    for i in range(steps):
        shock = np.random.normal(drift * dt, volatility * np.sqrt(dt)) * math.sqrt(forecasts.variance.loc['2024-04-11'].iloc[0])
        price = prices[-1] * np.exp(shock)
        prices+=[price]
    return prices

start_price = historical_prices[-1]  # 使用最后一个历史价格作为起始价格
dt = 1  # 时间间隔,假设为1天
steps = 21
simulations = 10 # 模拟次数,假设为10次

simulated_prices = []
for _ in range(simulations):
    prices = brownian_motion(start_price, drift, volatility,dt, steps)
    simulated_prices.append(prices)

sp=pd.DataFrame(simulated_prices).T

sp['lower']=sp[[0,1,2,3,4,5,6,7,8,9]].min(axis=1)
sp['higher']=sp[[0,1,2,3,4,5,6,7,8,9]].max(axis=1)
sp['simulation']=sp[[0,1,2,3,4,5,6,7,8,9]].mean(axis=1)
sp.index=['2024-04-12','2024-04-15','2024-04-16','2024-04-17','2024-04-18','2024-04-19','2024-04-22','2024-04-23','2024-04-24','2024-04-25','2024-04-26','2024-04-29','2024-04-30','2024-05-06','2024-05-07','2024-05-08','2024-05-09','2024-05-10','2024-05-13','2024-05-14','2024-05-15','2024-05-16']
plt.plot(sp.index, sp['simulation'], label='Predicted Price', marker='o')
plt.fill_between(sp.index, sp['lower'],
                 sp['higher'], color='gray', alpha=0.2, label='Predicted Stock Price Interval')
plt.xlabel('Days')
plt.ylabel('Stock Price')
plt.title('Monte Carlo Simulation of Stock Price (Using Brownian Process)')
plt.xticks(rotation=45)
plt.xticks(sp.index[::2])
plt.legend()
plt.show()

 预测结果如下:

这是非市场力量较少的情况:

这是存在较强非市场力量介入时的情况:

预测结果显示,在正常市场条件下,加入预测方差项后的模型表现良好,预测准确度得到了有效提高。同时,经过修正的模型也能够在一定程度上减弱突发事件对预测结果的影响,然而突发事件下预测股价与实际股价仍然存在明显的偏离。

这表明尽管对模型进行了改进和修正,但采用蒙特卡洛方法在股价预测在面临突发事件等不确定因素时仍然有一定的挑战。这也是合乎蒙特卡洛模型背后逻辑的——金融市场资产价格随机游走,因此蒙特卡洛在(或者说只在)预测纯市场力量时有较强优势。

尽管该方法(及其改进方法)在市场存在其他力量介入时表现欠佳,但其对于“纯市场力量造成的波动”的预测性已经高的惊人!这已经充分体现了该方法的有效性,和对于金融市场的深刻洞察力。

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值