Loading ...

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

Author: Hukybo, Created: 2019-06-26 10:27:41, Updated: 2019-06-29 15:45:42

[TOC]

前言

为什么要学习这门课? 学习这门课有什么收获呢?首先这门课程是基于JavaScript和Python编程语言,语言只是一门技术,最终我们还是要把这门技术应用到一个行业中。量化交易是一个新兴的行业,目前正处于快速发展阶段,对人才的需求量也很大。

通过本课程的系统学习,可以让你对量化交易这个领域有更深入的认识,如果你是准备跨入量化交易领域的同学对你也有所帮助,如果你是股票或期货投资爱好者,那么量化交易完全可以辅助你的主观交易,通过开发交易策略能够在金融市场上获得利润,也为自己拓宽投资理财的渠道和平台。

在这之前,先讲一下我个人的交易经历,我不是金融专业出身,学的是统计专业。最早从学生时代开始做股票主观交易,后来偶然因素成为国内私募基金的量化交易从业者,主要做策略研究和策略开发。

在交易这个圈子,前前后后也有十几年的时间,也开发过各种类型的策略。我的投资理念是:风险控制高于一切,专注于绝对收益。我们的课程题目就是:从量化交易到资产管理——绝对收益之CTA策略开发。

1、期货CTA策略赚钱逻辑

1.1 认识期货CTA

可能会有人问什么是CTA?CTA到底是什么东西?CTA在国外叫做商品交易顾问,在国内通常称为投资管理人。传统的CTA是将广大投资者的资金集中起来,然后委托给专业的投资机构,最后通过交易顾问(也就是CTA)进行股指期货、商品期货、国债期货投资。

但实际上,伴随着全球期货市场不断发展壮大,CTA的概念也在不断放大,范围之广远超传统期货。它不仅可以投资于期货市场,还可以投资利率市场、股票市场、外汇市场以及期权市场等等,只要这个品种有一定量的历史数据,就可以根据这些历史数据,开发对应的CTA策略。

早在80年代之前,电子盘技术还不太成熟,那个时候大部分交易员是通过手动绘制威廉指标、KDJ、RSI、MACD、CCI等技术指标来判断商品期货的未来走势。后来就有交易员成立专门的CTA基金帮助客户管理资产。直到80年代电子盘普及之后,真正意义上的CTA基金才开始出现。

CTA基金管理规模变动情况 img 单位:十亿美元

我们看上面这张图,特别是随着量化交易的兴起,全球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低很多。

img

我国CTA的发展也只是近十年的事,但是势头很强进,这多半是受益于国内商品期货比较开放的交易环境,交易资金门槛较低、采用保证金制度可以多空双向交易、交易费用低廉、交易所的技术架构相对于股票更先进、也更易于系统交易等等原因。

从2010年以来,CTA基金主要是以私募基金的形式存在。随着国内政策对基金专户投资范围的逐渐开放,CTA基金开始以基金专户的形式存在,其更加透明公开的运作方式,也成为更多投资者资产配置的必要工具。

img

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

img

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

1.2 期货CTA策略类型

从交易策略上来看CTA策略也是多元化的:它可以是趋势策略、也可以是套利策略;可以是大周期中长线策略、也可以是日内短线策略;策略逻辑可以基于技术分析、也可以基于基本面分析;可以是主观交易,也可以是系统交易。

CTA策略有不同的分类方法,根据交易方法,可以分为:主观交易和系统交易,国外的CTA策略发展的比较先进,系统交易的CTA策略已经接近100%。根据分析方法,可以分为:基本面分析和技术分析。根据收益来源,可以分为:趋势交易和震荡交易。

总体来看,CTA策略在整个交易市场上,趋势策略占比约70%,均值回归策略占25%左右,反趋势或趋势反转占5%左右。其中占比最大的趋势策略,根据持仓周期,又可以分为:高频交易、日内交易、中短线交易、中长线交易。

高频做市策略 目前市面上有两种主流的高频交易策略,一种是高频做市策略,另一种是高频套利策略。做市策略是在交易市场中提供流动性,也就是说在有做市商的交易市场中,有人想买卖交易,做市商就必须保证他的单子能成交。如果市场上的流动性不足,导致单子无法成交,做市商必须买卖别人的对手盘。

高频套利策略 高频套利是交易两个相关性强的股票或者ETF和ETF组合。根据ETF的计算方法,可以用相同的方法计算一个ETF期望价格。由ETF指数价格可能会减去ETF期望价格,可以得到一个价差,通常这个价差会在一个价格通道内运行,如果价差突破上下通道,就可以交易这个价差,等待价差的回归,从中赚取收益。

日内策略 如果按照字面的意思,只要是不持仓过夜的,都可以称为日内交易策略。由于日内交易持仓周期较短,通常在入市之后不能马上获利,就迅速离场。因此这种交易方式承受的市场风险较低。但是因为市场在短时间内变化较快,所以日内策略通常对交易者的要求比较高。

