从量化交易到资产管理—绝对收益之CTA策略开发

论坛 期权论坛 期权     
期权匿名问答   2021-12-31 14:48   10390   0
学习这门课有什么收获呢?首先这门课程是基于JavaScript和Python编程语言,语言只是一门技术,最终我们还是要把这门技术应用到一个行业中。量化交易是一个新兴的行业,目前正处于快速发展阶段,对人才的需求量也很大。

通过本课程的系统学习,可以让你对量化交易这个领域有更深入的认识,如果你是准备跨入量化交易领域的同学对你也有所帮助,如果你是股票或期货投资爱好者,那么量化交易完全可以辅助你的主观交易,通过开发交易策略能够在金融市场上获得利润,也为自己拓宽投资理财的渠道和平台。
在这之前,先讲一下我个人的交易经历,我不是金融专业出身,学的是统计专业。最早从学生时代开始做股票主观交易,后来偶然因素成为国内私募基金的量化交易从业者,主要做策略研究和策略开发。
在交易这个圈子,前前后后也有十几年的时间,也开发过各种类型的策略。我的投资理念是:风险控制高于一切,专注于绝对收益。我们的课程题目就是:从量化交易到资产管理——绝对收益之CTA策略开发。
1、期货CTA策略赚钱逻辑

1.1 认识期货CTA

可能会有人问什么是CTA?CTA到底是什么东西?CTA在国外叫做商品交易顾问,在国内通常称为投资管理人。传统的CTA是将广大投资者的资金集中起来,然后委托给专业的投资机构,最后通过交易顾问(也就是CTA)进行股指期货、商品期货、国债期货投资。
但实际上,伴随着全球期货市场不断发展壮大,CTA的概念也在不断放大,范围之广远超传统期货。它不仅可以投资于期货市场,还可以投资利率市场、股票市场、外汇市场以及期权市场等等,只要这个品种有一定量的历史数据,就可以根据这些历史数据,开发对应的CTA策略。
早在80年代之前,电子盘技术还不太成熟,那个时候大部分交易员是通过手动绘制威廉指标、KDJ、RSI、MACD、CCI等技术指标来判断商品期货的未来走势。后来就有交易员成立专门的CTA基金帮助客户管理资产。直到80年代电子盘普及之后,真正意义上的CTA基金才开始出现。
CTA基金管理规模变动情况




单位:十亿美元
我们看上面这张图,特别是随着量化交易的兴起,全球CTA基金规模已经从2005年的1306亿美元,到2015年已经超过3000多亿美元。并且CTA策略也成为全球对冲基金较为主流的投资策略之一。
与规模同时上升的是CTA基金的业绩,我们来看下图的巴莱克CTA指数,巴莱克CTA指数是全球商品交易顾问具有代表性的行业基准。自1979年末至2016年末,巴莱克CTA基金指数累积收益高达28.95倍,年化收益率为9.59%,夏普比率为0.37,最大回撤为15.66%。
由于在资产配置组合中,CTA策略通常与其他策略都保持着极低的相关性。如下图红圈处,在2000~2002年全球股票熊市以及2008年全球次贷危机时期,巴莱克CTA基金指数不仅没有下跌还实现了正收益,当股票市场和债券市场发生危机时,CTA可以提供强劲的收益。另外我们还可以看到,巴克莱商品CTA指数1980年以来的盈利水平,一直力压标普500,而且回撤也要比标普500低很多。



我国CTA的发展也只是近十年的事,但是势头很强进,这多半是受益于国内商品期货比较开放的交易环境,交易资金门槛较低、采用保证金制度可以多空双向交易、交易费用低廉、交易所的技术架构相对于股票更先进、也更易于系统交易等等原因。
从2010年以来,CTA基金主要是以私募基金的形式存在。随着国内政策对基金专户投资范围的逐渐开放,CTA基金开始以基金专户的形式存在,其更加透明公开的运作方式,也成为更多投资者资产配置的必要工具。



如上图所示,不管是从入手难易程度、资金门槛、交易策略执行方式以及API对接,相对于其他交易策略,CTA策略同样也更适合个人交易者。国内期货品种合约非常小,比如:一手玉米或豆粕几千块钱就可以交易,几乎没有资金门槛,另外由于一部分CTA策略来自于传统的技术分析,所以相对于其他策略而言,还是比较容易的。



CTA策略的设计流程也相对简单,首先把历史数据进行初步处理,然后输入到量化模型中,量化模型包括了数学建模、编程设计等工具形成的交易策略,通过计算分析这些数据产生交易信号。当然在实际开发中,并不像上图中那么简单,这里只是让大家有个整体的概念。
1.2 期货CTA策略类型

从交易策略上来看CTA策略也是多元化的:它可以是趋势策略、也可以是套利策略;可以是大周期中长线策略、也可以是日内短线策略;策略逻辑可以基于技术分析、也可以基于基本面分析;可以是主观交易,也可以是系统交易。
CTA策略有不同的分类方法,根据交易方法,可以分为:主观交易和系统交易,国外的CTA策略发展的比较先进,系统交易的CTA策略已经接近100%。根据分析方法,可以分为:基本面分析和技术分析。根据收益来源,可以分为:趋势交易和震荡交易。
总体来看,CTA策略在整个交易市场上,趋势策略占比约70%,均值回归策略占25%左右,反趋势或趋势反转占5%左右。其中占比最大的趋势策略,根据持仓周期,又可以分为:高频交易、日内交易、中短线交易、中长线交易。
高频做市策略
目前市面上有两种主流的高频交易策略,一种是高频做市策略,另一种是高频套利策略。做市策略是在交易市场中提供流动性,也就是说在有做市商的交易市场中,有人想买卖交易,做市商就必须保证他的单子能成交。如果市场上的流动性不足,导致单子无法成交,做市商必须买卖别人的对手盘。
高频套利策略
高频套利是交易两个相关性强的股票或者ETF和ETF组合。根据ETF的计算方法,可以用相同的方法计算一个ETF期望价格。由ETF指数价格可能会减去ETF期望价格,可以得到一个价差,通常这个价差会在一个价格通道内运行,如果价差突破上下通道,就可以交易这个价差,等待价差的回归,从中赚取收益。
日内策略
如果按照字面的意思,只要是不持仓过夜的,都可以称为日内交易策略。由于日内交易持仓周期较短,通常在入市之后不能马上获利,就迅速离场。因此这种交易方式承受的市场风险较低。但是因为市场在短时间内变化较快,所以日内策略通常对交易者的要求比较高。
中长线策略
理论上,持仓周期越长,策略容量越大,风险收益比越低。尤其在机构交易中,因为短线策略容量有限,大资金不能在短时间内进场出场,所以会配置更多的中长线策略。通常持仓周期是数天数月,甚至更长的时间。
CTA策略数据
一般来说CTA策略是以分钟、小时以及日线数据为研究对象,其中数据包括:开盘价、最高价、最低价、收盘价、成交量等等;只有少部分CTA策略会用到Tick数据,比如L2数据中的买价、卖价、买量、卖量等深度数据。



对于CTA策略的基本思路,我们首先想到的还是基于传统技术指标,因为这方面的公开参考资料比较多,逻辑通常也比较简单,大部分是基于统计学原理。比如大家耳熟能详的各种技术指标:MA、SMA、EMA、MACD、KDJ、RSI、BOLL、W&R、DMI、ATR、SAR 、BIAS、OBV、等等等等。
市面上也有一些经典的交易模型,也可以借鉴参考并加以改良,包括:多均线组合、DualThrust、R-Breaker、海龟交易法、网格交易法等等。
以上这些都是基于传统技术分析的交易策略,其过程就是根据历史数据以及正确的交易理念,提炼出有概率优势的因子或者买卖条件,并假设市场在未来时间依然存在这种规律,最后用代码实现交易策略并全自动交易。开仓、止盈、止损、加仓、减仓等等,这些在一般情况下都不需要人工干预。其实就是利用价格时间序列存在系数为正的自相关关系的追涨杀跌策略。
CTA策略最大的优点是,不管当前市场是上涨还是下跌都能获得绝对收益,特别是在市场牛熊快速转换,或者行情走势明显流畅时,这种策略的优势是非常大的,总之就是有趋势在收益就有。但是如果市场处于震荡行情,或者趋势不明显时,这种策略就会可能买在高点卖在低点,不停的来回止损。
1.3 期货CTA策略盈利原理

