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

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

【QLS-28】市场冲击模型

耶伦发表于:5 月 10 日 17:03回复(1)

在学习量化交易的过程中,笔者接触到Qua*pian上的一系列深入浅出的学习教程。搜索后发现,已经有前辈们将其中的一些经典课程翻译并分享到聚宽社区。可惜或许是工程量太大,大多数课程仍然静静地留在Qua*pian上。

笔者遂产生一个想法:将剩下的讲座(还有超过50%吧orz)边学习边解读,并将我的翻译和学习心得记录在聚宽社区,以供感兴趣的朋友一起学习讨论。原文链接附在正文底部,欢迎交流~

市场冲击模型¶

在股票交易中,对市场冲击的影响进行建模必不可少,但它又常常容易被人们忽视。不管你喜欢什么样的下单模型,只有通过实盘操作才有可能赚得货真价实的利润(模拟盘表现再好,真金白银也不会涌入你的钱包)。

而实盘操作中,股票订单通常无法立即成交,实际成交价取决于诸多因素。规模越大的订单往往成交时间越长,而波动率较大的市场也可能会使实际成交价与预期价格差之千里。

接下来我们通过实证分析来展示市场冲击对股票交易的影响。读完本文后,你就能:

1.从已有的市场冲击模型的研究和自身实际经验出发,思考影响交易成本的因素

2.了解股票换手率、交易成本和投资杠杆率对策略表现的影响

3.获悉机构投资者如何估算交易成本

import numpy as npimport matplotlib.pyplot as pltimport pandas as pdimport time

交易成本的介绍¶

交易成本分为两类:

  • 直接交易成本(佣金和税费):易衡量,易测试,属于交易成本中比重较小且相对固定的部分

  • 间接交易成本(执行成本和机会成本):执行成本可进一步分为市场冲击成本和市场时机成本

市场冲击成本反映了买卖价差和补偿买者(或卖者)与知情交易者交易风险作出的价格让步之和。市场时机成本其实也是一种机会成本,关于股票交易中的机会成本我们下次有机会再讨论。今天我们重点聊一聊市场冲击成本。

建立衡量市场冲击影响的模型前,我们首先要介绍一个概念:滑点。熟悉聚宽回测引擎的朋友应该对它并不陌生,set_slippage函数可以为你的回测/模拟设置滑点(详细用法参见聚宽API文档中的策略引擎介绍)。滑点,一般指真实的成交价位与预设的成交价位出现偏移,这种偏移一般对交易者不利,导致交易执行成本增加。研究表明,交易中对滑点影响最大的四个因素为:

  • 股票波动率

  • 股票流动性

  • 股票订单大小

  • 股票买卖价差

后文中,我们会对这四个因素逐一建模分析。

交易成本对中频统计套利策略的影响¶

中频策略是指换手率在0.05~0.67之间的策略——这意味着着策略的持有期介于一天和一周之间。统计套利是定量技术在算法交易上的应用,它通过统计技术来分析历史数据,从而来识别交易的机会。统计套利起源于20世纪80年代,由摩根士丹利率先使用,几十年来它在金融市场上得到了广泛的应用。统计套利策略类型很多,我们熟悉的市场中性套利、跨市场套利、ETF套利等都属于其范畴。

那么,交易成本的变动会如何影响统计套利策略呢?让我们来考察下面这个中频统计套利组合策略,该策略的算法特征为:

算法特征数量
持有期(周)1
投资杠杆率2
总金额 (万美元)100
年交易日252
日交易比率0.4

这个策略的持有期(holding period)为1周,这意味着我们每年进行约50次的股票买卖。按2倍的投资杠杆率计算,1亿美元的投资金额能产生高达200亿美元的年交易量。

问:试问在上述情形的换手率下,执行成本每提升1个基点的提升会对该策略的收益产生多大影响?

执行成本每个基点(0.01%)的增长就会使策略损失其2.016%的收益,计算过程代码如下:

def perf_impact(leverage, turnover , trading_days, txn_cost_bps):p = leverage *  turnover * trading_days * txn_cost_bps/10000.return p
print (perf_impact(leverage=2, turnover=0.4, trading_days=252, txn_cost_bps=1))
0.02016

量化投资机构团队是如何评估交易成本的?¶

量化投资机构团队在完成大额订单时,试图尽可能减小执行成本。为了实现这一目标,大额委托单通常被拆分成几个子单,这些子单在不同时间点分批执行,目的是攫取这段时间里市场所有可用的流动性从而最小化交易成本。由此,母单执行价格可以表示为以所有子单的交易量为权重的加权平均价格。就其本质而言,属于最优投资问题。以上的过程,通常使用算法交易。

算法交易,也被称为自动交易(Automated Trading)、黑盒交易(Black-box Trading)、无人值守交易(Robot Trading),是使用计算机来确定订单最佳的执行路径、执行时间、执行价格以及执行数量一种交易方法。算法交易广泛应用于对冲基金、企业年金、共同基金以及其他一些大型的机构投资者,他们使用算法交易对大额订单进行分拆,寻找最佳的路由和最有利的执行价格,以降低市场的冲击成本、提高执行效率和订单执行的隐蔽性。

算法交易中,测量交易成本时分为以下三个步骤 :

  • 计算交易佣金

  • 确定公平市场基准点

  • 将交易对成本的影响从其它因素的影响中分离出来

这三个问题中,公平市场基准点的确定是交易成本测量的关键。我们主要介绍公平市场基准点的选取。

问:算法交易中,机构投资者是如何选择公平市场基准点的?

公平市场基准点有以下取法:

  • AP(Arrival Price):到达价格,定义为算法下达母单时的中间报价(中间是最佳买入价和卖出价的平均数)

  • Interval VWAP(volume-weighted *erage price):算法启动时到算法结束期间的市场成交量加权均价

  • T + 10 min:回复基准,将最后一次成交单10分钟后的价格与执行价格作比较

  • T + 30 min:回复基准,将最后一次成交单30分钟后的价格与执行价格作比较

  • Close:回复基准,将收盘价与执行价格作比较

  • Open:回复基准,将开盘价与执行价格作比较

  • Previous close:动量基准,将前一个交易日的收盘价与执行价格作比较