中长线策略 理论上,持仓周期越长,策略容量越大,风险收益比越低。尤其在机构交易中,因为短线策略容量有限,大资金不能在短时间内进场出场,所以会配置更多的中长线策略。通常持仓周期是数天数月,甚至更长的时间。

CTA策略数据 一般来说CTA策略是以分钟、小时以及日线数据为研究对象,其中数据包括:开盘价、最高价、最低价、收盘价、成交量等等;只有少部分CTA策略会用到Tick数据,比如L2数据中的买价、卖价、买量、卖量等深度数据。

img

对于CTA策略的基本思路,我们首先想到的还是基于传统技术指标,因为这方面的公开参考资料比较多,逻辑通常也比较简单,大部分是基于统计学原理。比如大家耳熟能详的各种技术指标:MA、SMA、EMA、MACD、KDJ、RSI、BOLL、W&R、DMI、ATR、SAR 、BIAS、OBV、等等等等。

市面上也有一些经典的交易模型,也可以借鉴参考并加以改良,包括:多均线组合、DualThrust、R-Breaker、海龟交易法、网格交易法等等。

以上这些都是基于传统技术分析的交易策略,其过程就是根据历史数据以及正确的交易理念,提炼出有概率优势的因子或者买卖条件,并假设市场在未来时间依然存在这种规律,最后用代码实现交易策略并全自动交易。开仓、止盈、止损、加仓、减仓等等,这些在一般情况下都不需要人工干预。其实就是利用价格时间序列存在系数为正的自相关关系的追涨杀跌策略。

CTA策略最大的优点是,不管当前市场是上涨还是下跌都能获得绝对收益,特别是在市场牛熊快速转换,或者行情走势明显流畅时,这种策略的优势是非常大的,总之就是有趋势在收益就有。但是如果市场处于震荡行情,或者趋势不明显时,这种策略就会可能买在高点卖在低点,不停的来回止损。

1.3 期货CTA策略盈利原理

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

  1. 价格走势存在反身性,它总是以趋势的方式不断延续。当投资者观察到价格上升的时候,就会跟风买入,结果造成价格进一步上升。价格下跌也是同样的道理。由于投资者更多的是非理性行为,所以有时候我们会看到,价格涨时涨得离谱,跌时跌的离谱。
  2. 每一位投资者对盈利和亏损比例的容忍性是非对称的,对风险的承受能力也是不一样的。对于大多数散户来说,他们更倾向于选择更保守的顺势交易方法,市场也更容易走势趋势行情。
  3. 价格的形成是由成交决定的,真是成交的背后又都是人来推动的,但人性是很难改变的,这就导致固定形态会反复出现的原因,策略在历史数据上回测有效,也就预示着将来可能也会有效。

另外趋势跟踪的交易特点是,在没有行情的时候亏小钱,当行情来的时候赚大钱,但是做过交易的人都知道,市场在大部分时间是出于震荡行情,只有在少量时间是趋势行情。所以趋势跟踪策略在交易时胜率较低,但是综合下来每一次交易的盈亏比较大。

由于趋势跟踪策略在收益上不稳定,所以很多投资机构会用多品种多策略构建一个投资组合,这中间也会配置一定量的反转策略。反转策略就是价格的时间序列存在系数为负的自相关关系,也就是高抛低吸。

CTA与传统资产的相关性 img

我们看上面的图,理论上多种风格不同或者相关性比较低的策略,在同时面对市场价格各种变化时,会做出时而相同时而不同的交易信号。由于多条收益曲线相互叠加,使得整体收益形成互补,收益曲线会变得更加平缓,从而减小了收益的波动性。

由上面的观点可以得出,与其开发一个大师级的策略,还不如开发多个中庸子策略,那么如何控制这些策略呢?这里我们可以借鉴机器学习中随机森林算法,随机森林并不是一个独立的算法,它是一个包含多棵决策树的决策框架。相当于决策树这个子策略之上的母策略。通过母策略组织和控制子策略集群。

接下来就需要设计一个母策略了,可以通过对全商品期货市场中各个品种的流动性、收益性和稳定性进行评估,筛选出收益具有低波动率的商品期货品种组合,再进行行业中性化筛选,通过组合的行业分散配置来进一步降低整体波动率,最后再通过市值匹配构建实际的商品期货多品种组合进行交易。

每个品种还可以多参数策略配置,可以选择回测表现良好附近的参数组合,当市场趋势明显时,多组参数策略通常会表现一致,相当于加仓;当市场处于震荡行情,多组参数策略通常会表现不一致,从而各自做多或做空进行风险对冲,相当于减仓。这样可以进一步降低投资组合的最大回测率,同时还可以保持整体收益率不变。

2、经典期货CTA策略案例

牛顿曾经说过:如果说我看得比别人更远些,那是因为我站在巨人的肩膀上。