期货CTA策略之所以能赚钱,主要是因为以下几点:

  • 价格走势存在反身性,它总是以趋势的方式不断延续。当投资者观察到价格上升的时候,就会跟风买入,结果造成价格进一步上升。价格下跌也是同样的道理。由于投资者更多的是非理性行为,所以有时候我们会看到,价格涨时涨得离谱,跌时跌的离谱。
  • 每一位投资者对盈利和亏损比例的容忍性是非对称的,对风险的承受能力也是不一样的。对于大多数散户来说,他们更倾向于选择更保守的顺势交易方法,市场也更容易走势趋势行情。
  • 价格的形成是由成交决定的,真是成交的背后又都是人来推动的,但人性是很难改变的,这就导致固定形态会反复出现的原因,策略在历史数据上回测有效,也就预示着将来可能也会有效。
另外趋势跟踪的交易特点是,在没有行情的时候亏小钱,当行情来的时候赚大钱,但是做过交易的人都知道,市场在大部分时间是出于震荡行情,只有在少量时间是趋势行情。所以趋势跟踪策略在交易时胜率较低,但是综合下来每一次交易的盈亏比较大。
由于趋势跟踪策略在收益上不稳定,所以很多投资机构会用多品种多策略构建一个投资组合,这中间也会配置一定量的反转策略。反转策略就是价格的时间序列存在系数为负的自相关关系,也就是高抛低吸。
CTA与传统资产的相关性




我们看上面的图,理论上多种风格不同或者相关性比较低的策略,在同时面对市场价格各种变化时,会做出时而相同时而不同的交易信号。由于多条收益曲线相互叠加,使得整体收益形成互补,收益曲线会变得更加平缓,从而减小了收益的波动性。
由上面的观点可以得出,与其开发一个大师级的策略,还不如开发多个中庸子策略,那么如何控制这些策略呢?这里我们可以借鉴机器学习中随机森林算法,随机森林并不是一个独立的算法,它是一个包含多棵决策树的决策框架。相当于决策树这个子策略之上的母策略。通过母策略组织和控制子策略集群。
接下来就需要设计一个母策略了,可以通过对全商品期货市场中各个品种的流动性、收益性和稳定性进行评估,筛选出收益具有低波动率的商品期货品种组合,再进行行业中性化筛选,通过组合的行业分散配置来进一步降低整体波动率,最后再通过市值匹配构建实际的商品期货多品种组合进行交易。
每个品种还可以多参数策略配置,可以选择回测表现良好附近的参数组合,当市场趋势明显时,多组参数策略通常会表现一致,相当于加仓;当市场处于震荡行情,多组参数策略通常会表现不一致,从而各自做多或做空进行风险对冲,相当于减仓。这样可以进一步降低投资组合的最大回测率,同时还可以保持整体收益率不变。
2、经典期货CTA策略案例

牛顿曾经说过:如果说我看得比别人更远些,那是因为我站在巨人的肩膀上。
市面上公开的CTA策略有均线策略、布林带策略、海龟交易法者、动量策略、套利策略等等。量化交易策略都有一个特点,那就是见光死,策略一旦被公开就会慢慢失效。但这并不影响我们学习这些策略,借鉴其中的精髓,这样才能站在巨人肩膀上看待问题。
2.1 期货基本面分析(库存、基差、价格)

基本面分析不需要关心短期价格走势,相信价值最终将反映在价格上,更多的是分析影响价格背后的因素,判断这个品种值多少钱。一般采用自上而下的分析方法:从宏观因素、品种因素以及其他因素。



我们看上面这张图,影响商品价格的因素有很多,林林总总多达数十项,往细了分更有几十项之多,并且这些数据是在不停变化的。单个散户想要获取这些庞大的数据已经是力所不及的事了,更不用提客观分析。
其实,商品期货的基本面分析,并不是把所有的因素都加以分析,我们只需要抓住基本面分析核心要素,就能剥丝抽茧从错综复杂的信息中找出规律。
宏观因素
宏观经济数据复杂多变,每天每时每刻,有很多的经济数据公布,各国政界、央行、投行,官方的和非官方的。除了政治和经济危机外,宏观分析是聊天的好材料,实用性不大。美国著名的基金管理专家彼得·林奇曾发表看法:“我每年花在经济大势上的分析时间不超出十五分钟”。
品种因素
在基本面分析中,品种分析主要是分析升水贴水、供需关系、商品库存、产业利润等等,可以说掌握商品期货品种因素分析,基本上能够判断大部分行情走势。
做过期货的朋友都知道,国内的商品期货可以简单划分为:工业品和农产品。工业品和农产品的分析方式是有所区别的,我们从供给和需求这两个方面进行阐述,在工业品中供给是相对稳定的,除非是有重大技术突破,否则产能是不太可能在短时间内有重大变化的,所以影响工业品价格的因素主要是需求。在农产品中需求是相对稳定的,长期来看农产品的需求存在变化,但短期来看农产品的需求趋于稳定的,所以影响农产品价格的因素主要是供给。
因此,根据经济学规律,最终决定商品价格的是供需关系,理论上只要能获取供给和需求的数据,就能判断商品未来的价格。对于工业品来说,供给的数据比较容易获取,但是很难获取到需求的数据,对于农产品来说,需求的数据比较容易获取,要想获取供给的数据就很难了。
其实我们还可以进一步做减法,供给与需求在经济市场中的相互结果就是库存,我们可以通过库存数据,来判断市场供给与需求的强弱关系。如果某个商品库存很高,说明市场供给的力量大于需求,在外在条件不变的前提下,商品价格即将下跌。如果某个商品库存很低,说明市场需求的力量大于供给,在外在条件不变的前提下,商品价格即将上涨。
除了分析商品库存外,还需要分析现货市场与期货市场的价格差,也就是所谓的基差。如果期货价格大于现货价格,我们称之为期货升水;如果期货价格小于现货价格,我们称之为期货贴水。根据期货交割制度,在期货交割日期,期货价格应该等于现货价格。



无论是升水还是贴水,由于期货交割制度上的约束,理论上交割日期货价格应该等于现货价格。随着交割日期的临近,现货价格与期货价格都会趋于一致,一种是期货向现货回归,另一种是现货向期货回归。
根据上面的原理,我们可以用库存和基差同时判断未来的期货价格。如果某个商品库存较低,并且如果期货价格比现货价格低很多,那么我们可以判断:现货市场需求的力量大于供给的力量,未来现货价格上涨的概率较大;又由于期货交割制度,随着交割日期的临近,期货价格将会补涨,与现货价格持平,未来期货价格上涨的概率更大。
最后,我们通过库存和基差判断了未来价格的大概率方向,但是并没有较为精确的买卖点,因此还需要配合技术分析,给出明确的进出场信号。整个基本面分析的架构就是:低库存+深度贴水+技术分析多头信号=做多;高库存+大幅升水+技术分析空头信号=做空。
2.2 海龟交易法则

提到交易策略,我们就不得不说一下具有代表性的海龟交易法则。海龟交易法则来自交易史上最著名的一次实验,商品投机家理查德·丹尼斯想弄清楚伟大的交易员是天生的还是后天培养的。为此,在1983年他招募了13个人,教授给他们期货交易的基本概念,以及他自己的交易方法和原则。这些学员被称为“海龟”。
在随后的四年中海龟们取得了年均复利80%的收益。 丹尼斯也证明了用一套简单的系统和法则,就可以使仅有很少或根本没有交易经验的人成为优秀的交易员。但是有个别海龟在网站上出售海龟交易法则牟利。为了阻止这种行为,两个原版海龟科蒂斯·费思和阿瑟·马多克,决定在网站上将海龟交易法则免费公之于众。
等真相大白之后,人们发现海龟交易法则采用的是优化后的唐奇安通道,并且使用ATR指标进行头寸管理。经历几十年的历史考验,成为普通散户也能轻松赚钱的交易方法,至今在某些品种上依然有效。
海龟核心原则

  • 掌握优势:找到一个期望值为正的交易策略,因为从长期来看,它能创造正的回报。
  • 管理风险:控制风险,守住阵地,否则你可能等不到创造成果的一天。
  • 坚定不移:唯有坚定不移地执行你的策略,你才能真正获得系统的成效。
  • 简单明了:从长久来看,简单的系统比复杂的系统更有生命力。