选好基准后,用下面公式计算回复(动量)指标:

$$ Metric  = \frac{Side * (Benchmark - Execution\thinspace Price )* 100 * 100}{ Benchmark }$$

公式重点

  • 执行价格(EP):所有子单的加权平均价格,权重为交易量

  • 执行落差(IS):到达价格减执行价格((Benchmark - Execution Price),用基点表示。使用该基准是为了将实际执行价格与策略决策价格进行比较。 这种成本有时被称为“滑价”(slippage)或“执行落差”(implementation shortfall)

回复指标能反映订单执行后的临时性影响。一般来说,我们预计在订单完成后股价会有所回升,这是我们的股票订单在市场激起的小水花。动量指标则描述了订单执行前的股票价格漂移方向。通常,较大的动量交易可能会制约我们最小化买卖差价的效果。

确定下单最优时间¶

在执行订单时,主要需要权衡的是时间风险和市场冲击:

  • 时间风险:上一次订单和到达中间报价的时间间隔越长,价格漂移和信息走漏的风险越高

  • 市场冲击:上一次订单和到达中间报价的时间间隔越短,市场冲击效应越强

所以,交易成本固定部分基本不变时,投资者只需要在时间风险和市场冲击之间进行权衡,找出两者最佳结合点,确定最优时间————在这种情况下,两者都对执行成本的贡献相同。

下面代码所绘制的图形很好地诠释了权衡过程。

x = np.linspace(0,1,101)risk = np.cos(x*np.pi)impact = np.cos(x* np.pi+ np.pi)fig,ax = plt.subplots(1)# Make your plot, set your axes labelsax.plot(x,risk)ax.plot(x,impact)ax.set_ylabel('Transaction Cost in bps', fontsize=15)ax.set_xlabel('Order Interval', fontsize=15)ax.set_yticklabels([])ax.set_xticklabels([])ax.grid(False)ax.text(0.09, -0.6, 'Timing Risk', fontsize=15, fontname="serif")ax.text(0.08, 0.6, 'Market Impact', fontsize=15, fontname="serif")plt.title('Timing Risk vs Market Impact Affect on Transaction Cost', fontsize=15)plt.show()


上面这幅图描述的是影响交易成本的两方面因素:时间风险和市场冲击。坐标图中,横轴是时间间隔,纵轴是交易成本。蓝线是市场冲击成本,红线是时间风险成本。

显然,时间风险成本随时间间隔增大而递增,市场冲击成本则相反。通过权衡折衷,我们选取的最优下单时间在两条线的交点处。

股票流动性¶

股票流动性是指投资者以最小成本、最小价格影响和最快速度完成交易大宗股票的容易程度。当流动性严重不足时,往往会造成投资者的变现难度加大。对于一家缺乏流动性的上市公司而言,一旦出现大单折价抛售,则是股价的瞬间大跌,股价不足以抵御大单抛售压力。

一般来说,临近收盘时流动性最高,开盘时流动性其次,中午流动性最低。流动性也应该根据交易者的下单规模和同一行业类别的其他证券来相对评估。我们一般通过以下方式检测流动性:

  • 日内成交量曲线

  • 当天的交易量百分比

  • 一段时间内每日平均交易量的百分比

  • 日内累积交易量曲线

  • 订单相对大小

下面我们以Facebook为例,验证股票流动性受时间的影响情况。我们提取2016年1月1日到2016年7月1日的成交数据,并以2016年4月14日为例绘制日内成交量曲线。

tickers = symbols(['FB']) # Facebook tickernum_stocks = len(tickers)# %%timeit -n1 -r1 magic is not allowed in Qstart = time.time()data = get_pricing(tickers,   fields='volume',   frequency='minute',   start_date='2016-1-1',   end_date='2016-7-1')end = time.time()print ("Time: %0.2f seconds." % (end - start))data = data.tz_convert('US/Eastern') # Q data comes in as UTCdat = data[symbols('FB')]plt.subplot(211)dat['2016-04-14'].plot(title='Intraday Volume Profile') # intraday volume profile plot plt.subplot(212)(dat['2016-04-14'].resample('10t', closed='right').sum()/\     dat['2016-04-14'].sum()).plot(); # percent volume plotplt.title('Intraday Volume Profile, % Total Day');


上面是2016年4月14日Facebook的交易量曲线图和交易量比重图。

第一幅图刻画的是当日交易量随时间变化的走势图,第二幅图是计算每10分钟内的交易量占当日总交易量的百分比,再连成折线图。很清楚地,我们能看出开盘时和收盘时的交易活动最为活跃。

我们再来看一看从2016年1月1日到2016年7月1日期间Facebook的流动性情况。

从9点30分开盘时开始,统计每10分钟内的成交量并计算其占当天总成交量的比重。

df = pd.DataFrame(dat) # Facebook minutely volume datadf.columns = ['interval_vlm'] df_daysum = df.resample('d').sum() # take sum of each day df_daysum.columns = ['day_vlm']df_daysum['day'] = df_daysum.index.date # add date index as columndf['min_of_day']=(df.index.hour-9)*60 + (df.index.minute-30) # calculate minutes from opendf['time']=df.index.time # add time index as columnconversion = {'interval_vlm':'sum', 'min_of_day':'last', 'time':'last'}df = df.resample('10t', closed='right').apply(conversion) # apply conversions to columns at 10 min intervalsdf['day'] = df.index.datedf = df.merge(df_daysum, how='left', on='day') # merge df and df_daysum dataframesdf['interval_pct'] = df['interval_vlm'] / df['day_vlm'] # calculate percent of days volume for each rowdf.head()


以日内时间为横轴,10分钟内交易量占日内交易量比重为纵轴,绘制2016年1月1日到2016年7月1日的散点图。

plt.scatter(df.min_of_day, df.interval_pct)plt.xlim(0,400)plt.xlabel('Time from the Open (minutes)')plt.ylabel('Percent Days Volume')


上面这张图其实是一张散点图,横轴是(每日)开盘后经过的时间,纵轴是十分钟交易量占当日总交易量的百分比。

为什么这张散点图看起来是一根根柱子呢?因为这是2016年1月1日到2016年7月1日半年数据的汇总,每一条纵线体现的是半年里这个时刻的交易活跃度信息。我们可以看到,交易量占比分布呈现“微笑形状”,说明开盘时和收盘时流动性最高,中间时段流动性最低。

接下来,我们以日内时间为横坐标,交易量占日内比为纵轴,绘制实时交易量条形图和累积交易量曲线。从曲线斜率我们可以验证,Facebook的流动性确实是开盘后和收盘前最大。

grouped = df.groupby(df.min_of_day)grouped = df.groupby(df.time) # group by 10 minute interval timesm = grouped.median()  # get median values of groupbyx = m.indexy = m['interval_pct']fig, ax1 = plt.subplots();ax1.bar(x, 100*y, -60*10 ,alpha=0.75); # plot percent daily volume grouped by 10 minute interval timesax1.set_ylim(0,10);ax2 = ax1.twinx();ax2.plot(x,(100*y).cumsum()); # plot cummulative distribution of median daily volumeax2.set_ylim(0,100);plt.title('Intraday Volume Profile');ax1.set_ylabel('% of Day\'s Volume in Bucket');ax2.set_ylabel('Cumulative % of Day\'s Volume');


横轴是时间,纵轴是交易量占日内总交易量的比例,柱形图是十分钟交易量占日内总交易量的半年平均值分布(主坐标轴),曲线则是它的累积率(次坐标轴)。

从柱形图看,开盘和收盘时的柱子最高,中间的柱子最低。曲线图中,曲线斜率先减小后增大,说明开盘和收盘时确实交易最为频繁。

股票订单大小¶

当我们增大股票订单规模时,订单的完成时间也会延长。

假设我们运用VWAP策略下单。VWAP是一种调度策略,根据整个时间窗口内的交易量分布的预测,在预先指定的时间窗口执行订单。VWAP策略的主要目标是减小跟踪误差。

VWAP在订单数量较小时可以相对精确地跟踪市场均价,但是大规模交易会对预设价格产生较大冲击,使得跟踪误差的不确定性增大。因此对于大规模订单,VWAP跟踪市场均价的能力会大幅减弱。

当股票订单规模非常大时,我们转而选择流动性管理执行策略,以确保订单能在当日收盘前执行完毕。流动性管理执行策略对订单执行的紧迫性和执行场合的选择具有特定的约束。

现在让我们再重新考量风险曲线:交易时间越长,预计交易成本就会越高。因此,日均成交量(ADV)越高,股票的交易成本越高。

下面的代码描述了在订单相对大小和参与率不同的条件下,交易完成时间的情况。由图可见,订单越大,参与率越高,完成时间越短。

dat = get_pricing(symbols(['FB']), fields='volume', frequency='minute', start_date='2016-1-1', end_date='2018-1-2')dat = dat.tz_convert('US/Eastern') # Q data comes in as UTCdef relative_order_size(participation_rate, pct_ADV):fill_start = dat['2017-10-02'].index[0] # start order at 9:31ADV20 = int(dat.resample("1d").sum()[-20:].mean()) # calculate 20 day ADVorder_size = int(pct_ADV * ADV20)#print 'order size:', order_size, 'daily volume:', dat['2016-07-01'].sum()/(1.0*10**6), 'M shares'try :ftime = dat['2017-10-02'][(order_size * 1.0 / participation_rate)<=dat['2017-10-02'].cumsum().values].index[0]except: ftime = dat['2017-10-02'].index[-1] # set fill time to 4p fill_time = max(1,int((ftime - fill_start).total_seconds()/60.0))#print 'order fill time' ,fill_time,  'minutes'return fill_timedef create_plots(participation_rate, ax):df_pr = pd.DataFrame(data=np.linspace(0.0,0.1,100), columns = ['adv'] ) # create dataframe with intervals of ADVdf_pr['pr'] = participation_rate # add participation rate columndf_pr['fill_time'] = df_pr.apply(lambda row: relative_order_size(row['pr'],row['adv']), axis = 1) # get fill timeax.plot(df_pr['adv'],df_pr['fill_time'], label=participation_rate) # generate plot line with ADV and fill timefig, ax = plt.subplots() for i in [0.01,0.02,0.03,0.04,0.05,0.06,0.07]: # for participation rate valuescreate_plots(i,ax) # generate plot lineplt.ylabel('Time from Open (minutes)')plt.xlabel('Percent *erage Daily Volume')plt.title('Trade Completion Time as Function of Relative Order Size and Participation Rate')plt.xlim(0.,0.04)ax.legend()


上面的图里,横轴是订单的相对大小(占日总交易量的比例),纵轴是完成订单所花的时间,曲线的七种不同颜色代表着不同的策略参与率。我们发现,订单越大,成交时间越长;参与率越低,成交时间越长。可能你会好奇,为什么不管哪条曲线,最后都呈现“涨停板”一样的状态呢?很简单,那是因为时间已经到收盘了,订单必须在收盘前完成。

股票波动率¶

波动率是衡量股票收益离散程度的统计指标,用收益率的标准差来计算。 股票的波动率通常在开盘时达到峰值,此后一直下降到中午。波动率越高,收益的不确定性越大,这种不确定性是交易日开始时的价格发现过程中较大买卖价差的表现。

下面的演示中我们使用两种方法来计算波动率:OHLC法和最常见的近似逼近法。

OHLC法需要用到开盘价O,最高价H,最低价L和收盘价C。

  • OHLC:OHLC是Yang-Zhang在2000年提出的波动率计算方法,这是处理开盘跳空问题最有力的方法,它很好地使用了“隔夜波动率”数据。

$$\sigma^2 = \frac{Z}{n} \sum \left[\left(\ln \frac{O_i}{C_{i-1}} \right)^2  +  \frac{1}{2} \left( \ln \frac{H_i}{L_i} \right)^2 - (2 \ln 2 -1) \left( \ln \frac{C_i}{O_i} \right)^2 \right]$$

近似逼近法中,取对数收益的年化标准差作为波动率,其中d是普通股息(非调整后股息),c是股票收盘价,x是对数收益,sigma是x的标准差。

$$ Log \thinspace return = x_1 = \ln (\frac{c_i + d_i}{c_i-1} ) $$$$ Volatilty =  \sigma_x \sqrt{ \frac{1}{N} \sum_{i=1}^{N} (x_i - \bar{x})^2 }$$

以下是根据上面两个公式编写的计算代码,同样以Facebook为例,进行数据处理和计算,得到其波动率(每分钟):

tickers = symbols(['FB'])start = time.time()data = get_pricing(tickers, frequency='minute', start_date='2016-1-1', end_date='2016-7-1')end = time.time()print ("Time: %0.2f seconds." % (end - start))data.itemsdata.describedata['price']df = data.to_frame().unstack()df.columns = df.columns.droplevel(1) # drop the tickerdf.index.name = None df = df.tz_convert('US/Eastern') # Q data comes in as UTC, convert to ESTdf.head()


def gkyz_var(open, high, low, close, close_tm1): # Garman Klass Yang Zhang extension OHLC volatility estimatereturn np.log(open/close_tm1)**2 + 0.5*(np.log(high/low)**2) \- (2*np.log(2)-1)*(np.log(close/open)**2)def historical_vol(close_ret, mean_ret): # close to close volatility estimatereturn np.sqrt(np.sum((close_ret-mean_ret)**2)/390)
df['min_of_day'] = (df.index.hour-9)*60 + (df.index.minute-30) # calculate minute from the opendf['time'] = df.index.time # add column time indexdf['day'] = df.index.date # add column date indexdf.head()


df['close_tm1'] = df.groupby('day')['close_price'].shift(1)  # shift close value down one rowdf.close_tm1 = df.close_tm1.fillna(df.open_price)df['min_close_ret'] = np.log( df['close_price'] /df['close_tm1']) # log of close to closeclose_returns = df.groupby('day')['min_close_ret'].mean() # daily mean of log of close to closenew_df = df.merge(pd.DataFrame(close_returns), left_on ='day', right_index = True)# handle when index goes from 16:00 to 9:31:new_df['variance'] = new_df.apply(lambda row: historical_vol(row.min_close_ret_x, row.min_close_ret_y),axis=1)#df['variance'] = df.apply(#    lambda row: gkyz_var(row.open_price, row.high, row.low,#                         row.close_price, row.close_tm1),#    axis=1)new_df.head()


下面的代码将分钟级的波动率进行累加,得到当日波动率。

df_daysum = pd.DataFrame(new_df['variance'].resample('d').sum()) # get sum of intraday variances dailydf_daysum.columns = ['day_variance']df_daysum['day'] = df_daysum.index.datedf_daysum.head()


类似之前统计每10分钟股票成交量,我们现在统计每10分钟股票波动率。

conversion = {'variance':'sum', 'min_of_day':'last', 'time':'last'}df = new_df.resample('10t', closed='right').apply(conversion)df['day'] = df.index.datedf['time'] = df.index.timedf.head()


用分钟级波动率除以当日波动率,得到区间波动率比重,可以更好地衡量一段时间内的波动率。

df = df.merge(df_daysum, how='left', on='day') # merge daily and intraday volatilty dataframesdf['interval_pct'] = df['variance'] / df['day_variance'] # calculate percent of days volatility for each rowdf.head()


下面的分布图和曲线与介绍流动性一节的原理相同,可参考前文。

plt.scatter(df.min_of_day, df.interval_pct)plt.xlim(0,400)plt.ylim(0,)plt.xlabel('Time from Open (minutes)')plt.ylabel('Interval Contribution of Daily Volatility')plt.title('Probabilty Distribution of Daily Volatility ')


grouped = df.groupby(df.min_of_day)grouped = df.groupby(df.time) # groupby timem = grouped.median() # get medianx = m.indexy = m['interval_pct']fig, ax1 = plt.subplots()ax1.bar(x, 100*y, 60*10 ,alpha=0.75);# plot interval percent of median daily volatility ax2 = ax1.twinx()ax2.plot(x, (100*y).cumsum()) # plot cummulative distribution of median daily volatilityax2.set_ylim(0,100);plt.title('Intraday Volatility Profile')ax1.set_ylabel('% of Day\'s Variance in Bucket');ax2.set_ylabel('Cumulative % of Day\'s Variance');#cut off graph at 4pm


买卖价差¶

在实时交易数据中可以看出,买卖差价和订单属性之间存在以下关系:

  • 随着市值上涨,差价预计会减少。大公司往往表现出较低的买卖报差价。

  • 随着波动率的增加,差价预计会增加。更大的价格不确定性会导致更大的买卖报价差。

  • 随着日均交易量的增加,差价预计会减小。由于参与者数量较多而且报价更新频繁,流动性往往与差价成反比。

  • 随着价格的上涨,差价预计会减小(类似于市值),不过这种关系并不那么强烈。

  • 随着时间的推移,差价预计会减小。在交易日的早期阶段,市场进行价格发现。相反,在市场收盘前,完成订单是大多数参与者的首要任务,交易活动由流动性管理而不是价格发现来引导。

下面是一个适用于实时数据的对数线性模型,用于预测以上情形的股票差价。

def model_spread(time, vol, mcap = 1.67*10**10, adv = 84.5, px = 91.0159):time_bins = np.array([0.0, 960.0, 2760.0, 5460.0, 21660.0]) #seconds from market opentime_coefs = pd.Series([0.0, -0.289, -0.487, -0.685, -0.952])vol_bins = np.array([0.0, .1, .15, .2, .3, .4])vol_coefs = pd.Series([0.0, 0.251, 0.426, 0.542, 0.642, 0.812])mcap_bins = np.array([0.0, 2.0, 5.0, 10.0, 25.0, 50.0]) * 10 ** 9mcap_coefs = pd.Series([0.291, 0.305, 0.0, -0.161, -0.287, -0.499])adv_bins = np.array([0.0, 50.0, 100.0, 150.0, 250.0, 500.0]) * 10 ** 6adv_coefs = pd.Series([0.303, 0.0, -0.054, -0.109, -0.242, -0.454])px_bins = np.array([0.0, 28.0, 45.0, 62.0, 82.0, 132.0])px_coefs = pd.Series([-0.077, -0.187, -0.272, -0.186, 0.0, 0.380])return np.exp(1.736 +\                  time_coefs[np.digitize(time, time_bins) - 1] +\                  vol_coefs[np.digitize(vol, vol_bins) - 1] +\                  mcap_coefs[np.digitize(mcap, mcap_bins) - 1] +\                  adv_coefs[np.digitize(adv, adv_bins) - 1] +\                  px_coefs[np.digitize(px, px_bins) - 1])

预测订单的价差:¶

  • 股票:DPS

  • 数量:425股

  • 时间:2017年7月19日上午9:41,距离开盘600秒

  • 市值:1.67e10

  • 波动率:18.8%

  • 日均成交量:92.9万股; 84.5百万美元

  • 平均价格:91.0159美元

计算可得,买卖报价差是10.014159084591371。

t = 10*60vlty = 0.188adv = 84.5*10mcap = 1.67*10**10price = 91.0159 spread=model_spread(t, vlty,mcap, adv, price)print(str(spread)+'bps')
10.014159084591371bps

下面,我们绘制以波动率和交易时间为变量的差价等高图。

x = np.linspace(0,390*60) # seconds from open shape (50,)y = np.linspace(.01,.7) # volatility shape(50,)mcap = 1.67 * 10 ** 10adv = 84.5px = 91.0159vlty_coefs = pd.Series([0.0, 0.251, 0.426, 0.542, 0.642, 0.812])vlty_bins = np.array([0.0, .1, .15, .2, .3, .4])time_bins = np.array([0.0, 960.0, 2760.0, 5460.0, 21660.0]) #seconds from market opentime_coefs = pd.Series([0.0, -0.289, -0.487, -0.685, -0.952])mcap_bins = np.array([0.0, 2.0, 5.0, 10.0, 25.0, 50.0]) * 10 ** 9mcap_coefs = pd.Series([0.291, 0.305, 0.0, -0.161, -0.287, -0.499])adv_bins = np.array([0.0, 50.0, 100.0, 150.0, 250.0, 500.0]) * 10 ** 6adv_coefs = pd.Series([0.303, 0.0, -0.054, -0.109, -0.242, -0.454])px_bins = np.array([0.0, 28.0, 45.0, 62.0, 82.0, 132.0])px_coefs = pd.Series([-0.077, -0.187, -0.272, -0.186, 0.0, 0.380])# shape (1, 50)time_contrib = np.take(time_coefs, np.digitize(x, time_bins) - 1).reshape((1, len(x)))# shape (50, 1)vlty_contrib = np.take(vlty_coefs, np.digitize(y, vlty_bins) - 1).reshape((len(y), 1))# scalarmcap_contrib = mcap_coefs[np.digitize((mcap,), mcap_bins)[0] - 1]# scalaradv_contrib = adv_coefs[np.digitize((adv,), adv_bins)[0] - 1]# scalarpx_contrib = px_coefs[np.digitize((px,), px_bins)[0] - 1]z_scalar_contrib = 1.736 + mcap_contrib + adv_contrib + px_contribZ = np.exp(z_scalar_contrib + time_contrib + vlty_contrib)cmap=plt.get_cmap('jet')X, Y = np.meshgrid(x,y)CS = plt.co*ur(X/60,Y,Z, linewidths=3, cmap=cmap, alpha=0.8);plt.clabel(CS)plt.xlabel('Time from the Open (Minutes)')plt.ylabel('Volatility')plt.title('Spreads for varying Volatility and Trading Times (mcap = 16.7B, px = 91, adv = 84.5M)')plt.show()


上面这幅等高图里,横轴是开盘后经过的时间,纵轴是股票波动率,同颜色线上的点具有相同的差价。可以看到,差价从左上往右下方向递减,即:波动率越高,差价越大;时间越长,差价越小。

量化市场冲击¶

市场冲击模型理论试图使用订单属性来估计订单的交易成本。以下是几个市场冲击模型假设:

1.Qua*pian成交量滑价模型

2.Kissell模型(2004)

3.Almgren模型(2005)

4.J.P.摩根模型(2010)

这些模型具有一些共性,比如都包含股票订单大小,股票波动率等因素。模型间也存在显著差异,例如:

  • J.P.摩根模型给出了交易冲击的精确显式表达式

  • Almgren考虑了每日交易的流通股比例

  • Qua*pian成交量滑价模型没有考虑股票波动率

  • Kissel给出了几个临时性和永久性冲击的具体参数

特别指出,学术模型中有着临时性冲击和永久性冲击的区分。临时性冲击是由于交易紧迫性或侵略性而对交易成本产生的冲击;永久性冲击则是通过交易信息或是交易中的短期Alpha值来估测的。

Almgren模型(2005)¶

该模型假设初始订单X在一段时间区间T内以统一的交易速率完成,也就是说,交易速率是v = X / T,且在交易完成前一直保持不变。 Almgren模型的表达式为永久冲击加上临时冲击:

$$\text{tcost} = 0.5 \overbrace{\gamma \sigma  \frac{X}{V}\left(\frac{\Theta}{V}\right)^{1/4}}^{\text{permanent}} + \overbrace{\eta \sigma \left| \frac{X}{VT} \right|^{3/5}}^{\text{temporary}} $$


其中$ \ gamma $ 和 $ \ eta $ 是市场冲击的普遍系数,作者使用大量机构交易样本进行估算而得; $ \ sigma $是股票的每日波动率; $ \ Theta $是该股票的已发行股票总数; $ X $是想要交易的股票数量; $ T $是对交易时间进行分割的交易时间宽度; $ V $是股票的日平均交易量(ADV)。

潜在局限¶

需要注意的是,Almgren等人的论文发布时间较早,既没有经历2007年8月的“量化崩溃”,也没有遭受2008Q4开始的金融危机。论文发表之后金融市场微观结构已经有了许多发展变化。

下面我们用代码来实现以上公式。

def perm_impact(pct_adv, annual_vol_pct = 0.25, inv_turnover = 200):gamma = 0.314return 10000 * gamma * (annual_vol_pct / 16) * pct_adv * (inv_turnover)**0.25def temp_impact(pct_adv, minutes, annual_vol_pct = 0.25, minutes_in_day = 60*6.5):eta = 0.142day_frac = minutes / minutes_in_dayreturn 10000 * eta * (annual_vol_pct / 16) * abs(pct_adv/day_frac)**0.6def tc_bps(pct_adv, minutes, annual_vol_pct = 0.25, inv_turnover = 200, minutes_in_day = 60*6.5):perm = perm_impact(pct_adv, annual_vol_pct=annual_vol_pct, inv_turnover=inv_turnover)temp = temp_impact(pct_adv, minutes, annual_vol_pct=annual_vol_pct, minutes_in_day=minutes_in_day)return 0.5 * perm + temp

对于一支波动率为1.57%的股票,我们想要交易其日平均交易量的10%的股票,并且要求在半天内完成此操作,那么预计将会产生8个基点的市场冲击(这是Almgren在这种情况下对临时性冲击的成本估计)。

变量数值
$\Theta/V$263
日波动率 ($\sigma$)1.57%
日平均交易量的百分比 (X/V)10%
ItemFastMediumSlow
永久性冲击 (bps)202020
交易时间 ( 占一天的百分比)10%20%50%
临时性冲击 (bps)22158
总市场冲击 (bps)322518
print('Cost to trade Fast (First 40 mins):'+ str(round(tc_bps(pct_adv=0.1, annual_vol_pct=16*0.0157, inv_turnover=263, minutes=0.1*60*6.5),2))+ 'bps')print('Cost to trade Fast (First 90 mins):'+ str(round(tc_bps(pct_adv=0.1, annual_vol_pct=16*0.0157, inv_turnover=263, minutes=0.2*60*6.5),2))+ 'bps')print('Cost to trade Fast (First 40 mins):'+ str(round(tc_bps(pct_adv=0.1, annual_vol_pct=16*0.0157, inv_turnover=263, minutes=0.5*60*6.5),2))+ 'bps')
Cost to trade Fast (First 40 mins):32.22bps
Cost to trade Fast (First 90 mins):24.63bps
Cost to trade Fast (First 40 mins):18.41bps

如果想要交易其日平均交易量的0.5%,并且计划在30分钟完成此操作,那么将会产生近5个基点的市场冲击。

print(str(round(tc_bps(pct_adv=0.005, minutes=30, annual_vol_pct=16*0.0157),2))+'bps')
4.79bps

假设我们想要交易200万美元的Facebook股票,我们使用算法交易策略下单(例如VWAP),并计划在15分钟内完成。

让我们来看一看这种情况下的市场冲击情况。

trade_notional = 2000000 # 2M notionalstock_price = 110.89 # dollars per shareshares_to_trade = trade_notional/stock_pricestock_adv_shares = 30e6 # 30 Mstock_shares_outstanding = 275e9/110.89expected_tc = tc_bps(shares_to_trade/stock_adv_shares, minutes=15, annual_vol_pct=0.22)print ("Expected tc in bps: %0.2f" % expected_tc)print ("Expected tc in $ per share: %0.2f" % (expected_tc*stock_price / 10000))
Expected tc in bps: 1.66
Expected tc in $ per share: 0.02

交易时间长度不变,交易成本随着订单相对大小(交易量占股票日平均交易量的比例)的变化而改变。同时,波动率大的交易日内市场冲击也会更显著。

x = np.linspace(0.0001,0.03)plt.plot(x*100,tc_bps(x,30,0.25), label="$\sigma$ = 25%");plt.plot(x*100,tc_bps(x,30,0.40), label="$\sigma$ = 40%");plt.ylabel('tcost in bps')plt.xlabel('Trade as % of ADV')plt.title('tcost in Basis Points of Trade Value; $\sigma$ = 25% and 40%; time = 15 minutes');plt.legend();


此图中,横轴是相对订单大小(交易量占日平均交易量的比例),纵轴是总交易成本,蓝线是波动率为25%的情况,绿线是波动率为40%的情况。绿线在蓝线上方,说明震荡行情会加大交易成本。

那么,如果交易时间长度也改变呢?下面是两幅等高图。

x = np.linspace(0.001,0.03)y = np.linspace(5,30)X, Y = np.meshgrid(x,y)Z = tc_bps(X,Y,0.20)levels = np.linspace(0.0, 60, 30)cmap=plt.get_cmap('Reds')cmap=plt.get_cmap('hot')cmap=plt.get_cmap('jet')plt.subplot(1,2,1);CS = plt.co*ur(X*100, Y, Z, levels, linewidths=3, cmap=cmap, alpha=0.55);plt.clabel(CS);plt.ylabel('Trading Time in Minutes');plt.xlabel('Trade as % of ADV');plt.title('tcost in Basis Points of Trade Value; $\sigma$ = 20%');plt.subplot(1,2,2);Z = tc_bps(X,Y,0.40)CS = plt.co*ur(X*100, Y, Z, levels, linewidths=3, cmap=cmap, alpha=0.55);plt.clabel(CS);plt.ylabel('Trading Time in Minutes');plt.xlabel('Trade as % of ADV');plt.title('tcost in Basis Points of Trade Value; $\sigma$ = 40%');


这两幅等高图中,横轴仍是相对订单大小(交易量占日平均交易量的比例),纵轴是交易时间长度,相同颜色的线代表相等的交易成本。由图可知,交易时间越充裕,交易成本越低,这也符合我们的经验认识。

下面我们使用coutourf函数对等高线间的区域进行填充。

x = np.linspace(0.001,0.03) # % ADVy = np.linspace(1,60*6.5)   # time to tradeX, Y = np.meshgrid(x, y)levels = np.linspace(0.0, 390, 20)cmap=plt.get_cmap('Reds')cmap=plt.get_cmap('hot')cmap=plt.get_cmap('jet')plt.subplot(1,2,1);Z = tc_bps(X,Y,0.20)plt.co*urf(X*100, Z, Y, levels, cmap=cmap, alpha=0.55);plt.title('Trading Time in Minutes; $\sigma$ = 20%');plt.xlabel('Trade as % of ADV');plt.ylabel('tcost in Basis Points of Trade Value');plt.ylim(5,20)plt.colorbar();plt.subplot(1,2,2);Z = tc_bps(X,Y,0.40)plt.co*urf(X*100, Z, Y, levels, cmap=cmap, alpha=0.55);plt.title('Trading Time in Minutes; $\sigma$ = 40%');plt.xlabel('Trade as % of ADV');plt.ylabel('tcost in Basis Points of Trade Value');plt.ylim(5,20);plt.colorbar();


coutourf和coutour都是绘制等高图。上面两张用coutourf画出的图是不是更美观呢?下次你也可以试一试。

市场冲击成本拆分:永久性冲击和临时性冲击¶

对一支特定的股票,让我们看看市场冲击成本是如何拆分成永久性冲击和临时性冲击的。

minutes = 30x = np.linspace(0.0001,0.03)f, (ax1, ax2) = plt.subplots(ncols=2, sharex=True, sharey=True)f.subplots_adjust(hspace=0.15)p = 0.5*perm_impact(x,0.20)t = tc_bps(x,minutes,0.20)ax1.fill_between(x*100, p, t, color='b', alpha=0.33);ax1.fill_between(x*100, 0, p, color='k', alpha=0.66);ax1.set_ylabel('tcost in bps')ax1.set_xlabel('Trade as % of ADV')ax1.set_title('tcost in bps of Trade Value; $\sigma$ = 20%; time = 15 minutes');p = 0.5*perm_impact(x, 0.40)t = tc_bps(x,minutes, 0.40)ax2.fill_between(x*100, p, t, color='b', alpha=0.33);ax2.fill_between(x*100, 0, p, color='k', alpha=0.66);plt.xlabel('Trade as % of ADV')plt.title('tcost in bps of Trade Value; $\sigma$ = 40%; time = 15 minutes');


类似之前的图,横轴是相对订单大小(交易量占日平均交易量的比例),纵轴是总交易成本。图中黑色部分代表永久性冲击,紫色部分代表临时性冲击。

Kissell模型(2004)¶

该模型假设如果一支股票所有股份均进入买卖报价市场,那么将会产生理论上的冲击成本𝐼*。

$$ MI_{bp} = b_1 I^* POV^{a_4} + (1-b_1)I^*$$

其中:$$ I^*  = a_1 (\frac{Q}{ADV})^{a_2} \sigma^{a_3}$$

$$POV = \frac{Q}{Q+V}$$

  • $I^*$ 是瞬时冲击

  • $POV$ 是交易量比率的百分比形式

  • $V$ 是交易时间内预计的成交量

  • $b_1$ 是暂时性冲击参数

  • $ADV$ 是三十日平均日交易量

  • $Q$ 是订单大小

作者用上述形式的模型对大量市场数据进行拟合,得到以下参数:

参数数值
$b_1$0.80
$a_1$750
$a_2$0.50
$a_3$0.75
$a_4$0.50

编写Kissell模型函数,作图展示市场冲击的效果。

def kissell(adv, annual_vol, interval_vol, order_size):b1, a1, a2, a3, a4 = 0.9, 750., 0.2, 0.9, 0.5 #原代码这里很迷i_star = a1 * ((order_size/adv)**a2) * annual_vol**a3PoV = order_size/(order_size + adv)return b1 * i_star * PoV**a4 + (1 - b1) * i_star
x = np.linspace(0.0001,0.1)plt.plot(x,kissell(5*10**6,0.1, 2000*10**3, x*2000*10**3), label="$\sigma$ = 10%");plt.plot(x,kissell(5*10**6,0.25, 2000*10**3, x*2000*10**3), label="$\sigma$ = 25%");#plt.plot(x,kissell(5*10**6,0.40, 2000*10**3, x*2000*10**3), label="$\sigma$ = 40%");plt.ylabel('tcost in bps')plt.xlabel('Trade as % of ADV')plt.title('tcost in Basis Points of Trade Value; $\sigma$ = 25% and 40%; time = 15 minutes');plt.legend();


从图中可以看出,影响市场冲击的因素和冲击方向与前面的Almgren模型一致。

J.P.摩根市场冲击模型¶

$$MI(bps) = I \times \omega \times \frac{2 \times PoV}{1 + PoV} + (1-\omega) \times I + S_c$$

其中$$I = \alpha \times PoV^\beta \times Volatility^\gamma$$

  • $\omega$ 是临时性冲击所占的比例(流动性成本)

  • $\alpha$ 是一个缩放参数

  • $PoV$ 是订单相对规模

  • $S_c$ 是买卖价差

对于美国股票市场,截至2016年6月的拟合参数为

参数数值
b ($\omega$)0.931
a1 ($\alpha$)168.5
a2 ($\beta$)0.1064
a3 ($\gamma$)0.9233
from __future__ import divisiondef jpm_mi(size_shrs, adv, day_frac=1.0, spd=5,   spd_frac=0.5, ann_vol=0.25, omega=0.92,   alpha=350, beta=0.370, gamma=1.05):PoV = (size_shrs/(adv*day_frac))I = alpha*(PoV**beta)*(ann_vol**gamma)MI = I*omega*(2*PoV)/(1+PoV) + (1-omega)*I + spd*spd_fracreturn MIdef jpm_mi_pct(pct_adv, **kwargs):return jpm_mi(pct_adv, 1.0, **kwargs)

假设一个订单:

  • 购买100,000的XYZ,交易量为10%

  • XYZ的平均日交易量 = 1,000,000股

  • XYZ年化波动率= 25%

  • XYZ的平均差价= 5bps

我们来计算冲击成本。

spy_adv = 85603411.55print (str(round(jpm_mi(size_shrs = 10000, adv = 1e6),2))+' bps') # 1% pct ADV orderprint(str(round(jpm_mi(size_shrs = 0.05*spy_adv, adv = spy_adv, spd = 5, day_frac = 1.0),2))+ ' bps') # 5% pct ADV of SPY order
3.96 bps
7.02 bps

Qua*pian成交量滑点模型¶

$$\text{tcost} = 0.1 \left| \frac{X}{VT} \right|^2 $$

其中$X$ 是想要交易的股票数量; $T$ 是交易时间长度占一天的百分比; $V$ 是一支股票的日平均交易量。

特别需要注意的是,Qua*pian模型中不包含波动率因素,这点与前三个模型不同。

如果你想了解关于该模型更多的信息,参见以下网址 : https://www.qua*pian.com/help#ide-slippage

def tc_Q_vss_bps(pct_adv, minutes=1.0, minutes_in_day=60*6.5):day_frac = minutes / minutes_in_daytc_pct = 0.1 * abs(pct_adv/day_frac)**2return tc_pct*10000
print (str(tc_Q_vss_bps(pct_adv=0.1/390, minutes=1))+' bps')print (str(tc_Q_vss_bps(pct_adv=0.25/390, minutes=1))+' bps')
10.000000000000002 bps
62.5 bps
print (str(tc_Q_vss_bps(pct_adv=0.1, minutes=0.1*60*6.5))+' bps')print (str(tc_Q_vss_bps(pct_adv=0.1, minutes=0.2*60*6.5))+' bps')print (str(tc_Q_vss_bps(pct_adv=0.1, minutes=0.5*60*6.5))+' bps')
1000.0 bps
250.0 bps
40.00000000000001 bps
print (str(tc_bps(pct_adv=0.005, minutes=30, annual_vol_pct=0.2))+' bps')print (str(tc_Q_vss_bps(pct_adv=0.005, minutes=30))+' bps')
3.81208329371434 bps
4.2250000000000005 bps

下面是四种模型在两种波动率情况下的七条市场冲击曲线汇总图(3*2+1=7,1是不含波动率的Quanopian模型)。

x = np.linspace(0.0001, 0.01)plt.plot(x*100,tc_bps(x, 30, 0.20), label="Almgren $\sigma$ = 20%");plt.plot(x*100,tc_bps(x, 30, 0.40), label="Almgren $\sigma$ = 40%");plt.plot(x*100,tc_Q_vss_bps(x, minutes=30),label="Q VSS");plt.plot(x*100,jpm_mi_pct(x, ann_vol=0.2), label="JPM MI1 $\sigma$ = 20%");plt.plot(x*100,jpm_mi_pct(x, ann_vol=0.4), label="JPM MI1 $\sigma$ = 40%");plt.plot(x*100,kissell(5*10**6,0.20, 2000*10**3, x*2000*10**3), label="Kissell $\sigma$ = 20%");plt.plot(x*100,kissell(5*10**6,0.40, 2000*10**3, x*2000*10**3), label="Kissell $\sigma$ = 40%", color='black');plt.ylabel('tcost in bps')plt.xlabel('Trade as % of ADV')plt.title('tcost in Basis Points of Trade Value; time = 30 minutes');plt.legend();


我们可以看到,Qua*pian的那条线(Q VSS)斜率上也是最特殊的,只有它是斜率随着订单规模递增。这说明这个模型的计算下,交易量较小的订单,估测的交易成本相比其他模型会更低。

结论¶

综上所述,以下订单属性会导致更高的市场冲击成本:

  • 订单相对规模较大

  • 日交易量较低

  • 交易时间长度较短

  • 波动率较高

  • 交易紧迫性较强或股票订单较大

  • 日内交易时间较早

  • 买卖价差较大

  • 市场交易量下降

不同的投资者在买卖股票时,事实上存在交易能力的差异。交易能力的强弱直接影响股票投资所面临的可能风险和潜在收益。交易成本与投资收益密不可分,也是考察投资者交易能力的重要方式。如何降低交易成本,是投资者投资过程中不可回避的问题(特别是股票订单较大时)。

更进一步地说,投资者在进行股票交易时,选择不同的交易策略则可能会面临不同的执行成本和机会成本。我们已经在本文详细探讨了如何考量市场冲击及执行成本,那么机会成本又该如何衡量呢?我们下次再一起学习~


全部回复

0/140

量化课程

    移动端课程