能生存下来的,不是最强壮的,也不是最聪明的,而是对变化适应能力最强的物种。
——查尔斯·达尔文
01 引言
《交易系统与方法》(Trading Systems and Methods)作者是佩里 J. 考夫曼(Perry J. Kaufman),中文版由高闻酉、高瑄和郭思齐翻译。全书共有二十四章:1概述、2基本概念和计算、3图表分析、4图表系统和技术、5事件驱动趋势分析、6回归分析、7基于时间因子趋势分析、8趋势交易系统、9动量和震荡指标、10季节和日历效应模式、11价格循环模式分析、12成交量、未平仓合约和行情宽幅、13利差和套利、14行为金融下的交易策略、15模式识别、16日内交易、17自适应技术、18价格分布系统、19多重时间框架分析、20领先性的技术指标、21交易系统测试、22实证分析模式、23风险控制模式、24多元投资组合的资产配置,内容全面而系统,可以说是一本全面解读交易系统与量化投资的百科全书。
该书对“技术分析”赋予了新的内涵,认为技术分析必须有清晰完整的规则,是对价格、成交量、波幅和未平仓合约等的系统评估,包括任何定量分析以及所有形式的模式识别,其目标是提前决定一段时间内的价格走势,无论是 1 小时、1 天还是 5 年。在当前的交易大环境之下,技术分析不再仅仅是研究图表模式或识别趋势,它还包含了市场内部各要素的分析、复杂的指标分析、均值回归模式的分析,以及测试结果的评价等;同时,技术分析也可以用一个简单的移动平均线或神经网络来预测价格的变动。
该书将量化交易技术和方法融入传统技术分析框架,包括程序化交易、算法交易、高频交易、套利(期现、息差等)、配对交易、季节性交易模式等。在技术分析框架之下,可以对成千上万只股票进行扫描、分类,从而寻找其关键属性,比如发掘高动量模式,观察最近一次的行情突破,或者探索其他的价格指征,这些都是具有更广泛意义的技术分析。
市场行情由基本面因素所驱动。如就业、国内生产总值、通货膨胀率、消费者信心指数、供求关系、地缘政治等都会产生预期的价格变动。但是,要应用这些基本面因子进行交易很难,因为经济指标存在滞后性,而经济学家对这些基本面的把握从来都不是很准确。
任何市场所反映的大部分价格变动都是可预见的;它源于对宏观经济发展影响的预期或良好的企业管理和新产品的结果。如政府可能会阻止两家公司的合并,或者批准或拒绝一种新药。对就业报告、房屋开工或玉米生产报告的预期会导致高度公开的专业估计,这可能会在实际报告发布之前正确或错误地移动价格。然后市场对估计的准确性而不是经济数据本身做出反应。当公众准备采取行动时,消息已经反映在价格上。
许多基本面和经济分析的人认为:从某一天至第二天的价格运动方向没有序列相关性;也就是说,价格对其之前的运行模式没有“记忆”——这被称为“随机游走理论”。价格将寻求平衡供需因素的水平,但随着价格响应最新的可用信息或新闻发布,价格将立即或以不可预测的方式达到该水平。
如果随机游走理论是正确的,那么许多基于数学和模式识别的交易方法将会失败。当然,随机游走理论一直存在争议,有两个反对价格随机游走的论据:一是数据表明,很多对冲基金、衍生品基金的算法交易策略持续成功运行20年甚至30年;二是价格是按预期运行的。
从学术上来讲,所有参与者都可以准确地理解价格应该是根据新闻信息而运行的,然而事实并非如此,即信息与新闻的重要性没有那些对市场的进一步运行模式所做的预期那么大。例如,如果美联储今年两次降息,而经济尚未做出反应,你会期望它再次降息吗?你当然会。因此,一旦美联储宣布降息,你就会猜测下一次降息。当大多数交易者持有相同的预期时,价格会迅速升至该水平。然后价格对与预期相关的进一步消息做出反应。这种价格走势是否符合随机游走理论?否,但价格变动的实际模式可能与随机变动相似。
除了预期,价格的明显随机变动取决于时间间隔和观察数据的频率。当使用较长的时间跨度(从 1 到 20 年)并对数据进行平均以增强平滑过程时,趋势特征会随着季节和周期性变化而更加清晰。技术方法(如移动平均线)通常用于分离这些价格特征。平均每日或每周数据以创建每月或每季度的价格可以消除不规则的短期波动,从而提高连续价格之间的相关性。使用频率较低的数据,更容易看到趋势。一般来说,每日数据的使用显示出比每周或每月数据更多的噪音(随机移动)。
从长远来看,价格寻求一个均衡水平。股票价格的均衡是指投资回报(股票价值升值加上股息)与投资风险相平衡,使其与无风险投资(如国债)的回报处于同等地位。而期货价格的均衡主要是供需之间的平衡。
价格的走势不是对称的,也不是正态分布:这是另外两个反对随机游走的事实。指数市场的不对称性很容易理解,因为绝大多数的交易方是由多头所构成。但当价格在压力时期(或供需异常失衡)偏离正常价值时,价格走势也会表现出独特的模式。当某种价格单边“运行”几个小时或几天,即价格在同一方向上所持续的时间序列很长,且超出了正常水平,相关的价格数据将呈现一个“厚尾”的形态。其实,厚尾形态意味着非常态(常态指正态分布)的概率分布,因为尾部数据必须来自另类事件所驱动。这些价格模式的差异是某些交易方法奏效的原因。
02 价格噪音
噪音是于任意时刻围绕基础资产价格走势所生成的不稳定的行情变化模式。我们可以把高噪音比作一个正在醉醺醺地行走着的海员,而低噪声则宛如一条从起点至终点的直线。《交易系统与方法》介绍了三种衡量价格噪音的指标:效益比率、价格密度和分形维度。
效益比率也称为分形效率,计算公式如下:
其中ER表示效益比率,P为价格,n为计算时期周期。
下面使用Python计算价格噪音指标并获取股票指数数据进行量化分析。
导入Python金融量化常用的模块。
import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as snsimport tushare as tssns.set_style('darkgrid')
构建效益比率计算函数
def cal_er(ser): '''输入数据ser为价格的Series''' a=abs(ser.iloc[-1]-ser.iloc[0]) b=abs(ser-ser.shift(1)).sum() return a/b
下面通过模拟数据展示效益比率如何衡量时间序列的噪音。
from pylab import mplmpl.rcParams['font.sans-serif']=['SimHei']mpl.rcParams['axes.unicode_minus']=False#模拟数据x=np.arange(10)y1=0.4*x+2y2=0.4*x+2+0.5*np.random.randn(10)y3=0.4*x+2+1.5*np.random.randn(10)y2[0]=y3[0]=0.4*x[0]+2y2[-1]=y3[-1]=0.4*x[-1]+2#可视化plt.figure(figsize=(10,6))er1=cal_er(pd.Series(y1))er2=cal_er(pd.Series(y2))er3=cal_er(pd.Series(y3))plt.plot(x,y1,label=f'无噪音:ER={er1}');plt.plot(x,y2,label=f'中等噪音:ER={er2:.2f}');plt.plot(x,y3,label=f'高等噪音:ER={er3:.2f}');plt.xticks(range(10))plt.legend();
图中三条线展示了起点和终点相同的三种不同的价格运行模式,其中直线表示没有噪音,较小的变化差异对应中级噪音,而大幅波动意味着高噪音。
通过改变净价格变动,我们可以区分噪音和波动。如果单个价格变化的总和相同,但净变动较大,则噪声较小。
x=np.arange(10)y1=x+1.5*np.random.randn(10)y2=0.5*x+1.5*np.random.randn(10)y1[0]=y2[0]=x[0]plt.figure(figsize=(10,6))er1=cal_er(pd.Series(y1))er2=cal_er(pd.Series(y2))plt.plot(x,y1,label=f'相对低噪声:ER={er1:.2f}');plt.plot(x,y2,label=f'相对高噪声:ER={er2:.2f}');plt.xticks(range(10))plt.legend();
价格密度(Price Density)
def cal_pd(df): '''输入数据为包含最高、最低、收盘价的dataframe''' return (df.high-df.low).sum()/(df.high.max()-df.low.min())
分形维度(Fractal Dimension)
def cal_fd(df): '''输入数据为包含最高、最低、收盘价的dataframe''' n=len(df) L=np.sqrt((1/n)**2+abs(df.close-df.close.shift(1))/(df.high.max()-df.low.min())).sum() return 1+(np.log(L)+np.log(2))/np.log(2*n)
分形维度和效益比率之间相关性较强(分形效率);同时,价格密度和分形维度的构建过程比较相似,即值越大噪音越大,而效益比率则相反,值越大噪音越小。这三种测量噪音的方法中,效益比率法是最好理解和计算的,后面对噪音的分析将主要使用该指标。
03 噪音的应用分析
(1)获取数据
本文使用tushare pro获取全球主要股票指数2010-2022年价格数据,对比分析这些指数在某时间周期的噪音,并构建简单的交易策略。
token='输入你在tushare pro上获取的token'pro=ts.pro_api(token)def get_index_data(code,start='20100601', end='20220719'): if code[0].isdigit(): df=pro.index_daily(ts_code=code,start_date=start,end_date=end) else: df=pro.index_global(ts_code=code,start_date=start,end_date=end) df.index=pd.to_datetime(df.trade_date) df = df.rename(columns={'vol': 'volume'}) df=(df.sort_index()).drop('trade_date',axis=1) return df[['open','high','low','close','volume']]
china_indexs={'上证综指': '000001.SH','深证成指': '399001.SZ','沪深300': '000300.SH','创业板指': '399006.SZ','上证50': '000016.SH','中证500': '000905.SH','中小板指': '399005.SZ','上证180': '000010.SH'}global_indexs={'恒生指数':'HSI','道琼斯工业指数':'DJI','标普500指数':'SPX','纳斯达克指数':'IXIC','法国CAC40指数':'FCHI','德国DAX指数':'GDAXI','日经225指数':'N225','韩国综合指数':'KS11','澳大利亚标普200指数':'AS51','印度孟买SENSEX指数':'SENSEX','台湾加权指数':'TWII'}#指数合并字典index_dict=dict(**china_indexs, **global_indexs)#获取全部指数价格数据index_data=pd.DataFrame()for name,code in index_dict.items(): index_data[name]=get_index_data(code).close
如因积分受限或其他原因无法获取上述数据,可在公众号后台回复“指数数据”,免费获取百度网盘下载链接。
将这些指数价格数据初始化价格设置为1,则可以横向对比不同指数在2010-2022年的价格走势(类似基金的累计净值),反映的是从2010年开始投资1元,至2022年7月的累计净值。图中显示美股指数,尤其是纳斯达克指数表现最突出,12年内最高累计增长了6倍,近期累计增长4倍。国内指数价格走势中等偏下,而中国台湾的指数累计净值则垫底。
#保存本地数据并读取index_data.to_csv('global_data.csv')index_data=pd.read_csv('global_data.csv',index_col=0)
sss=index_data.fillna(method='ffill').dropna()(sss/sss.iloc[0]).plot(figsize=(16,7));
分别以3、5、14、21、60天作为滚动窗口,计算全球股票指数平均噪音(值越大噪音越小)。以日为周期的话,时间越长噪音越大,中间的可能波动空间较大。各大指数的噪音数值在相同周期下差异较小。
days=[3,5,14,21,60]from tqdm import tqdmindex_er={}for d in tqdm(days): index_er[str(d)+'天噪音']=sss.rolling(d).apply(cal_er).mean()pd.DataFrame(index_er)
(3)噪音对趋势交易的影响
下面通过构建一个简单的均线趋势交易策略,考察噪音与趋势交易之间的关系。
def cal_ret(ser,window=40,fig=False): df=pd.DataFrame(ser).rename(columns={ser.name:'close'}) df['ma']=ser.rolling(20).mean() df['ret']=df.close/df.close.shift(1)-1 df.loc[df.close>df.ma,'signal']=1 df.loc[df.close<df.ma,'signal']=0 #今天根据昨天的价格给出的信号交易 df['position']=df['signal'].shift(1) df['position'].fillna(method='ffill',inplace=True) df=df.dropna() df['capital_ret']=df.ret*df.position #计算累计收益率 df['趋势跟踪']=(df.capital_ret+1.0).cumprod()-1 df['买入持有']=(df.ret+1.0).cumprod()-1 if fig: df[['趋势跟踪','买入持有']].plot(figsize=(16,7)); return df['趋势跟踪'].iloc[-1]
以40日作为滚动窗口,计算各指数的平均噪音,作为横坐标,纵坐标是指数的均线趋势策略收益率。将各指数的效益比率(与噪音负相关)和趋势收益画成散点图并拟合一条回归线。图中显示,效益比率与趋势交易收益率呈现正相关,说明噪音越小越适合做趋势交易。
window=40x=sss.rolling(window).apply(cal_er).mean()y=sss.apply(cal_ret)txt=x.indexdata=pd.DataFrame()data['x']=xdata['y']=yplt.figure(figsize=(12,8))sns.regplot(x='x',y='y',data=data);plt.ylim(-1,5.5)for i in range(len(x)): plt.annotate(txt[i], xy = (x[i], y[i]), xytext = (x[i], y[i])) plt.show()
cal_ret(sss['上证综指'],fig=True)
0.84266
(4)构建交易策略
上证综指和效益比率(噪音)走势图。
plt.figure(figsize=(15,7))plt.subplot(211)sss['上证综指'].plot();plt.subplot(212)sss['上证综指'].rolling(14).apply(cal_er).plot(color='r');plt.xlabel('');
下面利用价格噪音指标构建一个简单的交易策略,即当效益比率大于某个阈值(默认设置为0.5)时持有多头头寸,否则空仓。此处代码较长,完整代码分享在Python金融量化知识星球(见文末)。
def trend_strategy(df,window=14,threshold=.5): pass#将上述函数整合成一个执行函数def main(code='000001.SH',start='20101117', end='20220720',window=14,threshold=.5): pass
回测结果如下:
main()
回测标的:上证综指指数回测期间:20101117—20220720总收益率: 策略:95.22%,标的:16.41%年化收益率:策略:6.07%, 标的:1.35%最大回撤: 策略:13.75%, 标的:52.3%策略Alpha: 0.06, Beta:0.22,夏普比率:1.17
上证综指利用该择时策略有较大的改善(当然这里没有考虑交易费用),最大回撤13.75%,而买入持有的最大回撤高达52%。而纳斯达克指数使用该策略则效果不佳,实际上纳斯达克指数经历长达十几年的牛市,买入持有可以获得373%的累计收益率。使用噪音择时可以避免大幅回撤,但也错过了很大的收益。
main(code='IXIC')
回测标的:纳斯达克指数指数回测期间:20101117—20220720总收益率: 策略:53.19%,标的:373.07%年化收益率:策略:3.71%, 标的:14.19%最大回撤: 策略:10.14%, 标的:33.7%策略Alpha: 0.02, Beta:0.15,夏普比率:0.41
将时间滚动窗口缩短至3天,噪音阈值降低至0.05(大部分时间持仓),此时相当于很少进行交易,与买入持有策略相近。这一参数的调整有点类似对历史数据的过度拟合了,虽然收益率提高了,但是最大回撤也提高到接近30%。
main(code='IXIC',window=3,threshold=0.05)
回测标的:纳斯达克指数指数回测期间:20101117—20220720总收益率: 策略:440.74%,标的:373.07%年化收益率:策略:15.5%, 标的:14.19%最大回撤: 策略:29.74%, 标的:33.7%策略Alpha: 0.02, Beta:0.96,夏普比率:2.32
04 结语
本文介绍了《交易系统与方法》的基本框架,并重点围绕书中第一章关于价格噪音衡量的方法,通过获取全球指数价格数据,为大家展示了价格噪音的计算,及其对趋势交易的影响,最后以噪音指标(效益比率)为阈值构建一个简单的择时策略。文中的策略仅供代码编程和学习参考,不构成任何买卖建议。
联系客服