那么接下来,我们看下海龟交易法则到底讲了什么?
1、 市场----买卖什么,本质上是在哪些市场上进行交易,海龟们是期货交易者,他们只选择交易量大流动性高的市场,因为选择交易不活跃的市场,会增加进出场的额外滑价,还会错过很多趋势的机会。
2、 头寸规模----买卖多少是整个策略中非常重要的一部分,通常大部分人都会忽视或者错误对待这一点。海龟交易法则使用ATR,也就是平均真实波动幅度指标,来计算开仓头寸、加仓信号、止损信号。 这是一个非常巧妙的设计,本意是通过市场的绝对波动幅度来调整头寸规模,当市场波动性较强时,减少持仓量,当市场波动率较弱时,增加持仓量。它先定义了一个单位,这个单位的公式是:(总资产*1%)/ATR。初始仓位是1个单位,即便当日品种的跌幅达到ATR的水平,当日的损失都能控制在1%的总资产水平内。如果价格上涨了0.5个单位,多头就再加仓1个单位,最多加到4个单位。
3、 入市----海龟的入市借鉴了唐奇安通道,当价格升破前20或55根K线的最高价,就进场做多,当价格跌破前20或55根K线的最低价,就进场做空。信号出现时就进场交易,不等收盘或下根K线。
4、止损----长期来看,不会止损的交易是不会成功的,但大部分交易者都是抱着亏损的头寸,企图侥幸希望市场翻转。海龟严格规定了何时退出亏损的头寸,如果持有多单,并且价格下跌了2个单位,多头就止损平仓。如果持有空单,并且价格上涨了2个单位,空头就止损平仓。
5、止盈----海龟的止盈意味着损失很多浮盈,这也是很多交易者难以接受的部分。如果当前持有多单,并且价格跌破10日唐奇安通道下轨,就平掉所有的多单;如果当前持有空单,并且价格升破10日唐奇安通道上轨,就平掉所有空单。
由此我们可以看到,海龟交易法则看上去虽然很简单,但实际上它已经形成了正真意义上的交易系统雏形,它涵盖了一个完整的交易系统的各个方面,没有给交易员留下主观想象决策的余地,这正好使得程序化操作该系统的优势得到发挥。包括:进出场规则、资金管理和风控等等。
海龟交易法的最大优点是帮助我们建立一套行之有效的交易方法,它是一个结合了分批建仓、动态止盈止损以及对行情的趋势跟随策略,尤其是ATR值的使用以及头寸管理的理念,十分值得大家学习。当然它也有一个趋势跟踪策略共有的问题,就是浮盈回吐。追涨得到的浮盈,很有可能会由于随之而来的一波大跌而全部吐出。在大趋势中十分强劲,在震荡市中表现不如人意。
3、实战开发期货CTA策略

3.1 基于麦语言的CTA趋势策略开发

在上个世纪末,美国的金融投资领域开始流行一种很神奇的交易方法,在经过成千上万人的实践之后,人们发现这个方法存在有效性和巨大的实用价值,同时得到了很多投资专家和职业交易者的认同,直到现在也能够完美地应用于几乎所有金融投资领域,无论是外汇、黄金、股票、期货、原油,还是指数和债券,这就是混沌操作法。
混沌一词原指宇宙混乱状态的描述,其思想是:结果是必然的,但是由于现有知识无法计算出结果,因为计算本身也在改变结果,最后可能出现最大或最小的结果,而没有必然的结果。这与交易市场非常类似,参与者在分析市场,并买卖交易的时候也改变了市场。市场具有永恒变异性,当参与者了解到市场新形态后,市场同样也了解到它被参与者所认识,于是变异就发生了。并且它一定会趋向于参与者未知的方向去变异,它具有足够的智慧防止参与者捕捉到它的变化规律,也就是说,市场不具有稳定性,对市场过去的认识不能代表未来。
混沌操作法,是一整套完整的投资思想、交易策略和进出场信号,由比尔·威廉姆斯发明。目前国际上有很多投资者用混沌操作法参与市场交易,由于我国金融市场发展滞后,而混沌理论也是相对新潮的一种思想,所以国内研究混沌操作法的人也很少。鉴于混沌操作法是一个普适性非常高的交易策略,能够应用于几乎所有的金融投资领域,包括股票、债券、期货、外汇、数字货币,所以本节课程以简化版的混沌策略作为抛砖引玉,提高大家的投资兴趣和收益。
顾名思义,混沌操作法的理论基础就是混沌理论,混沌理论由气象学家 Edward Lorenz 提出,是20世纪末最伟大的科学发现之一。著名的“蝴蝶效应”就是他提出来的。 比尔威廉姆斯创造性地将混沌理论应用于金融投资领域,并结合分形几何学、非线性动力学等学科,创造出了一系列非常有效的技术分析指标。
整个混沌操作法是由五大维度(技术指标)构成的:

  • 鳄鱼线(Alligator)
  • 碎形(The Fractal)
  • 动量(The Momentum)
  • 加速(Acceleration)
  • 均衡线(The Balance Line)




我们看上面这张图,鳄鱼线就是运用分形几何学与非线性动力学的一组平衡线,其本质就是扩展指数加权移动平均线,属于均线的一种,只不过计算方法比普通均线稍微复杂一些。接下来,我们来看下如何用麦语言定义鳄鱼线:
// 参数
N1:=11;
N2:=21;

// 定义价格中线
N3:=N1+N2;
N4:=N2+N3;
HL:=(H+L)/2;

// 鳄鱼线
Y^^SMA(REF(HL,N3),N4,1);
R:=SMA(REF(HL,N2),N3,1);
G:=SMA(REF(HL,N1),N2,1);首先我们先定义2个外部参数N1和N2,然后根据外部参数计算出最高价与最低价的平均值HL,然后分别以不同的参数计算出HL的平均值,对于唇吻来说就是中线的小周期再次平均,牙齿就是中线的中周期再次平均,颚部就是中线的大周期再次平均。在这个策略中,我们使用的是颚部。
在混沌操作法中很形象的定义了一个分形的概念,我们可以打个比方:把手掌张开,手指朝上,中指就是上分形,左边的小指和无名指,右边的食指和拇指分别代表未创新高的K线。一个基本的分形就由这5根K线组成。那么可以用下面的代码定义分形:
// 分形
TOP_N:=BARSLAST(REF(H,2)=HHV(H,5))+2;
BOTTOM_N:=BARSLAST(REF(L,2)=LLV(L,5))+2;

TOP:=REF(H,TOP_N);
BOTTOM:=REF(L,BOTTOM_N);

MAX_YRG^^MAX(MAX(Y,R),G);
MIN_YRG^^MIN(MIN(Y,R),G);

TOP_FRACTAL^^VALUEWHEN(H>=MAX_YRG,TOP);
BOTTOM_FRACTAL^^VALUEWHEN(L<=MIN_YRG,BOTTOM);计算出鳄鱼线和分形,我们就可以根据这2个条件编写一个简单的混沌操作法策略了,以一组指数加权移动平均线作为鳄鱼线和分形指标计算的基准价格。当然原版的混沌操作法策略会更复杂些。代码如下:
// 如果当前无多单,并且收盘价升破上分形,并且上分形在鳄鱼线上方时,多头开仓
BKVOL=0 AND C>=TOP_FRACTAL AND TOP_FRACTAL>MAX_YRG,BPK(1);
// 如果当前无空单,并且收盘价跌破下分形,并且下分形在鳄鱼线下方时,空头开仓
SKVOL=0 AND C<=BOTTOM_FRACTAL AND BOTTOM_FRACTAL<MIN_YRG,SPK(1);

// 如果收盘价跌破鳄鱼的下巴时,多头平仓
C<Y,SP(BKVOL);
// 如果收盘价升破鳄鱼的下巴时,空头平仓
C>Y,BP(SKVOL);为了方便理解,我直接把详细的注释也写到代码里面了,我们可以把这个策略的交易逻辑简单列为以下几点:

  • 多头开仓:如果当前无多单,并且收盘价升破上分形,并且上分形在鳄鱼线上方。
  • 空头开仓:如果当前无空单,并且收盘价跌破下分形,并且下分形在鳄鱼线下方。
  • 多头平仓:如果收盘价跌破鳄鱼下巴。
  • 空头平仓:如果收盘价升破鳄鱼下巴。
接下来,我们来看下这个简单的混沌操作法策略回测的结果究竟是怎样的?为了将回测更接近于实盘环境,这里把手续费设置为交易所的2倍,开仓和平仓各加2跳的滑点。回测的数据品种为螺纹钢指数,交易品种为螺纹钢主力连续,固定1手开仓。以下是在1小时级别的初步回测绩效报告。