市面上公开的CTA策略有均线策略、布林带策略、海龟交易法者、动量策略、套利策略等等。量化交易策略都有一个特点,那就是见光死,策略一旦被公开就会慢慢失效。但这并不影响我们学习这些策略,借鉴其中的精髓,这样才能站在巨人肩膀上看待问题。

2.1 期货基本面分析(库存、基差、价格)

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

img

我们看上面这张图,影响商品价格的因素有很多,林林总总多达数十项,往细了分更有几十项之多,并且这些数据是在不停变化的。单个散户想要获取这些庞大的数据已经是力所不及的事了,更不用提客观分析。

其实,商品期货的基本面分析,并不是把所有的因素都加以分析,我们只需要抓住基本面分析核心要素,就能剥丝抽茧从错综复杂的信息中找出规律。

宏观因素 宏观经济数据复杂多变,每天每时每刻,有很多的经济数据公布,各国政界、央行、投行,官方的和非官方的。除了政治和经济危机外,宏观分析是聊天的好材料,实用性不大。美国著名的基金管理专家彼得·林奇曾发表看法:“我每年花在经济大势上的分析时间不超出十五分钟”。

品种因素 在基本面分析中,品种分析主要是分析升水贴水、供需关系、商品库存、产业利润等等,可以说掌握商品期货品种因素分析,基本上能够判断大部分行情走势。

做过期货的朋友都知道,国内的商品期货可以简单划分为:工业品和农产品。工业品和农产品的分析方式是有所区别的,我们从供给和需求这两个方面进行阐述,在工业品中供给是相对稳定的,除非是有重大技术突破,否则产能是不太可能在短时间内有重大变化的,所以影响工业品价格的因素主要是需求。在农产品中需求是相对稳定的,长期来看农产品的需求存在变化,但短期来看农产品的需求趋于稳定的,所以影响农产品价格的因素主要是供给。

img

因此,根据经济学规律,最终决定商品价格的是供需关系,理论上只要能获取供给和需求的数据,就能判断商品未来的价格。对于工业品来说,供给的数据比较容易获取,但是很难获取到需求的数据,对于农产品来说,需求的数据比较容易获取,要想获取供给的数据就很难了。

其实我们还可以进一步做减法,供给与需求在经济市场中的相互结果就是库存,我们可以通过库存数据,来判断市场供给与需求的强弱关系。如果某个商品库存很高,说明市场供给的力量大于需求,在外在条件不变的前提下,商品价格即将下跌。如果某个商品库存很低,说明市场需求的力量大于供给,在外在条件不变的前提下,商品价格即将上涨。

除了分析商品库存外,还需要分析现货市场与期货市场的价格差,也就是所谓的基差。如果期货价格大于现货价格,我们称之为期货升水;如果期货价格小于现货价格,我们称之为期货贴水。根据期货交割制度,在期货交割日期,期货价格应该等于现货价格。

img

无论是升水还是贴水,由于期货交割制度上的约束,理论上交割日期货价格应该等于现货价格。随着交割日期的临近,现货价格与期货价格都会趋于一致,一种是期货向现货回归,另一种是现货向期货回归。

根据上面的原理,我们可以用库存和基差同时判断未来的期货价格。如果某个商品库存较低,并且如果期货价格比现货价格低很多,那么我们可以判断:现货市场需求的力量大于供给的力量,未来现货价格上涨的概率较大;又由于期货交割制度,随着交割日期的临近,期货价格将会补涨,与现货价格持平,未来期货价格上涨的概率更大。

最后,我们通过库存和基差判断了未来价格的大概率方向,但是并没有较为精确的买卖点,因此还需要配合技术分析,给出明确的进出场信号。整个基本面分析的架构就是:低库存+深度贴水+技术分析多头信号=做多;高库存+大幅升水+技术分析空头信号=做空。

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)

    img

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

// 参数
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小时级别的初步回测绩效报告。

img img img

从资金曲线和回测绩效数据来看,该策略表现良好,整体资金曲线是稳步向上的。但是螺纹钢品种从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[i].ContractType == tradeType) {
            if (positionData[i].Type == type) {
                if (positionData[i].Amount > 0) {
                    return positionData[i].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[i].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[i].Type == 0) { // 如果是多单
                type = 10; // 设置下单参数
            } else if (positionData[i].Type == 1) { // 如果是空单
                type = -10; // 设置下单参数
            }
            // 平掉所有仓位
            this.trade(positionData[i].ContractType, type, positionData[i].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秒
    }
}

二、 获取并计算数据

  1. 获取基础数据对象、账户余额、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数据就返回
}

三、 下单并对后续处理

  1. 根据上述的策略逻辑,执行买卖操作。首先会判断价格和指标条件是否成立,然后再判断持仓条件是否成立,最后执行 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开多
        }
    }
}
  1. 下单完成后,需要对未成交的订单、持有单个合约等非正常情况做处理。以及绘制图表。
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[i].ContractType == tradeType) {
            if (positionData[i].Type == type) {
                if (positionData[i].Amount > 0) {
                    return positionData[i].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[i].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[i]

More