从资金曲线和回测绩效数据来看,该策略表现良好,整体资金曲线是稳步向上的。但是螺纹钢品种从2016年底之后,市场特性已经发生了改变,由之前高波动率单边走势转变为宽幅震荡走势。从资金曲线上看,2017年至今盈利明显乏力。
总之,混沌操作法的精髓就是找到转折点,而不需要关心市场怎么走,也不需要关心真假突破,如果突破分形就直接入场。永远不要试图去预测市场,而是做一个观察者和跟随者。
3.2 基于JavaScript语言的CTA套利策略开发

索罗斯在1987年撰写的《金融炼金术》 一书中,曾经提出过一个重要的命题:我相信市场价格在他们对未来有偏见的意义上总是错误的。他认为市场有效假说只是理论上的假设,实际上市场参与者并不总是理性的,并且在每一个时间点上,参与者不可能完全获取和客观解读所有的信息,再者就算是同样的信息,每个人的反馈都不尽相同。也就是说,价格本身就已经包含了市场参与者的错误预期,所以本质上市场价格总错误的。这或许是套利者的利润来源。
根据上述原理,我们也就知道,在一个非有效的期货市场中,不同时期交割合约之间受到市场影响也并不总是同步,其定价也并非完全有效的原因。那么,根据同一种交易标的的不同时期交割合约价格为基础,如果两个价格出现了较大的价差幅度,就可以同时买卖不同时期的期货合约,进行跨期套利。
与商品期货一样,数字货币也有与之相关的跨期套利合约组合。如在 OkEX 交易所中就有:ETC 当周、ETC 次周、ETC 季度。 举个例子,假设 ETC 当周和 ETC 季度的价差长期维持在 5 左右。如果某一天价差达到 7,我们预计价差会在未来某段时间回归到 5。那么就可以卖出 ETC 当周,同时买入 ETC 季度,来做空这个价差。反之亦然。
尽管这种价差是存在的,但是人工操作耗时、准确性差以及价格变化的影响,人工套利往往存在诸多不确定性。通过量化模型捕捉套利机会并制定套利交易策略,以及程序化算法自动向交易所下达交易订单,快速准确捕捉机会,高效稳定赚取收益,这就是量化套利的魅力所在。
本节课程将教大家如何在数字货币交易中,利用发明者量化交易平台和 OkEX 交易所中 ETC 期货合约,以一个简单的套利策略,来演示如果捕捉瞬时的套利机会,把握住每一次可以看得到的利润,同时对冲有可能遇到的风险。
创建一个数字货币跨期套利策略
难易度:普通级
策略环境

  • 交易标的:以太经典(ETC)
  • 价差数据:ETC 当周 - ETC 季度(省略协整性检验)
  • 交易周期:5 分钟
  • 头寸匹配:1:1
  • 交易类型:同品种跨期
策略逻辑

  • 做多价差开仓条件:如果当前账户没有持仓,并且价差小于 boll 下轨,就做多价差。即:买开 ETC 当周,卖开 ETC 季度。
  • 做空价差开仓条件:如果当前账户没有持仓,并且价差大于 boll 上轨,就做空价差。即:卖开 ETC 当周,买开 ETC 季度。
  • 做多价差平仓条件:如果当前账户持有 ETC 当周多单,并且持有 ETC 季度空单,并且价差大于 boll 中轨,就平多价差。即:卖平 ETC 当周,买平 ETC 季度。
  • 做空价差平仓条件:如果当前账户持有 ETC 当周空单,并且持有 ETC 季度多单,并且价差小于 boll 中轨,就平空价差。即:买平 ETC 当周,卖平 ETC 季度。
上面是一个简单的数字货币跨期套利策略逻辑描述,那么如何在程序中实现自己的想法呢?我们试着在发明者量化交易平台先把框架搭建起来。
function Data() {}  // 基础数据函数
Data.prototype.mp = function () {}  // 持仓函数
Data.prototype.boll = function () {}  // 指标函数
Data.prototype.trade = function () {}  // 下单函数
Data.prototype.cancelOrders = function () {}  // 撤单函数
Data.prototype.isEven = function () {}  // 处理单只合约函数
Data.prototype.drawingChart = function () {}  // 画图函数

function onTick() {
    var data = new Data(tradeTypeA, tradeTypeB);  // 创建一个基础数据对象
    var accountStocks = data.accountData.Stocks;  // 账户余额
    var boll = data.boll(dataLength, timeCycle);  // 计算boll技术指标
    data.trade();  // 计算交易条件下单
    data.cancelOrders();  // 撤单
    data.drawingChart(boll);  // 画图
    data.isEven();  // 处理持有单个合约
}

//入口函数
function main() {
    while (true) {  // 进入轮询模式
        onTick();  // 执行onTick函数
        Sleep(500);  // 休眠0.5秒
    }
}想象一下,我们在主管交易中的交易流程是怎样的?在系统交易中并没有本质上的区别,无非就是:获取数据、计算数据、下单交易、下单之后的处理。那么在程序中也是如此,首先程序会先执行第20行main函数,这是一个约定俗成的规定,当程序执行完交易策略预处理后(如果有的话)就会进入无限循环模式,也就是轮询模式,在轮询模式中,会重复的执行onTick函数。
那么在onTick函数中,就是我们在主观交易中的交易流程:首先获取基础价格数据,然后获取账户余额,接着计算指标,之后开始计算交易条件并下单,最后就是下单之后的处理,包括:撤单、画图、处理单个合约。
对照着策略思路以及交易流程,可以很轻松把策略框架搭建起来。整个策略可以简化为三个步骤:

  • 交易前预处理。
  • 获取并计算数据。
  • 下单并对后续处理。
交易策略框架搭建完之后,就需要根据实际交易流程和交易细节,在策略框架里面填充必要的细节代码。
一、 交易前预处理
1. 声明必要的全局变量

  • 声明一个配置图表的 chart 对象
    var chart = {}
  • 调用 Chart 函数,初始化图表
    var ObjChart = Chart ( chart )
  • 声明一个空数组,用来存储价差序列
    var bars = []
  • 声明一个记录历史数据时间戳变量
    var oldTime = 0
2. 配置策略的外部参数
var tradeTypeA = "this_week"; // 套利A合约
var tradeTypeB = "quarter"; // 套利B合约
var dataLength = 10; //指标周期长度
var timeCycle = 1; // K线周期
var name = "ETC"; // 币种
var unit = 1; // 下单量3. 定义数据处理函数

  • 基础数据函数:Data ( )
    创建一个构造函数 Data,并定义它的内部属性。包括:账户数据、持仓数据、K线数据时间戳、套利A/B合约的买/卖一价、正/反套价差。
function Data(tradeTypeA, tradeTypeB) { // 传入套利A合约和套利B合约
    this.accountData = _C(exchange.GetAccount); // 获取账户信息
    this.positionData = _C(exchange.GetPosition); // 获取持仓信息
    var recordsData = _C(exchange.GetRecords); //获取K线数据
    exchange.SetContractType(tradeTypeA); // 订阅套利A合约
    var depthDataA = _C(exchange.GetDepth); // 套利A合约深度数据
    exchange.SetContractType(tradeTypeB); // 订阅套利B合约
    var depthDataB = _C(exchange.GetDepth); // 套利B合约深度数据
    this.time = recordsData[recordsData.length - 1].Time; // 获取最新数据时间
    this.askA = depthDataA.Asks[0].Price; // 套利A合约卖一价
    this.bidA = depthDataA.Bids[0].Price; // 套利A合约买一价
    this.askB = depthDataB.Asks[0].Price; // 套利B合约卖一价
    this.bidB = depthDataB.Bids[0].Price; // 套利B合约买一价
    // 正套价差(合约A卖一价 - 合约B买一价)
    this.basb = depthDataA.Asks[0].Price - depthDataB.Bids[0].Price;
    // 反套价差(合约A买一价 - 合约B卖一价)
    this.sabb = depthDataA.Bids[0].Price - depthDataB.Asks[0].Price;
}

  • 获取持仓函数:mp ( )
    遍历整个持仓数组,返回指定合约、指定方向的持仓数量,如果没有就返回 false
Data.prototype.mp = function (tradeType, type) {
    var positionData = this.positionData; // 获取持仓信息
    for (var i = 0; i < positionData.length; i++) {
        if (positionData.ContractType == tradeType) {
            if (positionData.Type == type) {
                if (positionData.Amount > 0) {
                    return positionData.Amount;
                }
            }
        }
    }
    return false;
}

  • K线和指标函数:boll ( )
    根据正/反套价差数据,合成新的K线序列。并返回由boll指标计算的上轨、中轨、下轨数据。
Data.prototype.boll = function (num, timeCycle) {
    var self = {}; // 临时对象
    // 正套价差和反套价差中间值
    self.Close = (this.basb + this.sabb) / 2;
    if (this.timeA == this.timeB) {
        self.Time = this.time;
    } // 对比两个深度数据时间戳
    if (this.time - oldTime > timeCycle * 60000) {
        bars.push(self);
        oldTime = this.time;
    } // 根据指定时间周期,在K线数组里面传入价差数据对象
    if (bars.length > num * 2) {
        bars.shift(); // 控制K线数组长度
    } else {
        return;
    }
    var boll = TA.BOLL(bars, num, 2); // 调用talib库中的boll指标
    return {
        up: boll[0][boll[0].length - 1], // boll指标上轨
        middle: boll[1][boll[1].length - 1], // boll指标中轨
        down: boll[2][boll[2].length - 1] // boll指标下轨
    } // 返回一个处理好的boll指标数据
}

  • 下单函数:trade ( )
    传入下单合约名称和下单类型,然后以对价下单,并返回下单后的结果。由于需要同时下两个不同方向的单子,所以在函数内部根据下单合约名称对买/卖一价做了转换。
Data.prototype.trade = function (tradeType, type) {
    exchange.SetContractType(tradeType); // 下单前先重新订阅合约
    var askPrice, bidPrice;
    if (tradeType == tradeTypeA) { // 如果是A合约下单
        askPrice = this.askA; // 设置askPrice
        bidPrice = this.bidA; // 设置bidPrice
    } else if (tradeType == tradeTypeB) { // 如果是B合约下单
        askPrice = this.askB; // 设置askPrice
        bidPrice = this.bidB; // 设置bidPrice
    }
    switch (type) { // 匹配下单模式
        case "buy":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Buy(askPrice, unit);
        case "sell":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Sell(bidPrice, unit);
        case "closebuy":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Sell(bidPrice, unit);
        case "closesell":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Buy(askPrice, unit);
        default:
            return false;
    }
}

  • 取消订单函数:cancelOrders ( )
    获取所有未成交订单数组,并逐个取消。并且如果有未成交的订单就返回false,如果没有未成交的订单就返回true。
Data.prototype.cancelOrders = function () {
    Sleep(500); // 撤单前先延时,因为有些交易所你懂的
    var orders = _C(exchange.GetOrders); // 获取未成交订单数组
    if (orders.length > 0) { // 如果有未成交的订单
        for (var i = 0; i < orders.length; i++) { //遍历未成交订单数组
            exchange.CancelOrder(orders.Id); //逐个取消未成交的订单
            Sleep(500); //延时0.5秒
        }
        return false; // 如果取消了未成交的单子就返回false
    }
    return true; //如果没有未成交的订单就返回true
}

  • 处理持有单个合约:isEven ( )
    在处理套利交易中出现单腿情况,这里直接用简单的平掉所有仓位处理。当然,也可以改为追单方式。
Data.prototype.isEven = function () {
    var positionData = this.positionData; // 获取持仓信息
    var type = null; // 转换持仓方向
    // 如果持仓数组长度余2不等于0或者持仓数组长度不等于2
    if (positionData.length % 2 != 0 || positionData.length != 2) {
        for (var i = 0; i < positionData.length; i++) { // 遍历持仓数组
            if (positionData.Type == 0) { // 如果是多单
                type = 10; // 设置下单参数
            } else if (positionData.Type == 1) { // 如果是空单
                type = -10; // 设置下单参数
            }
            // 平掉所有仓位
            this.trade(positionData.ContractType, type, positionData.Amount);
        }
    }
}

  • 画图函数:drawingChart ( )
    调用 ObjChart.add ( ) 方法,在图表中画出必要的行情数据和指标数据:上轨、中轨、下轨、正/反套价差。
Data.prototype.drawingChart = function (boll) {
    var nowTime = new Date().getTime();
    ObjChart.add([0, [nowTime, boll.up]]);
    ObjChart.add([1, [nowTime, boll.middle]]);
    ObjChart.add([2, [nowTime, boll.down]]);
    ObjChart.add([3, [nowTime, this.basb]]);
    ObjChart.add([4, [nowTime, this.sabb]]);
    ObjChart.update(chart);
}4. 在入口函数 main ( ) 里面,执行交易前预处理代码,这些代码在程序启动后,只运行一次。包括:

  • 过滤控制台中不是很重要的信息 SetErrorFilter ( )
  • 设置要交易的数字货币币种 exchange.IO ( )
  • 程序启动前清空之前绘制的图表 ObjChart.reset ( )
  • 程序启动前清空之前的状态栏信息 LogProfitReset ( )
定义完上述的交易前预处理,紧接着就要进入下一个步骤,进入轮询模式,重复执行 onTick ( ) 函数。 并设置 Sleep ( ) 轮询时的休眠时间,因为部分数字货币交易所的 API 对一定时间内内置了访问次数限制。
function main() {
    // 过滤控制台中不是很重要的信息
    SetErrorFilter("429|GetRecords:|GetOrders:|GetDepth:|GetAccount|:Buy|Sell|timeout|Futures_OP");
    exchange.IO("currency", name + '_USDT'); //设置要交易的数字货币币种
    ObjChart.reset(); //程序启动前清空之前绘制的图表
    LogProfitReset(); //程序启动前清空之前的状态栏信息
    while (true) { // 进入轮询模式
        onTick(); // 执行onTick函数
        Sleep(500); // 休眠0.5秒
    }
}二、 获取并计算数据

  • 获取基础数据对象、账户余额、boll 指标数据,以供交易逻辑使用。
function onTick() {
    var data = new Data(tradeTypeA, tradeTypeB); // 创建一个基础数据对象
    var accountStocks = data.accountData.Stocks; // 账户余额
    var boll = data.boll(dataLength, timeCycle); // 获取boll指标数据
    if (!boll) return; // 如果没有boll数据就返回
}三、 下单并对后续处理

  • 根据上述的策略逻辑,执行买卖操作。首先会判断价格和指标条件是否成立,然后再判断持仓条件是否成立,最后执行 trade ( ) 下单函数。
// 价差说明
// basb = (合约A卖一价 - 合约B买一价)
// sabb = (合约A买一价 - 合约B卖一价)
if (data.sabb > boll.middle && data.sabb < boll.up) { // 如果sabb高于中轨
    if (data.mp(tradeTypeA, 0)) { // 下单前检测合约A是否有多单
        data.trade(tradeTypeA, "closebuy"); // 合约A平多
    }
    if (data.mp(tradeTypeB, 1)) { // 下单前检测合约B是否有空单
        data.trade(tradeTypeB, "closesell"); // 合约B平空
    }
} else if (data.basb < boll.middle && data.basb > boll.down) { // 如果basb低于中轨
    if (data.mp(tradeTypeA, 1)) { // 下单前检测合约A是否有空单
        data.trade(tradeTypeA, "closesell"); // 合约A平空
    }
    if (data.mp(tradeTypeB, 0)) { // 下单前检测合约B是否有多单
        data.trade(tradeTypeB, "closebuy"); // 合约B平多
    }
}
if (accountStocks * Math.max(data.askA, data.askB) > 1) { // 如果账户有余额
    if (data.basb < boll.down) { // 如果basb价差低于下轨
        if (!data.mp(tradeTypeA, 0)) { // 下单前检测合约A是否有多单
            data.trade(tradeTypeA, "buy"); // 合约A开多
        }
        if (!data.mp(tradeTypeB, 1)) { // 下单前检测合约B是否有空单
            data.trade(tradeTypeB, "sell"); // 合约B开空
        }
    } else if (data.sabb > boll.up) { // 如果sabb价差高于上轨
        if (!data.mp(tradeTypeA, 1)) { // 下单前检测合约A是否有空单
            data.trade(tradeTypeA, "sell"); // 合约A开空
        }
        if (!data.mp(tradeTypeB, 0)) { // 下单前检测合约B是否有多单
            data.trade(tradeTypeB, "buy"); // 合约B开多
        }
    }
}

  • 下单完成后,需要对未成交的订单、持有单个合约等非正常情况做处理。以及绘制图表。
data.cancelOrders(); // 撤单
data.drawingChart(boll); // 画图
data.isEven(); // 处理持有单个合约以上,我们通过 200 多行,就把一个简单的数字货币跨期套利策略完完整整的创建出来。完整的代码如下:
// 全局变量
// 声明一个配置图表的 chart 对象
var chart = {
    __isStock: true,
    tooltip: {
        xDateFormat: '%Y-%m-%d %H:%M:%S, %A'
    },
    title: {
        text: '交易盈亏曲线图(详细)'
    },
    rangeSelector: {
        buttons: [{
            type: 'hour',
            count: 1,
            text: '1h'
        }, {
            type: 'hour',
            count: 2,
            text: '3h'
        }, {
            type: 'hour',
            count: 8,
            text: '8h'
        }, {
            type: 'all',
            text: 'All'
        }],
        selected: 0,
        inputEnabled: false
    },
    xAxis: {
        type: 'datetime'
    },
    yAxis: {
        title: {
            text: '价差'
        },
        opposite: false,
    },
    series: [{
        name: "上轨",
        id: "线1,up",
        data: []
    }, {
        name: "中轨",
        id: "线2,middle",
        data: []
    }, {
        name: "下轨",
        id: "线3,down",
        data: []
    }, {
        name: "basb",
        id: "线4,basb",
        data: []
    }, {
        name: "sabb",
        id: "线5,sabb",
        data: []
    }]
};
var ObjChart = Chart(chart); // 画图对象
var bars = []; // 存储价差序列
var oldTime = 0; // 记录历史数据时间戳

// 参数
var tradeTypeA = "this_week"; // 套利A合约
var tradeTypeB = "quarter"; // 套利B合约
var dataLength = 10; //指标周期长度
var timeCycle = 1; // K线周期
var name = "ETC"; // 币种
var unit = 1; // 下单量

// 基础数据
function Data(tradeTypeA, tradeTypeB) { // 传入套利A合约和套利B合约
    this.accountData = _C(exchange.GetAccount); // 获取账户信息
    this.positionData = _C(exchange.GetPosition); // 获取持仓信息
    var recordsData = _C(exchange.GetRecords); //获取K线数据
    exchange.SetContractType(tradeTypeA); // 订阅套利A合约
    var depthDataA = _C(exchange.GetDepth); // 套利A合约深度数据
    exchange.SetContractType(tradeTypeB); // 订阅套利B合约
    var depthDataB = _C(exchange.GetDepth); // 套利B合约深度数据
    this.time = recordsData[recordsData.length - 1].Time; // 获取最新数据时间
    this.askA = depthDataA.Asks[0].Price; // 套利A合约卖一价
    this.bidA = depthDataA.Bids[0].Price; // 套利A合约买一价
    this.askB = depthDataB.Asks[0].Price; // 套利B合约卖一价
    this.bidB = depthDataB.Bids[0].Price; // 套利B合约买一价
    // 正套价差(合约A卖一价 - 合约B买一价)
    this.basb = depthDataA.Asks[0].Price - depthDataB.Bids[0].Price;
    // 反套价差(合约A买一价 - 合约B卖一价)
    this.sabb = depthDataA.Bids[0].Price - depthDataB.Asks[0].Price;
}

// 获取持仓
Data.prototype.mp = function (tradeType, type) {
    var positionData = this.positionData; // 获取持仓信息
    for (var i = 0; i < positionData.length; i++) {
        if (positionData.ContractType == tradeType) {
            if (positionData.Type == type) {
                if (positionData.Amount > 0) {
                    return positionData.Amount;
                }
            }
        }
    }
    return false;
}

// 合成新K线数据和boll指标数据
Data.prototype.boll = function (num, timeCycle) {
    var self = {}; // 临时对象
    // 正套价差和反套价差中间值
    self.Close = (this.basb + this.sabb) / 2;
    if (this.timeA == this.timeB) {
        self.Time = this.time;
    } // 对比两个深度数据时间戳
    if (this.time - oldTime > timeCycle * 60000) {
        bars.push(self);
        oldTime = this.time;
    } // 根据指定时间周期,在K线数组里面传入价差数据对象
    if (bars.length > num * 2) {
        bars.shift(); // 控制K线数组长度
    } else {
        return;
    }
    var boll = TA.BOLL(bars, num, 2); // 调用talib库中的boll指标
    return {
        up: boll[0][boll[0].length - 1], // boll指标上轨
        middle: boll[1][boll[1].length - 1], // boll指标中轨
        down: boll[2][boll[2].length - 1] // boll指标下轨
    } // 返回一个处理好的boll指标数据
}

// 下单
Data.prototype.trade = function (tradeType, type) {
    exchange.SetContractType(tradeType); // 下单前先重新订阅合约
    var askPrice, bidPrice;
    if (tradeType == tradeTypeA) { // 如果是A合约下单
        askPrice = this.askA; // 设置askPrice
        bidPrice = this.bidA; // 设置bidPrice
    } else if (tradeType == tradeTypeB) { // 如果是B合约下单
        askPrice = this.askB; // 设置askPrice
        bidPrice = this.bidB; // 设置bidPrice
    }
    switch (type) { // 匹配下单模式
        case "buy":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Buy(askPrice, unit);
        case "sell":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Sell(bidPrice, unit);
        case "closebuy":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Sell(bidPrice, unit);
        case "closesell":
            exchange.SetDirection(type); // 设置下单模式
            return exchange.Buy(askPrice, unit);
        default:
            return false;
    }
}

// 取消订单
Data.prototype.cancelOrders = function () {
    Sleep(500); // 撤单前先延时,因为有些交易所你懂的
    var orders = _C(exchange.GetOrders); // 获取未成交订单数组
    if (orders.length > 0) { // 如果有未成交的订单
        for (var i = 0; i < orders.length; i++) { //遍历未成交订单数组
            exchange.CancelOrder(orders.Id); //逐个取消未成交的订单
            Sleep(500); //延时0.5秒
        }
        return false; // 如果取消了未成交的单子就返回false
    }
    return true; //如果没有未成交的订单就返回true
}

// 处理持有单个合约
Data.prototype.isEven = function () {
    var positionData = this.positionData; // 获取持仓信息
    var type = null; // 转换持仓方向
    // 如果持仓数组长度余2不等于0或者持仓数组长度不等于2
    if (positionData.length % 2 != 0 || positionData.length != 2) {
        for (var i = 0; i < positionData.length; i++) { // 遍历持仓数组
            if (positionData.Type == 0) { // 如果是多单
                type = 10; // 设置下单参数
            } else if (positionData.Type == 1) { // 如果是空单
                type = -10; // 设置下单参数
            }
            // 平掉所有仓位
            this.trade(positionData.ContractType, type, positionData.Amount);
        }
    }
}

// 画图
Data.prototype.drawingChart = function (boll) {
    var nowTime = new Date().getTime();
    ObjChart.add([0, [nowTime, boll.up]]);
    ObjChart.add([1, [nowTime, boll.middle]]);
    ObjChart.add([2, [nowTime, boll.down]]);
    ObjChart.add([3, [nowTime, this.basb]]);
    ObjChart.add([4, [nowTime, this.sabb]]);
    ObjChart.update(chart);
}

// 交易条件
function onTick() {
    var data = new Data(tradeTypeA, tradeTypeB); // 创建一个基础数据对象
    var accountStocks = data.accountData.Stocks; // 账户余额
    var boll = data.boll(dataLength, timeCycle); // 获取boll指标数据
    if (!boll) return; // 如果没有boll数据就返回
    // 价差说明
    // basb = (合约A卖一价 - 合约B买一价)
    // sabb = (合约A买一价 - 合约B卖一价)
    if (data.sabb > boll.middle && data.sabb < boll.up) { // 如果sabb高于中轨
        if (data.mp(tradeTypeA, 0)) { // 下单前检测合约A是否有多单
            data.trade(tradeTypeA, "closebuy"); // 合约A平多
        }
        if (data.mp(tradeTypeB, 1)) { // 下单前检测合约B是否有空单
            data.trade(tradeTypeB, "closesell"); // 合约B平空
        }
    } else if (data.basb < boll.middle && data.basb > boll.down) { // 如果basb低于中轨
        if (data.mp(tradeTypeA, 1)) { // 下单前检测合约A是否有空单
            data.trade(tradeTypeA, "closesell"); // 合约A平空
        }
        if (data.mp(tradeTypeB, 0)) { // 下单前检测合约B是否有多单
            data.trade(tradeTypeB, "closebuy"); // 合约B平多
        }
    }
    if (accountStocks * Math.max(data.askA, data.askB) > 1) { // 如果账户有余额
        if (data.basb < boll.down) { // 如果basb价差低于下轨
            if (!data.mp(tradeTypeA, 0)) { // 下单前检测合约A是否有多单
                data.trade(tradeTypeA, "buy"); // 合约A开多
            }
            if (!data.mp(tradeTypeB, 1)) { // 下单前检测合约B是否有空单
                data.trade(tradeTypeB, "sell"); // 合约B开空
            }
        } else if (data.sabb > boll.up) { // 如果sabb价差高于上轨
            if (!data.mp(tradeTypeA, 1)) { // 下单前检测合约A是否有空单
                data.trade(tradeTypeA, "sell"); // 合约A开空
            }
            if (!data.mp(tradeTypeB, 0)) { // 下单前检测合约B是否有多单
                data.trade(tradeTypeB, "buy"); // 合约B开多
            }
        }
    }
    data.cancelOrders(); // 撤单
    data.drawingChart(boll); // 画图
    data.isEven(); // 处理持有单个合约
}

//入口函数
function main() {
    // 过滤控制台中不是很重要的信息
    SetErrorFilter("429|GetRecords:|GetOrders:|GetDepth:|GetAccount|:Buy|Sell|timeout|Futures_OP");
    exchange.IO("currency", name + '_USDT'); //设置要交易的数字货币币种
    ObjChart.reset(); //程序启动前清空之前绘制的图表
    LogProfitReset(); //程序启动前清空之前的状态栏信息
    while (true) { // 进入轮询模式
        onTick(); // 执行onTick函数
        Sleep(500); // 休眠0.5秒
    }
}套利交易起源于摩根士丹利的股票交易策略,其理念是:两个高度相关的品种,它们的价差波动符合“爆米花过程”,即价差不断从偏离历史均值的位置回归到均值,然后又从均值进行再一次偏离。
由此我们就可以对价差进行低买高卖获取利润,那么布林带根据统计学中的标准差原理,由一根中轨和用标准差计算出的上轨和下轨,形成的三条网状带,在价差套利交易中非常实用。
经过测试,按照该策略进行操作,在不考虑手续费以及冲击成本情况下,虽然每次收益并不是很高,但整体收益相对稳定。需要注意的是,由于属于统计套利,因此价差存在反向扩大的风险,我们在设计时必须考虑止损问题。 其次,还需要关注冲击成本,当参与交易的两个合约流动性缩小时,会对收益产生很大影响,投资者应当酌情规避。
4、CTA策略开发进阶迭代

4.1 避免期货CTA策略的陷阱

在上两节课中,我们用麦语言写了一个趋势策略,用JavaScript写了一个套利策略,在策略回测上并没有看出什么问题。但是量化交易并不是写个程序,回测没问题就可以直接实盘了。
实际上回测只是对策略的一种模拟,仅仅用来评估这个策略在历史数据中的表现,能够让交易者快速评估以及抛弃一些交易策略。
很多情况下,在回测中看起来很棒的策略,在实盘中往往达不到回测的标准,理由有很多,其中有一部分超出了交易者的控制能力,但有些失败是因为常见的、或者潜在的错误导致的。
静态数据与动态数据
我们做量化首先要有一个静态数据和动态数据的概念,在回测中,我们用的都是静态的历史数据,开高低收每根K线的价格都是完整的,每一个交易信号都可以100%成交。但是在实盘中的数据是动态的。举个例子,最高价大于开盘1小时内的最高价就买入,但如果当前的K线还没有走完,那么最高价就是动态的,交易信号就可能会来回闪烁。出现这种情况,说明策略在判断买卖交易的条件中使用了未来函数。
未来函数
什么是未来函数?我们先来看下百度百科是怎么解释的:某一量依赖另一量,如量 A和量B,B变化使A改变,那么A是B的函数,如果B是稍后的量,A是稍早的量,A跟着B变,A是B的未来函数。大家可能会看的一头雾水。
通俗点说就是引用未来数据的函数,比如用明天的价格预测明天的价格。如果一个技术指标包含未来函数,那么它的信号是不确定的,常常是当前发出交易信号,等下根K线出现的时候,这个信号消失或者改变了位置。
收盘价就是一个未来函数,在最新的K线走完之前,收盘价一直是在变动的,你必须等到K线走完才能确定收盘价。那既然收盘价本身就是未来函数,那么所有基于收盘价的技术指标也都是未来函数。
所以,如果一个技术指标,使用已经确认的收盘价作为基础数据,无论过去了多久,买卖信号都不会改变,就可以说这个技术指标没有引用未来函数。但是它使用的基础数据是还没有确认的收盘价,那么这个技术指标就引用了未来函数,在实际应用中买卖的信号就可能会发生改变。
过去的价格
未来函数是用到了未来的价格,那也有可能会相反用到过去的价格,这点也是很多新手容易忽视的一个问题。未来更好的说明这个问题,我们还是举个例子:如果当前最高价大于开盘后1小时内的最高价,就以开盘价买入。很明显这个买卖信号的条件没有什么问题,但是下单的价格却使用了过去的价格。
在回测中,策略是正常的,因为基于静态数据的回测引擎,只有有买入信号,就能100%成交,但是在实盘中,当最高价大于开盘后1个小时内的最高价时,肯定就不能再用过去的价格开盘价去发单了。
价格真空
所谓的价格真空就是指在K线图上显示的价格,但是在实盘中不能成交的价格,主要分为下面几种情况:

  • 做过交易的人都知道,在价格涨停的时候是很难买入的,在价格跌停的时候是很难卖出的。但是在回测中是可以成交的。
  • 交易所的撮合机制是:价格优先、时间优先。有些品种盘口会经常有巨量挂单,实盘时如果你是挂单买卖,那么你必须排到别人的挂单后面,等之前别人的挂单成交后,你才能成交,甚至来不及成交价格就已经变了。但是在回测的时候,如果你的策略是挂单交易的,那么会及时成交,这就与真实的实盘环境不一样了。
  • 如果你用的是套利类策略,那么回测利润是很高的,因为回测时每次都已经假设了抢到了这些价差。真实的情况下,很多价差都抢不到,或者只抢到了一条腿,一般来说肯定是不利于你的方向的那条先成交,那么就需要马上去补另一条腿,这时候滑点已经不是1、2个点了,而套利策略本身就赚这几个点的价差,这种情况是回测中无法模拟的。真实利润完全不如回测。
  • 黑天鹅事件虽然不常用,但是对量化交易的影响还是很大的,如下面这张图,在外汇瑞郎黑天鹅事件中,从图表上看开高低收都有,其实在当天极端行情中,中间的价格是真空,大量的止损单,造成踩踏事件,流动性为零,成交难度非常大,但是在回测中却能止损。



过度拟合
过度拟合是量化交易初学者经常犯的一个错误,那什么是过度拟合呢,举一个简单的例子:上学考试的时候,有的人采用的是题海战术,把每个题背下来。在考试的时候题目稍微变换一下,他就不会做了。因为他非常复杂的记住了每道题的做法,但是没有抽象出通用的规则。




就像上面这张图,一个模型只要足够复杂,那么就可以完美的适应数据。在量化交易中的过度拟合也是这个道理,如果你的策略本身很复杂,又有很多外置的参数,那么在有限的历史数据回测中,总会有一个或几个参数能完美的拟合历史行情。
但是,在以后的实盘中,价格的变化可能超出你的策略局限,实际上量化交易策略开发的本质就是从大量貌似随机的数据中匹配局部非随机数据的过程,因此就需要我们借助统计学知识来避开这个陷阱,怎么做呢?
折中的解决办法是:使用样本内和样本外数据。把整个数据分成两份,样本内数据作为训练集,负责数据回测。样本外数据作为测试集,负责验证。如果历史数据很少,还可以采用交叉测试的方法。
如果发现样本外数据表现不好,又觉得丢掉模型太可惜或者不愿意承认自己这个模型不行,而对着样本外数据继续做模型优化,直到样本外数据上也表现得一样好,那最后受伤的一定是你的真金白银。
幸存者偏差
幸存者偏差可以通过下面几个例子来解释:
1、 站在风口,猪都会飞。
2、 网上卖降落伞的都是好评,因为降落伞有问题的人不存在了。
3、 记者在车上采访是否买到车票,因为买不到车票的人根本上不了车。
4、 媒体宣传彩票可以中大奖,因为媒体不会主动宣传没有中奖的人。
上面的例子我们可以发现,通常人们接受到的信息其实是已经经过筛选之后的,这使得大量的数据或样本被选择性的忽视了,产生的结果就是基于幸存者偏差的结论已经偏离了实时。那么在量化交易中,我们也要主要回测的结果是否属于运气的成分,很多情况下回测的结果可能是在整个回测中表现较好的一次,注意看下面这张图:




左边这张图是一个非常棒的交易策略,资金曲线良好,没有大幅的回撤,可以获得稳定的利润回报。但是请看右边这张图,它只是这几百次交易回测中表现最好的一个而已。反过来我们在看金融市场中,始终是明星多寿星少,如果交易者的策略与市场行情契合,那么每年的行情都能造就一批批明星,但是你很难见到连续3年以上都能稳定盈利的寿星。
成本冲击
除非你是挂单交易,否则你在交易的时候都可能有滑价。在交易活跃的品种上,买一价和卖一价通常是一个点差,在交易不活跃的品种上,点差可能会更大。每次你想主动成交就需要至少一个点差,甚至更多。但是在回测中,我们不需要考虑成交问题,只要有信号就可以成交,所以为了模拟真实的交易环境,必须加上至少一个滑价。
尤其是交易频率比较高的策略,在策略回测的时候,如果不加上滑价,资金曲线是一直倾斜向上的,一旦加上合理的滑价就立马变为亏损。另外,造成这种现象不止是点差的问题,在真实的交易环境中,还需要考虑:网络延迟、软硬件系统、服务器响应等问题。
策略容量
同样的策略在高效市场和低效市场会有截然不同的差别,甚至完全相反。例如在国内股市、商品期货、国外数字货币等低效市场中,由于交易量的基数小,使得高频策略本身的容量就不是很大,用的人多了就没有利润空间了,甚至本来之前是盈利的策略变得亏损。但是在高效的外汇市场上,可以容纳很多不同种类的高频策略。
以上就是在策略开发中和使用中,可能出现的问题和陷阱,对于一个有经验的交易系统开发者来说,回测是必须要做的。因为它能告诉你一个策略的想法在历史交易中是否能被验证有效。但是很多时候回测并不代表未来能盈利。因为回测里面有太多坑了,不用钱买点教训,你是不会明白的。而这些教训都是用真金白银堆出来的。这节课程至少能让你少走很多量化的弯路和陷阱。
4.2 建立最佳的头寸管理

在《股票做手回忆录》中,有一个很有意思的桥段:和主人公利弗莫尔在同一个证券公司的老火鸡(原名帕特里奇)总是大手笔买卖交易,每当别人建议他获利后先卖掉,等股票价格回调后再买入。老火鸡总是语重心长的说:不,你知道的,这是牛市!
就连利弗莫尔最后都感慨:看对走势并没有什么了不起的,在市场中总能找到很多在牛市早就看涨的人,在熊市早就看跌的人。但他们总是善于跟市场讨价还价,试图买在最低点,卖在高点。就像老火鸡一样,真正赚大钱的,正是那些看对市场,又紧握头寸的人,这也是最难学的。这不仅面临选择标的、选择时机,还面临一个更重要的问题:究竟应该持有(承担)多大头寸(风险)?
失败的交易者都有一个片面思维,在交易的时候,贪婪的人只看收益不看风险,胆小的人只看风险不看收益,既贪婪又胆小的人在上涨时忘了风险,在下跌时忘了收益。但是成功的交易者会同时考虑风险和收益,也就是说每赚一块钱承担了几块钱的风险。那么衡量收益和风险的指标就是收益风险比。
很多人都知道利润有多大风险就有多大,即:收益与风险成正比。在部分人看来,收益和风险的关系应该是下面这个样子,横轴是风险百分百,纵轴是利润百分比:




但是在实际交易中,收益与风险远不是两点一线这么简单,至少它并不总是呈线性运动。真实的风险是在预期收益下,承担亏损的最大幅度,也就是我们所说的最大波动率。虽然有时候从交易的结果看,最大浮动亏损并不一定等于平仓亏损,但最大浮动亏损却是真实存在的。
由此,我们可以知道上图中收益与风险比并不是真实的表现,在真实的交易环境中,收益与风险比应该是下图中的样子:




我们看上面这张图,黄色的曲线是净值在不同风险下的波动情况,随着预期收益的不断扩大,风险也在逐渐扩大。如果我们把破产先设置在0.5,也就是最大亏损达到50%的幅度,那么这就是一个失败的交易策略。尽管从结果上看策略这个策略的最终收益是正的,但实际上中间早已破产。
哪怕你的策略是一个正期望的策略,在错误的头寸管理下,一样会爆仓。所以从这个角度讲,买卖多少比什么时候买卖更为重要,如何科学管理头寸,在金融交易中也就成了根本问题。那么在试图解决这个问题之前,我们来看在赌博中是如何科学下注的。




我们以抛硬币为例,假设一枚硬币的两面是一样重的,如果出现正面盈利2元,出现反面亏损1元,很明显这是一个正期望的游戏,胜率是50%,赔了是2。问题来了:现在你有100元,那么怎么重复下注,可以使100元以最快的速度达到100万元。
如果不进行严谨的思考,我们会觉得既然每次赌的收益是50%*2-50%*1,也就是50%,那么为了快速实现最大收益,应该在每次赌博中尽量投入跟多的本金,这个下注必应应该是100%才对。
但是很明显在每一局赌博中都投入100%的本金,显然是不合理的,因为只要有一次赌输了本金就没有了,哪怕这可能性非常小。因为只要你赌的次数足够多,赔钱的赌局就一定能发生。
那可能有人要问了,既然100%赌注是不合理的,那么90%或者更低的赌注会怎么样?实际上解决这个问题,我们可以做个试验模拟这个赌局,看下每次下注的结果是怎样的。如下面这张图:



从图中我们可以看到,当我们把仓位逐渐降低,从90%、80%、70%、60%、50%的时候,在同样的赌局中,结果却完全不一样,细心的朋友可能已经注意到了,随着仓位的逐渐缩小,最后的资金反而是在逐渐扩大。
那可能又有人会问,是不是每次的赌注越小越好呢,比如10%,总不能把每个下注比例都计算一遍吧,这就是著名的凯利公式要解决的问题。在统计学中,凯利公式可以使一个拥有正期望重复下注的策略长期增长率最大化,它可以在每次赌博中计算出最佳的下注比例。
不仅如此,假设本金和赌局可以无尽分割的情况下,使用凯利公式可以在任何赌局中,都不可能破产。特别是在金融交易的实际应用中,是一个攻防兼备的头寸管理策略。我们来看下凯利公式是如何计算的,看下面这张图:




  • f为现有本金的最佳下注比例
  • b为赔率,在交易中也可以称为盈亏比
  • p为成功率
  • q为失败率
那么我们就可以根据凯利公式,计算出这节课中赌博的例子,100元的初始资金,在胜率为50%,赔率为2的情况下,使用多少的下注比例,可以使资金以最快的速度达到100万元,套入凯利公式,计算的过程就是:
(0.5*(2+1) -1)/2=0.25
50%的胜率也就是0.5,乘以赔率2加1,然后减去1,最后再除以2,计算的结果就是0.25,也就是在每一次赌局中,使用25%的本金下注,可以以最快的速度达到100万元。我们可以根据计算结果手动模拟一下,看对不对。



上面这样图是手动模拟结果,请看最后一行,同样的赌局,在经过100多次回合中,25%的仓位率先达到100万元。而90%、80%、70%、60%的仓位其结果是负的,这也充分说明,即使是一个正期望的交易策略,在错误的头寸管理下也会破产出局。
我们也可以看到,50%的仓位最后赌下来不输不赢,这也符合大数定律的结果。为了更加说明问题,在手动模拟的时候也加入了10%的仓位,虽然最后的结果是正收益,但是与25%的仓位比起来,效果差了几个数量级。
大家看到凯利公式的威力了吧,如果你在实际应用中选择10%的本金仓位,那么在100多次的赌局中,你的本金将会变成3万多,虽然收益很大,但是与25%的本金仓位结果比起来,相当于没赚钱。这就是知识的力量。
如果要把凯利公式在生活中赚钱,那就需要满足凯利公式的应用条件,毫无疑问,这个赌局一定是来自金融市场。特别是在量化交易中,通过历史数据回测,就能大概计算出相应的胜率和赔率。
当然凯利公式在金融交易中的实际应用不可能这么简单,还有许多细节需要处理,比如在杠杆交易中的资金成本、真实交易中的资金和仓位是不可能无线分割的、在交易中胜率和赔率是在动态变化的等等。但不管怎么样,凯利公式为我们指明了建立最佳的头寸管理方法。
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:400157
帖子:80032
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP