输入/搜索内容
3
关注
224
关注者
Python商品期货量化入门教程
入门手册
创建于 2024-09-12 09:11:33  更新于 2025-08-05 11:49:33
 2
 4898

img

企业微信:

img

导入课程

大家好,欢迎加入商品期货量化交易课程!在这个课程中,我们将使用Python语言,并借助优宽量化平台,从零开始系统学习量化知识。我们的目标是帮助大家成为精通量化交易的高手。

或许你们已经听说过量化交易,也对一个量化策略能够获得多大的收益感到好奇。那么,让我们通过一个实例来感受量化交易的神奇吧!我们将在优宽量化平台上选择一个策略,选取纯碱期货品种,并设定策略在2023年全年运行。结果显示,这个策略获得了150%以上的收益,确实令人惊叹。如果你们想学习如何从零开始研究、开发、编写和实践量化策略,那么就从今天开始吧!让我们一起踏上这段量化之旅。

神奇策略:在优宽量化平台实现一个年化150%+策略

image

第一章:商品期货量化交易基础

本节课我们首先介绍一下量化交易的概念以及商品期货市场。作为一种新型的金融投资方法,量化交易利用计算机技术,结合数学建模和统计学分析,从大量的历史数据中提炼出交易策略,通过计算机强大的运算能力实现自动交易,减少了交易者因情绪影响做出的非理性交易。

1.1 什么是量化交易

量化交易作为交易与计算机结合的产物,正在改变着现代金融市场的格局。如今已经有不少交易者将目光转向了这一领域。如何最大限度地降低风险并尽可能取得收益?也是许多交易者孜孜以求的目标。

1.1.1 量化交易概述

很多人一听到“量化交易”就会觉得高端大气、一夜暴富。人工智能时代,伴随着深度学习、大数据、云计算等先进技术的兴起,更是赋予它神秘的色彩。似乎只要运用量化交易,就能构建出“完美无缺”的交易策略。

在一定程度上,量化交易已经被神话了。量化交易其实就是借助计算机,并利用统计学、数学等方法,通过科学的投资体系,从中找到一套正期望的交易信号系统。这个信号系统会告诉我们应该在什么时间以什么价格进行买卖。

1.1.2 量化交易发展

追本溯源,早在19世纪,法国股票经纪人助理朱尔斯·雷格纳特就采用量化方法来分析价格数据变化,从中发现市场价格涨跌规律,并提出了股票价格变化的现代理论,随后出版了《概率计算和股票交易哲学》一书,在书中详细阐述了自己发现的市场涨跌规律(正态分布):“价格的偏差与时间的平方根成正比”,最后以理性量化的投资方法获取交易上的成功。

现如今,在互联网+大数据+云计算+人工智能的时代背景下,量化交易也得到了快速发展。曾经的全球金融腹地伦敦金丝雀码头,早已变成了IT公司集散地。世界顶尖投行,也都在培养自己的量化团队,试图跻身到“得策略者得天下”的金融大战之中,这些开发交易模型的IT团队也被称为Quant Team。

反观国内,无论是硬件设备还是投研实力,都还在发展初期阶段。但已经有越来越多的机构和专业投资者意识到量化交易的好处,并参与到这一领域,特别是在商品期货市场逐步规范、市场有序开放,量化交易更具有广阔的成长空间。

1.1.3 量化交易的特点

量化交易脱胎自主观交易,主观交易每次下单前需要人为判断行情,这在实际交易中很难保持一致,尤其是当行情波动剧烈,账户盈亏时时刻刻左右交易者的心智,使交易者很难做出正确判断。而主观交易的缺点正是量化交易的优点,具体如下:

  • 科学验证:当编写完交易策略后,如果用模拟盘测试它的有效性,可能会付出很大的时间成本,如果用实盘测试,更有可能损失真金白银。但量化交易中的回测功能,可以通过大量的历史数据,以科学的方式去检验交易系统。
  • 客观准确:在交易中,我们真正的敌人是自己,心态管理说起来容易,做起来难。贪婪、恐惧、侥幸等人性的弱点,在交易市场中会数倍放大,量化交易则可以屏蔽这些弱点,在交易中做出更理性的决策。
  • 及时高效:在主观交易中,人的反应速度是无法快过电脑的,并且人的体力和精力也无法24小时运行,在机会稍纵即逝的交易市场,量化交易完全可以代替主观交易,寻找更多的交易机会,及时快速地跟踪市场变化。
  • 风险控制:量化交易能从历史数据中挖掘价格未来可能重复的规律,这些规律可以转化为较大概率取胜的策略。还可以构建多种不同的投资组合,降低系统性风险,平滑资金曲线。

注意:主观交易并非一无是处,在量化交易中,计算机很难识别千变万化的K线形态,比如:双重顶底、头肩定底、V型反转等等,但主观交易就很容易分辨出来。另外相对来说,主观交易更加细腻,比如对于一些似是而非的交易信号会选择性回避。

1.2 为什么选择量化交易

很多人在探讨量化交易时会以复杂的策略编程为切入点,这无形中给量化交易披上了一层神秘的面纱。本节将以通俗易懂的语言,为量化交易做一个简单“素描”,即便是毫无基础的小白也能轻松理解。

1.2.1 量化交易与主观交易的区别

主观交易更重视人为的分析和盘感,即使出现了买卖信号,也会选择性地下单交易,宁可错过行情,也不愿做错。人的感觉是复杂多变的且不可靠的,大多数交易者一旦发生连续亏损,往往就转而用另一种方法。交易的随机性较强,容易被浮动盈亏困扰,导致难以稳定盈利。

量化交易通过对交易的理解,制定一致性的买卖策略。在交易中,对所有的走势都一视同仁,开仓平仓全部系统化处理,宁可做错,也不愿错过。它还具有完整的评价体系,通过历史数据回测,确定策略更适合哪一类的行情和品种,并搭配多种策略和品种实现盈利。

简而言之,主观交易是量化交易的基础,量化交易是主观交易的提炼。主观交易更像是练武,最后能成功与否,天赋占大多数,有十年不悟的,也有一朝悟道的。量化交易更像是健身,只要刻苦努力,就算没有天赋,也能练出一身肌肉。

1.2.2 量化交易比主观交易更好?

一个成功的主观交易者,从某种角度上说,也是一个量化交易者。因为一个成功的主观交易者,必然有一套自己行之有效的规则和方法,也就是交易系统。成功的主观交易必须建立在交易规则和交易纪律之上,而交易规则其实就是主观交易中的量化部分。

相反,成功的量化交易者,也都脱胎自主观交易,因为量化交易策略的开发,其实就是主观交易方法的具体实现。如果一个对市场的理解和认知,从一开始就是错误的,那么开发出来的交易策略,长期以来也是难以获利的。

所以从长期稳定盈利的角度讲,决定一个交易者最终能否成功,关键因素是交易理念,而不在于是主观交易还是量化交易。量化交易表面上看似高大上,其盈利的本质与主观交易没有本质的区别,它们就像是一件事物的两面性既对立又统一。但是不可否认,从交易工具上来说,量化交易确实有很多优势。

  • 复盘更快: 想要检验一个交易策略,就需要计算大量的历史数据,量化交易几分钟之内就能计算出结果。这个速度要比主观交易快许多倍。
  • 更加科学: 评价一个策略是否优秀,依靠的是数据(比如:夏普比率、最大回撤率、年化收益),优秀的策略和交易理念往往具有可证伪性。
  • 更多机会: 国内商品期货有几十个交易品种,主观交易不可能同时盯盘,但是量化交易可以全市场实时盯盘,不错过任何交易机会,增加交易效率。

1.2.3 量化交易一定能赚钱吗?

当然能,但长期坚持下来却是一件很难的事。赚钱与否并不取决于量化交易本身,它只是一个工具,量化交易只是把交易思想用程序化、规则化、数量化实现出来,程序代替的只是执行力。难的是长期稳定地赚钱,因为市场是动态博弈,交易思路也要跟着市场转变。

1.2.4 量化交易的风险

量化交易也有风险,为什么呢?因为量化交易是在历史数据中去挖掘规律,形成交易策略。但是金融市场是一个生态体系,其规律和人性是一个相互作用的动态过程,归根到底还是人的市场。市场的规律会被人性所影响,而人性中间的贪婪、恐惧都会随着市场的变化而变化。所以市场上很少有一成不变的规律,再厉害的交易策略也很难应对这种突如其来的规律变化。

通过上面的解释可以看到,量化交易不是一种独特的交易方法,它只是一种交易工具,帮助我们分析交易逻辑,完善交易策略。无论是价值派、技术派,无论做的是股票、债券、商品还是期权,其实都可以量化。相比于靠个人经验做决策的交易者,量化交易者手中的武器就是市场证据和理性。

注意:量化交易的风险大部分来源于市场的风险,所以先学会交易,再学会量化。

1.3 量化交易需要准备哪些

一个完整的量化交易生命周期,不仅仅只是交易策略本身。它至少由六个环节构成,包括:策略构思、建立模型、回测调优、仿真交易、实盘交易、策略监控等。

1.3.1 策略构思

首先,做量化交易必须先回到交易市场,要在市场中多观察价格,理解市场波动的规律,并尝试推断每一个交易逻辑,最后总结出交易策略。这里并没有捷径,阅读经典的投资书籍或许有帮助,或者不断地坚持做交易,在失败中总结经验。

对于初学者来说,开发交易策略最好的方式就是模仿。直接利用现成的技术分析指标构建策略逻辑,写入买卖规则,这样就可以得到一个简单的交易策略。例如单均线的策略逻辑是:如果价格高于最近10天的平均价格就买入,如果价格低于最近10天的平均价格就卖出。当然,随着市场经验的积累,形成自己的交易方式后,策略逻辑的选择会越来越多样化,再进阶到更加系统的量化交易。如果能做一个有量化思维的交易者,无论是在股票还是期货市场上,这都是一件值得庆幸的事。

1.3.2 建立模型

其次,你需要掌握一个量化交易工具,用来编写交易策略,实现你的交易想法。市面上的常用软件都可以。但是如果你想成为一名高端的量化交易者,就需要学会一门计算机语言,这里推荐使用Python编程语言,因为它是科学计算的权威语言,并且提供各种开源的分析包,文件处理,网络,数据库等。

1.3.3 回测调优

当编写完策略后,下一步就是对策略进行回测,以及参数的筛选和优化。可以利用不同的参数对策略进行回测,观察该策略的夏普比率、最大回撤、年化收益等。通过对策略的不断调试和修改,最终得到一个完善的量化交易策略。

比如,可以把2010年~2015年的历史数据作为样本内数据,2016~2020年的历史数据作为样本外数据。先用样本内数据优化出几组表现好的参数,再用这些参数对样本外数据进行回测。一般情况下,样本外的回测结果没有样本内的回测结果好,但是如果样本外与样本内的结果大相径庭,那么这个策略可能是无效的,就要观察分析,判断策略失效的原因。如果发现策略失效由于是样本外数据,某几次极端行情导致的大幅亏损,那么就可以增加一个固定止损条件来规避这种风险;如果发现策略失效是由于交易次数过多,那么就可以将交易逻辑收紧,降低交易频率。如果一开始交易逻辑本身就是错误的,再怎么修改也很难得到一个赚钱的策略,这个时候就需要重新审视自己的策略思路了。另外,在参数优化中,可用的参数组越多越好,说明策略的适用性广泛。

注意:核心的策略参数越少越好,如果参数过多很容易造成数据拟合。在回测时,交易次数太少的策略其回测结果可能是幸存者偏差。如果回测的结果是一个超级赚钱的资金曲线,很多情况下是策略逻辑写错了。

1.3.4 仿真交易

当交易逻辑正确,样本内外回测都赚钱时,先不要急着在真实账户上交易。尤其对于初学者来说,一定要先用仿真账户运行至少3个月,如果是中低频隔夜策略,则需要更长的仿真交易时间。在未来一段完全未知的仿真行情中,观察策略在仿真交易中表现,仔细核对回测信号与仿真交易信号是否吻合,下单时的价格与成交时的价格是否有偏差,如果表现与预期相符合,那么说明策略有效。

1.3.5 实盘交易

通过一段时间仿真交易检验之后,就可以将策略放入实战中进行交易了。不过在量化交易的过程中也要保持警惕,防范极端行情。在实盘交易中,策略的期望一般都要打折扣的,很难达到回测时的状态。

1.4 一个完整的策略有哪些要素

一个完整的策略,其实就是交易者给自己定的各种规则,它包括了交易的各个方面,并且不给交易者留下一点点主观想象的余地,每个买卖决定,策略都会给出答案。它至少包含策略选择、品种选择、资金管理、下单交易、极端行情应对、交易心态等等。

1.4.1 策略选择

从专业的角度讲,主流的交易策略可以分为趋势交易、配对交易、一揽子交易、事件驱动、高频交易、期权策略等等,当然,策略的分类方式不是固定的。对于刚入门的量化交易初学者来说,不必管这么多名词概念,一步一步从最简单的开始。趋势交易是一个不错的选择,其特点是策略逻辑简单,用有限的亏损换取无限的利润,长期来看是正期望策略。

1.4.2 买卖什么

做过交易的人应该知道,每个品种都有各自的性格。有些品种性格很“火爆”,流动性好、波动率高;有些品种性格很“温顺”,常年都在一定区间内震荡,波动率低。相比之下通常工业品比农产品波动率高。在选择交易品种时,需要考虑品种的波动率情况,波动率高的品种,往往很容易走出一波不错的趋势行情。如果是趋势跟踪策略,尽量选择工业品,从品种属性上来讲,工业品往往比农产品波动率要大。不同的策略适应不同的品种,选择合适交易品种,对期货交易这项大工程来说尤为关键。总的来说,没有绝对好的品种也没有绝对不好的品种。根据投资风格的不同,以及风险承受力的不同,需要针对自己的标准进行相应的调整。

1.4.3 买卖多少

交易是赔钱容易赚钱难的行当,当账户资金亏损50%后,挽回损失则需要100%的盈利。就算赚很多次100%,但只需要赔一次100%就全部亏光。所以成熟的交易策略应该包含资金管理。事实上,很多以传统技术指标构建的交易策略,最大回撤率会很大,甚至超过50%。但一个风险很大的策略完全不能用吗?显然不是,最大回撤率完全可以通过资金管理控制,也就是买卖多少。如果把仓位降低一半,那么风险也会降低一半,如果把仓位再降低一半,那么风险就会降低更多,这就是一个简单的资金管理方法。但是一味的降低仓位也不是较好的办法,因为降低仓位也就意味着降低了利润,如何取舍就要看交易者对风险的承受能力。

1.4.4 何时买卖

一个好的买点,是成功的一半,它能够迅速摆脱成本区。但是价格不是上涨就是下跌,从大数定律的角度讲,好的买点很难超过50%,一味的追求胜率反而是舍本求末。笔者认为开仓不是决定最终盈利的核心,对于趋势策略来说,交易的核心是开仓之后,如何尽可能优化处理持仓,达到赢冲输缩的目的。不管是短线策略,还是长线策略,比的不是看谁持仓时间长,而是风险收益比。换言之,影响策略绩效的最终结果是如何出场,出场方法又可以分为两种:止损出场和止盈出场,这两个部分也是关乎交易策略成败的分水岭。

1.4.5 如何买卖

如果说何时买卖是艺术,那么如何买卖就是技巧。在价格瞬息万变的环境中,酌情使用订单类型和下单方式,可以增加订单成交率,也可以降低滑点,减少交易成本。交易技巧通常需要考虑下列几种情况:

  1. 委托下单类型和方式:
    委托下单的类型和方式有许多种,比如可以用:排队限价单、对手价、最新价、超价、涨停价、跌停价、买一价、买二价、卖一价、卖二价,或者先用排队价,再用超价,分批报单,或者把大单拆成一个个小单,或者干脆直接把单子全部报出去。

  2. 撤单
    如果下单没有成交,就要考虑是继续等待还是撤单,继续等待意味着可能错失行情。如果撤单就要考虑是否继续追单。

  3. 追单
    如果撤单后继续追单是按最新价去追,还是对手价,还是涨跌停价,如果追单仍未成交是否继续追单。当价格与最初的信号相差甚远时,是不限成本追单,还是放弃这个信号。

  4. 涨跌停价
    有时候当下单信号出现,刚好是涨跌停价格。那么是否在涨跌停价挂单排队成交,如果没有成交怎么办,尤其是在持仓与行情反向的时候,如何对冲补救。

  5. 集合竞价
    开盘集合竞价时,哪个报价最多就采用哪个价格开盘,盘面是不显示价格的,只能根据自己的预判进行申报,这里面充满不确定性,要不要参与,以及怎么参与。

  6. 夜盘
    有些商品期货品种夜盘是从21:00至次日02:30,人的精力是有限的,这段时间做不做,人工做还是让电脑来做。

  7. 重大节日
    重大节日的超长假期之前,仓位需不需要保留。如果保留的话如何控制风险。如果节后价格反向跳空后如何处理。

  8. 极端行情和突发事件
    价格瞬间涨跌停、连续涨跌停、乌龙指事件、黑天鹅行情等价格踩踏事件发生时,或者突然断电、断网、电脑故障、软件宕机、银期转账暂停、自然灾害等,出现时如何应对。

1.4.6 交易心态

市场会无形中放大交易者的情绪,影响交易的负面情绪有很多种,其中贪婪、恐惧和侥幸是交易中常见的三种负面情绪。因此交易者需要一个强大的交易心理体系,在不同阶段对上述三种情绪加以控制甚至利用。

注意:交易不仅考察技术基本功,还考察交易者心态,可以说人性的弱点在交易过程中都会被展现无遗和放大。只有不断学习和总结经验教训,不断历练,才能克服人性的思维共性和心理弱点。

没有完美的策略,也没有更好的策略,只有更适合自己性格的策略。结合自身的性格和资金情况一起去衡量该策略是否适合自己,如果适合自己的话,要充分评估自己坚持下去的可能性有多大,最坏的结果要事先规划好,如果最惨的一面你都想好了,那么执行下去的可能性就相对较大。

1.5 认识商品期货

1.5.1 商品期货市场介绍

中国商品期货市场是一个多层次、多元化的市场体系,涵盖多个交易所和广泛的交易品种,涉及金属、能源、化工、农产品等多个领域。以下是对中国商品期货市场的简要介绍:

交易所

  1. 郑州商品交易所(郑商所):主要交易农产品和部分化工产品。
  2. 大连商品交易所(大商所):以农产品、化工原料和塑料制品为主。
  3. 上海期货交易所(上期所):交易金属、能源化工产品以及贵金属等。
  4. 中国金融期货交易所(中金所):提供股指期货和国债期货等金融期货产品。
  5. 上海国际能源交易中心(能源中心):专注于原油,国际铜等国际化产品的交易。
  6. 广州期货交易所(广期所):国内首家混合所有制交易所,目前上市品种有碳酸锂和工业硅。

品种种类

中国商品期货市场提供了丰富的交易品种,包括但不限于:

  • 农产品:玉米、小麦、豆类、棕榈油等。
  • 金属:铜、铝、锌、镍、锡、铅等有色金属,以及黄金和白银等贵金属。
  • 能源:原油、低硫燃料油、燃料油、沥青等。
  • 化工产品:塑料、PTA、PVC、PP等。
  • 建材:螺纹钢、线材、热轧卷板、玻璃等。

交易机制

  • 交易时间:各品种交易时间略有不同,通常包括日盘和夜盘交易时段。
  • 交易方式:期货合约的买卖可以通过电子交易平台进行。
  • 保证金制度:投资者需按规定比例缴纳保证金,以确保合约履行。
  • 涨跌停板:各品种设有涨跌停板限制,以控制风险。
  • 交易制度:期货交易实行T+0制度,风险较高,需谨慎参与。
  • 交割方式:期货合约可以采取实物交割或现金交割的方式。

1.5.2 商品期货代码设置

我们来了解一下期货合约的设置规范。由于商品期货合约连续的特殊性,为满足回测的使用需求,针对每一个品种提供主力连续合约和指数合约,其主要是根据当前时间段内有效的商品期货合约数据人工合成。其中:

  • 主力连续合约:由该期货品种不同时期的主力合约(价格和成交量)直接拼接而成,代码以888结尾,例如rb888。合约首次上市时, 以当日收盘同品种持仓量最大者作为从第二个交易日开始的主力合约。如果同品种其他合约持仓量在收盘后超过当前主力合约1.1倍时, 则在第二个交易日进行主力合约切换。
  • 指数合约:由该期货品种所有正在交易的合约,以持仓量加权平均计算。代码以000结尾。

在回测中,我们就可以使用主力连续合约或者指数合约设置不同的期货品种。这主要是在回测系统中使用更加方便。但是在实盘使用的时候,当我们使用主力或者指数合约下单,交易所是无法识别的,这里我们使用调试工具对接真实的市场进行仿真交易,当我们使用指数合约下单,会返回Not select symbol的错误。

在实盘中,我们需要设置具体的合约名称。针对于不同交易所的具体合约,期货合约代码规则可以总结为这张图中展示的这样。

交易所具体合约规则主力连续合约指数合约具体合约
中国金融期货交易所品种代码(大写) + 交割年份(2位) + 交割月份(2位)IF888IF000IF2201
上海期货交易所品种代码(小写) + 交割年份(2位) + 交割月份(2位)ag888ag000ag2201
上海国际能源交易中心品种代码(小写) + 交割年份(2位) + 交割月份(2位)bc888bc000bc2201
郑州商品交易所品种代码(大写) + 交割年份(1位) + 交割月份(2位)AP888AP000AP201
大连商品交易所品种代码(小写) + 交割年份(2位) + 交割月份(2位)a888a000a2201
广州期货交易所品种代码(小写) + 交割年份(2位) + 交割月份(2位)lc888lc000lc2405

中国金融期货交易所:合约代码由品种代码(大写)+ 交割年份(2位)+ 交割月份(2位)组成。例如具体合约为IF2201。

上海期货交易所,上海能源期货交易所,大连商品期货交易所和广期所,广期所是最新成立的,这四个交易所它们的合约命名的规则都是一样的:合约代码由品种代码(小写)+ 交割年份(2位)+ 交割月份(2位)组成。例如具体合约为ag2201。

比较特殊的是郑州商品交易所:合约代码由品种代码(大写)+ 交割年份(1位)+ 交割月份(2位)组成。例如具体合约为AP201。

根据不同的交易所和品种,合约代码会有一定的差异,但通常都遵循这种命名规则。关于其他类型的合约,比如套利合约,期权和股票合约,大家可以看一下这篇文档(优宽量化商品期货合约代码明细)。

1.5.3 K线概念

在期货市场中,每一个品种的价格都随着时间的推移而发生变化。金融量化就是利用计算机来分析这些期货品种价格变化背后的规律,方便来预测未来的价格走势并做出最合适的投资决策。在这个过程中,数据起着至关重要的作用。对于金融量化来说,我们需要收集大量品种价格的历史数据,并把它们转换成数字形式,然后方便用计算机进行分析。通过对大量的历史数据进行分析,我们可以找到隐藏在价格波动中的规律和趋势,并根据这些规律和趋势制定投资策略。

在量化的世界里,数据就是K线。我们首先来介绍一下K线,K线经常以蜡烛图的形式呈现,是一种广泛应用于股票、期货、外汇等金融市场的技术分析工具。它通过绘制价格走势、开盘价、收盘价、最高价和最低价等信息,它是按照时间顺序形成一根根“蜡烛形状”的图表,来描绘出市场的交易情况与趋势。

在趋势交易策略中,K线图可以用来确认和预测市场的趋势变化。通常使用较长时间周期的K线图,如日线、周线或月线等,判断市场是否呈现出明显的上升或下降趋势,并相应地制定投资决策。而在高频交易策略中,K线图常常被用于构建高频交易模型,预测实时的市场波动趋势并进行交易。同时,高频K线图也可以用于快速识别买卖机会,尤其在股票及期货领域中。高频交易的K线周期更短,一些投资者会通过以秒为单位或者分钟为单位的K线图发现价格的快速反弹或下跌机会。无论是趋势交易策略还是高频交易策略,K线都是一个重要的分析工具。投资者应该根据自己的投资风格、交易周期和市场情况选用相应的K线周期进行分析,并结合其他技术指标综合分析,这样可以获取更全面、准确的市场信息和交易信号。

我们看到的K线经常是蜡烛图的形式。从数字的角度,我们来了解一下K线数据是什么样子的。我们在优宽量化平台打开运行一个策略,设置周期时间为一分钟,使用以下代码,读取K线数据。

python
def main(): exchange.SetContractType('rb888') records = exchange.GetRecords() Log('最新K线数据', records)

这里我们使用Python代码订阅期货品种,获取K线数据;可以看到这里看到返回的是一个数组的形式,这中间包含的都有时间戳(这是从1970年1月1日以来的毫秒数)、开盘价(open)、最高价(high)、最低价(low)、收盘价(close)、成交量(volume)和持仓量(OpenInterest)。这就是一根K线柱的样子。

第二章: 认识优宽量化

开发策略需要量化平台的支持,才能支撑起整个量化工程。量化平台一般包括策略编写、策略回测、策略分析、模拟交易、实盘交易等功能。在本章中,我们将深入了解优宽量化平台。

2.1 平台简介

优宽量化交易平台(原发明者FMZ)是国内最专业的量化社区之一,创建于2014年。在该平台可以学习、编写、分享、买卖量化策略,在线回测和使用模拟盘模拟交易,运行、公开、围观实盘机器人。支持商品期货 CTP、易盛接口。该平台适用于量化交易初学者,即使无基础也可以快速入门,平台功能强大灵活,也可以满足进阶需要。支持使用 JavaScript、Python、C++ 等主流编程语言,也支持可视化语言和 My 语言(兼容文华财经)实现策略。

2.1.1 平台架构

优宽量化交易平台系统,并不是单一的传统软件,而是一个集实盘、回测、分析工具、分布式部署、分享展示、社区交流等多元一体的量化交易平台软件系统。该平台的架构特殊,具有很多优势。

特有的量化交易实盘机器人,专门负责执行交易策略,实盘通常部署在用户自己的服务器或电脑的托管者程序上。另外托管者程序负责和优宽量化交易平台网站通信,进行传递日志、直接访问交易所获取行情和交易等任务。如果优宽量化交易平台网站出现短暂问题,也不会影响策略实盘的执行。用户可以将托管者部署到任何地方来提高交易速度,通过网站或者手机随时随地的管理实盘、查看日志、修改参数。

注意:优宽量化聚焦于实盘交易,通过将计算密集和高延迟的任务从交易关键路径分离,以保证平台内部最低的延迟、最好的性能和最高的吞吐。

2.1.2 平台功能

  • 控制中心:用户的操作界面,包含了经常使用的各个功能的快捷入门,方便浏览账户的状态和迅速的进行需要的操作。
  • 策略:策略广场,用户公开和出售的策略在这里,大家在这里学习不同类型和不同语言的各类成熟的策略,可以进行搜索,或者点击相应的标签筛选某一类的策略。
  • 围观:是优宽量化用户公开运行的实盘,大家可以对不同类型的实盘策略进行围观评论。
  • 文库:这是优宽量化官方和量化大佬出品的一些精品文章,涵盖了量化理论知识的深入介绍、最新热门策略的细致解析,以及关于量化策略编写的宝贵经验分享。通过阅读这些文章,新入门的同学可以迅速提升自己的量化知识水平,资深玩家也能从中获得新的启示和感悟。
  • 社区:用户发帖提问交流平台,大家在使用优宽量化平台如果遇到哪些问题,都可以在这里提出,我们的量化大佬都会热心回复。
  • API文档:这里也做了全新的改版。(1)第一部分是开发文档,具体包括用户指南,这是优宽量化平台功能和运行机制的介绍,可以帮助大家更加了解优宽量化的使用方法;语法手册包含编写策略所需要的API介绍,具体包括内置变量,结构体,和内置函数的介绍,这里提供了详细的API文档和常见问题解答。大家可以在这里搜索有关API的任何问题,并找到相关的解决方案和示例代码。Pine语言和My语言文档包含了这两种语言的具体的语法手册和使用指南。(2)第二部分是公开课的视频资源:这是优宽量化官方精心策划的一系列专业量化入门课程,旨在帮助不同背景和水平的学员从零开始掌握量化策略编写。这些课程提供多种语言选项,并分为基础级和实践级,确保学员在学习过程中得到全面而深入的指导。通过这些课程,学员将逐步掌握量化策略的核心知识,为未来的量化交易之路打下坚实的基础。重点是这些课程全部是免费的,大家可以自由的探索学习。(3)第三部分是文档中心,这种汇总整理了各种的教学帖子,大家可以熟悉一下,帮助大家解决在使用优宽量化平台时遇到的问题。

2.1.3 实盘

实盘是交易策略的执行者,一个实盘只能执行一个策略,但一个策略可以给多个 实盘执行。在优宽量化平台实盘页面,用来管理托管者上的实盘、显示实盘的运行状态。

2.1.4 策略库

顾名思义策略库是存放策略的仓库,同时策略库也是编程和回测策略的入口,不仅可以在策略库中新建策略,还可以分别对不同的策略进行分组。对于一些优质策略可以以出租或出售的方式授权分享给别人。
注意:可以在授权管理中,分别管理策略和实盘授权。

2.1.5 托管者程序

托管者是交易机器人的载体,是管理机器人的程序,主要负责处理底层数据、访问接口、通信等任务。托管者程序有适用于主流操作系统的各个版本,托管者如何和优宽量化交易平台通信?在部署运行托管者程序时,需要输入一个地址,该地址即为优宽量化交易平台通信的地址,这个地址中有一个用户唯一标识ID,地址输入完以后,还需要另外一个配置,配置上优宽量化交易平台账号的密码。使用这两样信息来让托管者程序和优宽量化交易平台上的账户对应起来。

2.1.6 策略编写界面

点击策略库就可以进入策略编辑界面,该界面主要包含两大功能:策略编写和策略回测。

1、 点此进入回测,关于回测的具体说明将在后续教程中讲解。
2、 策略使用的语言选择,策略创建后不可切换语言。
3、 笔记是编写策略的记录,仅自己可见;描述是策略说明,策略公开后其他人可以在策略页面看到;手册是策略的使用说明,购买或复制策略的可以看到。
4、 策略类型,分为通用策略和模板,关于模板以后教程会有详细说明。
5、 策略分组。
6、 远程编辑,包含常用的代码编辑器插件,可在本地编写策略,自动同步到优宽量化交易平台。
7、 保存,可在编辑状态使用 Ctrl+S 的快捷键。
8、 选择要引用的模板类库,要先在策略广场把模板复制下来。

2.1.7 其余板块功能

交易终端

这是优宽量化最新改版的手动交易和半自动交易的量化交易工具,依靠账号品种的分租绑定,功能模块的自由布局,和插件的自主编写三大特性可以方便的我们进行交易的操作。后续将重点展开讲解。

数据探索

优宽量化新版推出的量化金融数据分析系统,优宽量化官方这里为我们储存了海量的期货数据(包含宏观面,基础面以及宏观面),我们可以通过SQL查询数据,然后使用可视化界面配置出多种类型的图表,让分析数据变得简单又有趣。后续将重点展开讲解。

分析工具

Alpha因子分析工具可以方便的调取期货合约数据,进行相关数学统计分析。具体分析公式参考了worldquant公开的行情计算的方法, 基本兼容了其语法(未实现的有说明),并进行了增强。用于快速对时间序列进行运算、验证想法。

调试工具

调试工具页面提供了一个快速实盘测试代码的免费测试环境,目前仅支持JavaScript语言。调试工具测试代码时,代码直接在指定的托管者上运行,最长运行时间为3分钟。可以调用优宽量化交易平台的所有API函数,仅支持一个交易所对象。

账单

这里可以进行账号的充值和管理,帮助大家了解账户的消费记录和可用余额。

工单

大家在优宽量化平台有问题除了发论坛,也可以直接发工单,我们的量化大佬随时在线,帮助大家获得及时回复,进行问题解决。

消息中心

各种消息汇总,包括账户安全信息,论坛回帖、工单信息等消息。

账号设置

这里可以对自己的账号进行更多安全性的设置,另外当我们想要接收实盘的推送消息的时候,点击推送设置,这里可以绑定优宽量化移动APP,邮箱和webhook;同时也具有API 接口设置,基本上实盘的所有操作都可以通过 API 完成,提供了丰富的扩展性,我们后续将展开讲解。

授权管理

可以查看策略的授权状况。

推广返佣

当我们推广优宽量化平台,可以获得注册用户充值的返佣。

2.2 配置交易所和部署托管者

本节内容中,我们首先学习如何添加配置交易所和部署托管者程序。在优宽量化交易平台上做量化交易,运行量化交易实盘,需要三个必要条件,与之对应的操作就是:

  1. 部署托管者程序(托管者程序是实盘的载体,托管者程序负责和期货公司前置机交互,处理底层数据、负责和优宽量化网站通信等)。
  2. 在优宽量化交易平台上配置交易所(商品期货账号、密码、期货公司前置机信息)。
  3. 所需要运行的交易策略(包括交易策略代码,参数配置信息等)。

2.2.1 配置交易所

在控制中心页面,点击交易所就可以跳转到交易所页面。然后点击“添加交易所”按钮,即可添加期货账户信息。例如:选择CTP协议,选择宏源期货主席(看穿式监管),选择对应的网络节点后,系统会自动匹配行情服务器和交易服务器,以及Broker ID,剩下的只需要填入账号密码即可。

  1. 在选择了期货公司之后,行情服务器、交易服务器、Broker ID会自动填充,也可以根据自己的网络,选择期货公司其它的线路节点。通常不用设置,直接使用默认。
  2. 配置期货公司资金账号(CTP登录验证)。
  3. 配置期货公司资金账号的密码(CTP登录验证)。
  4. 设置标签,可以使用默认显示的,如果需要区分不同的账户,可以自行修改。

注意:如果需要配置自定义授权码,勾选配置即可,优宽量化交易平台已经对以下期货公司自动配置,就不用勾选了,只用配置对所在期货公司的资金账号和密码即可。或者可以直接在配置时搜索“看穿式”查询,选择对应的交易所。添加好期货公司配置信息后,在交易所页面会出现已经添加好的信息。

2.2.2 一键租用托管者

部署托管者有两种方式:即通过优宽量化交易平台一键部署到优宽量化平台为我们租用的服务器上。也可以手动部署在自己的设备、服务器、电脑上。在控制中心,点击“托管者”,跳转到托管者管理页面。点击按钮“部署托管者”,跳转到部署托管者页面。

相对于手动部署,一键部署托管者操作比较方便快捷。点击选择购买后,云服务器会自动的为我们进行托管者程序的安装和部署,并且对于python语言,也会安装一些量化交易常用的包和工具。稍微等待片刻,可以看到当托管者展示部署完成的时候,我们就可以直接使用。当我们不想使用的话,可以直接删除,扣费也会自动停止。

2.2.3 本地部署托管者

下面我们来看手动部署托管者。点击进入,可以看到托管者程序支持多种操作系统,可以部署到各种设备上。手动部署托管者可以在我们自己购买的云服务器或者本地的电脑。本地电脑部署托管者可以根据策略运行的需要,随用随停,不需要额外的花费。

首先展示一下Windows本地部署,Windows同时具有命令版和界面版部署。不熟悉命令行的朋友可以选择界面版,根据自身电脑系统点击下载程序,然后解压安装,打开输入地址node.youquant.com/数字串,数字串每位用户均不同,需要根据你的界面显示进行填写。这里很贴心的可以直接点击复制,然后输入本平台的密码,当界面出现“Login OK”的时候,windows界面版托管者就部署完成了。点击进入托管者界面,可以看到我们刚刚部署好的托管者。

对于命令行版,我们也要下载对应的安装包,然后进行解压,接着打开终端,打开安装包的地址,输入这段命令robot.exe -s node.youquant.com/数字串 -p 本平台登录密码。当命令行出现“Login OK”,代表部署成功。

然后,我们看下Mac系统本地布置,这里根据苹果系统下载对应的安装包,进行解压。解压完成打开终端,首先进入安装包所在的文件,这里我们进入桌面,然后输入第一行代码chmod +x robot,接着第二行./robot -s node.youquant.com/数字串 -p 本平台登录密码,这里会显示无法验证开发者,我们这时候需要进入系统偏好设置,安全性和隐私,点击解锁,然后点击仍然运行,再次输入这段代码,当出现“Login OK”部署成功。

2.2.4 云服务器部署托管者

当我们需要长时间运行一个策略的时候,相对来说云服务器是更加适合的。云服务器更新稳定,不容易受外部条件,比如硬件故障,断网或者停电的影响。关于云服务器的选择,一个初级入门,价格低廉的服务器就可以满足基本的策略运行需要。

下面我们使用云服务器进行一下Linux系统的部署。这里有一个托管者部署的帮助文档大家可以参考一下(https://www.youquant.com/bbs-topic/8290),我给大家展示一下。首先登陆云服务器,具体登录云服务器的方法,大家可以根据自己的习惯,这里我们选择在腾讯云直接登录:

  • 第一步,在服务器输入wget下载托管者程序。
bash
wget https://www.youquant.com/dist/robot_linux_amd64.tar.gz
  • 第二步,输入这行代码进行robot解压。
bash
tar -xzvf robot_linux_amd64.tar.gz
  • 第三步,测试托管者运行,输入这两行代码,然后添加优宽登录密码。
bash
chmod +x robot ./robot -s node.youquant.com/数字串
  • 第四步,当出现Login OK就代表云端托管者设置成功。

当然Linux的托管者也是可以部署在本地的,大家也可以尝试一下。

2.2.5 Docker部署托管者

我们来看下使用docker部署托管者,这里我们示范使用云服务器进行部署,首先在云服务器下载安装docker。

  • 在确保docker安装完成后,按照教程首先下载镜像,运行这段代码。
bash
docker pull fmzcom/docker:latest
  • 运行镜像,输入这段代码。
bash
docker run -d --name FMZDocker -e UID=数字串 -e ZONE=CN -e PASSWORD=密码 fmzcom/docker
  • 输入这段代码查看docker日志。
bash
FMZDocker logs

当日志出现Login OK,docker版的托管者也部署成功。

注意: 一个设备或者服务器上可以运行多个托管者,一个托管者上可以运行多个实盘。即使配置最低的阿里云服务器上,同时运行 6~7 个实盘也是没有压力的。部署托管者、配置交易所后,不要轻易修改优宽量化交易平台的账号密码,修改密码会导致配置的交易所配置失效, 需要重新配置交易所信息、重启托管者才能正常使用(已经运行起来的没有影响) 。

2.3 创建管理策略和实盘

当配置好交易所、部署好托管者之后,就只剩下选择所需要让实盘执行的交易策略了。本节学习如何创建管理策略和实盘。实盘可以理解为以托管者程序为底层支持,在托管者上运行起来的策略实例,操作控制配置好的期货账户(通过交易所配置信息)。

2.3.1 创建策略

策略创建方式有 2 种:新建策略和在策略广场中复制别人分享的开源策略。我们先来看创建一个新策略,点击“新建策略”按钮。填入策略名称,点击保存之后,会显示在“策略库”页面。也可以在页面右边选择策略类型和设置策略分组。

也可以从策略广场复制策略,创建到自己的策略库中。点击“策略”按钮会跳转到策略广场页面,可以选择相应策略复制到自己的策略库当中。复制完成的策略,我们可以根据自己的交易理念进行相应的修改和完善。

注意:复制的策略在实盘运行前请务必检测此策略代码的安全性。

2.3.2 管理策略

管理策略时,可以预先设置一些分组,例如设置一个名为“趋势策略”的分组,设置过分组后,就可以点击策略名称,拖动,放入设置好的分组标签中。创建分组既是创建策略标签,在这个分组中就可以看到刚才从策略广场复制创建的策略了。可以点击策略名称,进入策略编辑页面。也可以点击右侧“操作”,对策略库中的策略,编辑,复制、删除、更多(包括分享,出租和运行)。

点击“出租”可以以两种方式进行出售,内部出租:通过设置使用时间,并发个数等信息之后,会生成一个注册码和注册地址,租用该策略的用户拿到这个注册码、注册地址后,打开这个注册地址,输入注册码,即可获得该策略的使用权限,但是无法看到策略代码。公开出售:申请在策略广场上架,需要审核,并且具备上架条件。

其中,点击“编辑”选项可以跳转至该策略编辑页面。点击“公开”可以将策略以两种方式分享,内部分享:设置时间、复制次数等信息之后,会生成一个复制码和复制地址,可以通过打开这个复制地址,输入复制码获取策略。公开分享:点击公开分享后,策略会显示在策略广场中,其他用户可以复制获取。

注意:对于出租、分享等操作创建的注册码、复制码等信息可以从“授权管理”页面进行管理。

2.3.3 创建实盘

如果当前已经配置好了交易所配置信息,已经部署了托管者,策略库中直接创建或者复制过来创建了策略。打开“实盘”选项,点击“新建实盘”按钮,即可跳转至创建实盘页面。我们需要进行相应的“实盘配置”的填写:实盘名称,K线周期,运行策略,托管主机。具体的“交易设置”中:选择交易平台以及相应协议,点击“创建实盘”,就可以跳转到实盘管理页面,在这个页面显示了实盘的运行概况。也可以点击实盘名称,进入实盘运行页面,了解实盘详细情况。一般情况下在编写策略时,需要 Log 一些必要信息或数据,方便在实盘运行时查看。

2.3.4 管理实盘

对运行中的实盘可以开启监控功能:在实盘页面,可以显示每个机器人运行的状态,以及每个实盘具体的盈利额度。有时候策略会因为某些原因出现错误,导致机器人停止,错失开平仓机会,可以在创建机器人后,点击“监控”按钮,一旦出现机器人停止,邮箱与绑定的微信将收到通知。当点击停止按钮,实盘会停止,操作按钮也会发生变化。当点击重启按钮,可以重启该实盘。

注意:以上是手动管理机器人的使用方法,除此之外优宽量化还开放了扩展 API,可以使用程序批量自动管理机器人。

2.4 回测系统

回测就像沙盘推演,它是量化交易中不可或缺的环节。回测的目的是:过滤策略代码BUG、检验策略逻辑是否有效等等,本节将介绍优宽量化的两种回测方式。

2.4.1 模拟级别回测系统

优宽量化交易平台提供的模拟级别回测系统是基于on Tick回测机制运行,即:行情接口数据随着回测中时间序列上移动而逐个放出。把策略程序放在一个行情数据随着回测时间流动而实时变化的沙盒环境中运行。区别于on bar机制,即:新K线BAR出现时,才让策略程序运行一次。回测参数配置上可以设置:

  1. 回测时间范围:确定了回测时间区间。
  2. 默认K线周期:程序默认的K线周期大小。
  3. 日志、收益、图表条数:设置回测系统打印信息数量。
  4. 底层K线周期:模拟级别回测时用于控制回测数据粒度大小的周期参数。
  5. 滑点:成交撮合时的模拟滑点。
  6. 容错:在容错模式回测时,模拟发生错误的概率设置。点击开始回测按钮右侧小三角可以进行容错回测。
  7. 延迟:模拟网络延迟,让数据返回延迟一定时间。
  8. 柱长:K线数据的Bar数量上限。
  9. 手续费相关:手续费费率设置。
  10. 深度、市场成交数据:设置深度档位、是否需要分笔数据。
  11. 设置市场类型:设置回测市场类型。
  12. 添加回测配置按钮。

注意:以上设置,使用鼠标放置在页面控件上,会显示详细说明。

2.4.2 底层K线周期

模拟级别回测是按照回测系统的底层K线数据,按照一定算法在给定的底层K线Bar的最高价、最低价、开盘价、收盘价的数值构成的框架内,模拟出ticker数据插值到这个Bar的时间序列中。实盘级别回测没有底层K线选项(因为ticker数据都是真实的,不用底层K线来模拟生成)。模拟级别回测中,基于K线数据模拟生成的ticker。这个K线数据就是底层K线。在实际使用模拟级别回测中,底层K线周期必须小于设置的默认K线周期。否则,由于底层K线周期较大,生成的ticker数量不足,调用API获取指定周期的K线时,数据会有失真的情况。在使用大周期K线回测时,可以适当调大底层K线周期,以增加回测速度。底层K线如何生成模拟tick数据的相关链接帖子:链接。可以理解为底层K线周期控制着tick的粒度,底层K线周期1分钟或者5分钟,生成的tick数据粒度(密度)肯定是不一样的。粒度越小价格变动跳跃越小,回测精细度越高。但是回测时间会相对较长。如果比较追求回测速度,可以适当调大底层K线周期,以损失一点回测精细度,来加快回测速度。对于中低频趋势策略来说影响不大。

2.4.3 实盘级别回测系统

实盘级别回测使用真实的ticker级别数据。对于基于ticker级别数据回测的策略来说,使用实盘级别回测更贴近真实环境。实盘级别回测,ticker数据是真实记录的数据,并非模拟生成。在回测时逐个ticker数据放出,回测粒度较为精细。回测耗时也相对较多。适合交易频率相对较高的策略、基于盘口数据计算的策略等。实盘级别回测,除了提供tick数据,还支持真实的深度数据回放,回放深度快照数据用于回测一些基于订单薄的策略。同时也快照了市场上逐笔成交数据。由于实盘级别回测,数据量特别大。所以只能支持一定时间范围内的回测,时间范围不能选择太大,否则会超出数据承载限制。实盘级别回测只支持有限的几个交易市场,具体可以参看回测页面上的选项说明。

第 3 章 Python 编程入门

Python 是一个面向对象的脚本语言,凭借极其简洁高效的语言特性,以及数据分析方面的巨大优势,在金融领域得到了广泛的应用。本章内容通过对Python语言学习,将其作为策略开发工具,为期货量化交易提供助力。

3.1 为什么要学习 Python

量化交易离不开数据分析,而Python有很多像talib、pandas、NumPy和matplotlib这些以数据分析和处理为主的第三方库,使得Python成为量化交易策略开发的首选编程语言。从数据获取到策略回测再到实盘交易,Python已经覆盖了整个量化交易应用链。

3.1.1 Python 的特点

完整的量化交易流程可以分为这些步骤:获取数据、分析计算数据、处理数据、下单交易等。在数据分析方面,Python既精于计算又能保持较好的性能,特别是在时间序列分析数据(K线就是时间序列数据)处理,Python有更加简洁高效的优势。另外,比起其他编程语言,Python的语法更加简单容易,不需要大量的计算机系统理论知识,学习曲线比较平缓,即使是非专业的初学者也可以轻松掌握。有意思的是Python代码与英语区别不大,具有极高的可读性。

Python在量化交易领域是一门比较全面而且平衡的编程语言,既可以满足量化交易策略程序运行时的性能,又能轻松处理各种复杂的数学运算、建模分析、统计分析、机器学习等数据处理任务。并且有众多的工具库(包)支持,非常方便实现量化交易策略开发过程中的各种需求。并且市面上的很多量化交易平台,基本上都支持Python编程语言,使得各个量化交易平台所编写出的策略很容易学习、研究、迁移、二次开发。在量化交易领域,Python特点可以归纳为:

  • 语法简单,不需要考虑计算机底层细节问题,初学者更容易入门。
  • 生态丰富,大量成熟的第三方库,带来的是无与伦比的便利。
  • 应用广泛,许多量化交易平台都支持Python语言,方便学习研究。
  • 跨平台、多线程、数据库等方面都有很好的支持。
  • 扩展性强,代码通俗易懂,易于维护。
  • 学习资料十分丰富,有众多活跃的社区可以进行讨论、学习、研究。

3.1.2 优宽量化支持的 Python 版本

优宽量化交易平台支持Python各个版本,如果同时安装了Python 2和Python 3,可以在策略中编写:#!python3或者#!python2即可设置当前使用的Python版本。由于Python官方宣布,2020年1月1日,停止Python2的更新,所以本书代码以Python3为主。

注意:在优宽量化交易平台使用Python语言开发策略,就如同使用原生Python一样,没有任何区别。编写完Python策略后,回测策略或者实际机器人运行策略,如果不在代码中指定Python版本,则默认为Python3执行策略代码。

3.2 Python 基础语法

Python 语言与 C 和 Java 语言有很多相似之处,但又比这2个语言更为简洁。Python 的变量无需声明,可以直接给变量赋值。并且代码块强制以 4 个空格缩进,来区分代码之间的层次。

3.2.1 编码

Python 可以在代码文件开头设置编码,如果不设置则默认为:UTF-8 编码。除非特殊需要,一般不用设置,使用默认 UTF-8 编码即可。你也可以设置为:cp-1252 字符集。

python
# -*- coding: cp-1252 -*-

3.2.2 变量命名

顾名思义变量就是一个可以变化的量,它就像一个盒子,里面可以存放各种东西。在编写 Python 代码时,对于声明的变量,在变量名称命名时需要注意,以下是 Python 变量命名规则:

  1. 变量名是区分大小写的。
  2. 变量名只能由字母、数字、下划线组成,且不能以数字开头。
  3. 变量名不能包含空格。
  4. Python 的关键字和函数名不能作为变量名。
  5. 避免使用小写字母 l 和大写字母 O,否则可能会错看成 1 和 0。
python
name = "优宽量化"

注意:Python 的变量在赋值的时候不需要类型声明。在使用该变量之前,必须对其赋值,赋值之后变量才会创建。

3.2.3 关键字

在使用 Python 语言编写代码时,有一些特殊的名词是不能作为变量名、函数名或者其他用途使用的,这些名词叫做“关键字”或者“保留字”。Python 自带的 keyword 模块可以输出这些系统关键字。例如我们在编写的 Python 策略代码中使用:

python
import keyword def main(): Log(keyword.kwlist)

输出结果为:

python
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

注意:这些 'False'、'None'、'True' 等等名词,都有它们自己的用途,是已经预先保留的关键字,不能再用作其他的命名。另外 Python 是一种动态语言,关键字会随着时间的变化而改变。

3.2.4 注释

为了提高代码可读性,可以在代码中添加解释和说明。良好的代码注释可以传达代码作用和上下文关系,便于理解策略逻辑,也方便日后维护策略。Python 的单行注释由一个“#”号开头,之后跟上注释文本:

python
# 第一个注释,单行注释,Log 函数是用于输出一条信息的函数 Log("你好,发明者!")

如果注释的内容比较多,可以使用多行注释三个连续的单引号'''或者三个连续的双引号""",一次性注释多行的内容(包含一行),具体格式如下:

python
''' 第 1 行注释 第 2 行注释 '''

程序在运行时会忽略已经注释的代码,所以基本不会影响代码的运行速度。除此之外,注释还以帮助调试程序 BUG,如果觉得某段代码可疑,可以先把该段代码注释起来,代码可以再次正常运行,则说明 BUG 是由于这段代码引起的。合理的利用注释,可以缩小 BUG 的范围,提高调试策略的效率。

3.2.5 缩进

Python 的缩进是一种独特的语法,也是该语言的一大特点,它没有像其他语言一样用花括号{}分隔代码块,而是使用 Tab 键或 4 个空格进行代码缩进,以此来控制代码的作用域,相同缩进行的代码处于同一个作用域范围。

需要注意的是,空格和 tab 缩进不能混在一起用,否则会报错。使用空格缩进时,如果空格数量不一致,也会引起报错,例如:

python
if True: print("Answer") print("True") else: print("Answer") print("False") # 缩进不一致,会导致运行错误

3.2.6 代码块

缩进行相同的一组语句构成一个代码块,很多关键字例如:while、def、class、if 等关键字使用时,在冒号“:”后换行,缩进相同的代码构成代码块。例如:

python
if 1 < 2: Log("1 小于 2 为真") Log("计算一下 2 比 1 大多少? ") # 代码块 Log("计算:2-1=", 2-1) # 输出1 elif 1 > 2: # 1>2 结果是 False Log("1 大于 2 为真") # 所以这个条件不会触发

输出结果为:

python
1 小于 2 为真 计算一下 2 比 1 大多少? 计算:2-1= 1

3.2.7 空行

通常在编写代码时,习惯于在函数之间或者类成员函数之间使用空行分隔,表示新的一段代码。这个并不是语法,仅仅是编写策略时的习惯,便于之后代码阅读,主要作用是分隔两段功能或者含义不同的代码。

3.2.8 导入模块

模块就像已经制造好的汽车零部件,通过生产线把各个零件组装成一体。编程也是同样的道理,在编写策略时,可以通过“import”导入模块。模块的好处是提高了策略开发效率,一般写在代码开头,有以下四种形式:

  • 整个模块导入,写为:import module
  • 从某个模块中导入某个函数,写为:from module import def
  • 从某个模块中导入多个函数,写为:from module import def1,def2
  • 某个模块中的全部函数导入,写为:from module import *

3.3 Python 变量和数据类型

变量其实是内存中的值,当变量创建的时候,Python 会自动识别值的类型,并根据类型分配到指定的内存中。变量可以存储不同的数据类型,包括:数字、字符串、列表、字典等等。

3.3.1 变量

Python 中变量不需要声明,但每个变量在使用之前必须赋值,变量赋值之后该变量才成功创建。使用“=”等号给变量赋值,等号左边为变量名称,等号右边为储存在变量中的值。例如:

python
pi = 3.1415926535897 name = "圆周率" year = 2019 Log(pi, name, year)

输出结果为:

python
3.1415926535897 圆周率 2019

3.3.2 标准数据类型

Python 中的变量仅仅只是一个名字(name),它关联了内存中的一个数据(object)。而变量类型实际上指的是该变量关联在内存中数据(object)的类型。Python3 有多个标准类型,它们分别是:

  • Number(数值)
  • Bool (布尔)
  • String(字符串)
  • List(列表)
  • Tuple(元组)
  • Set(集合)
  • Dictionary(字典)

通常在编写一般的策略代码时用的最多的就是 Number(数值)、Bool (布尔)、String(字符串)、List(列表)、Dictionary(字典)这些数据类型。在接下来的章节中将重点讲解这些常用的基本数据类型的使用。

3.3.3 Number(数字)

Number 数据类型用于储存数字,常用的数字类型为:整型(int)、浮点型(float)。整型就是不带小数点的数字,正整数和负整数都是整数类型。数字是不可变类型,一旦改变其数据类型的值,那么就意味着重新分配内存空间。

python
value_int = 10 # int 类型,整型变量,简单理解就是整数字变量。 value_float_1 = 3.14 # float 类型,浮点型变量,简单理解就是有小数部分的变量。 Value_float_2 = 3.00 # 值为 3.00 的变量也是浮点类型变量。

由于 Python 属于动态语言,很多时候需要判断对象的类型,可以使用内置的 type 函数,调用它就能查询对象类型信息。例如:

python
def main(): value_int = 10 # int 类型,整型变量,简单理解就是整数字变量。 value_float = 3.14 # float 类型,浮点型变量,简单理解就是有小数部分的变量 # 值为 3.00 的变量也是浮点类型变量。 Log(type(value_int)) # 打印变量 value_int 的类型。 Log(type(value_float)) # 打印变量 value_float 的类型。

输出结果为:

python
<class 'int'> <class 'float'>

这个例子中,分别定义了 int(整数)类型和 float(浮点)类型变量,Log 函数打印了type()函数返回的变量类型。无论是整型还是浮点型变量,都是用来表示数字,用于计算。注意:在浮点型、整型变量混合计算时,Python 会把整型先转换为浮点型。

3.3.4 Bool(布尔类型)

布尔类型变量用于表示真(True)和假(False)两种状态。在Python中,布尔类型的变量可以直接使用关键字True和False来表示。

python
is_open = True is_closed = False Log(is_open) # 输出: True Log(is_closed) # 输出: False

3.3.5 String(字符串)

字符串是若干个字符的集合,表示文本的数据类型,Python 中的字符串用单引号''或者双引号""括起来,可以使用反斜杠\转义特殊字符。字符串的第一个索引是 0,第二个索引是 1,依此类推。也可以对字符串相加、截取、复制等操作。例如:

python
def main(): str = 'hello fmz' Log(str) # 输出字符串 Log(str[0:-1]) # 输出第一个到倒数第二个的所有字符 Log(str[0]) # 输出字符串第一个字符 Log(str[2:5]) # 输出从第三个开始到第五个的字符 Log(str[2:]) # 输出从第三个开始的后的所有字符 Log(str * 2) # 输出字符串两次 Log(str + "!!") # 连接字符串

输出结果为:

python
hello fmz hello fm H L lo llo fmz hello fmzhello fmz hello fmz!!

注意:Python 中的字符串不能改变,也就是说当字符串被创建完成后,就不能再改变它的状态了。例如在下面的例子中,重新给字符串的第一个索引位置赋值,会引起报错:

python
def main(): str = 'hello fmz' Log(str[0]) str[0] = 'H'

输出结果为:

python
h Traceback (most recent call last): File "<string>", line 1481, in Run File "<string>", line 9, in <module> File "<string>", line 4, in main TypeError: 'str' object does not support item assignment

3.3.6 List(列表)

列表就像是备忘清单,每一个编号记录着清单详情,它是有序数据的集合,通过编号就可以引用列表中的数据。列表也是策略开发中使用比较频繁的数据类型,商品期货 API 接口返回的大部分数据都是以列表形式呈现。Python 的列表可以存储不同类型的元素,包括:数字、字符串、列表、字典等等。
列表使用方括号“[]”包含元素,其中每个元素中间使用逗号“,”作为间隔符。和字符串类似,列表也可以通过索引获取其中的元素,也可以使用索引截取列表中的一部分,列表被截取后返回一个新的列表。例如:

python
def main(): list = ["abc", 10, 3.14, ["1", 2, 3.0]] Log(list) # 输出整个列表 Log(list[0]) # 输出列表中的第一个元素 Log(list[1:3]) # 从第二个元素开始输出到第三个元素 Log(list[2:]) # 从第三个元素开始输出所有元素 Log(list * 2) # 两个 list 列表连接在一起 Log(list[-1][-1]) # 输出列表中嵌套的列表的最后一个元素 Log(list + list[-1]) # 连接两个列表

输出结果为:

python
['abc', 10, 3.14, ['1', 2, 3.0]] abc [10, 3.14] [3.14, ['1', 2, 3.0]] ['abc', 10, 3.14, ['1', 2, 3.0], 'abc', 10, 3.14, ['1', 2, 3.0]] 3.0 ['abc', 10, 3.14, ['1', 2, 3.0], '1', 2, 3.0]

列表中的元素是可以改变的,包括:索引、切片、增删改查等基本操作。例如:

python
def main(): list = ["abc", 10, 3.14, ["1", 2, 3.0]] Log("修改 list[0]之前:", list) list[0] = "hello fmz!" Log("修改 list[0]之后:", list)

输出结果为:

python
修改 list[0]之前:['abc', 10, 3.14, ['1', 2, 3.0]] 修改 list[0]之后:['hello fmz!', 10, 3.14, ['1', 2, 3.0]]

Python 有很多适用于列表的函数,例如:len()函数可以获取列表里面有多少个元素,append()函数可以向列表尾部添加一个元素,pop()函数可以移除一个元素,默认移除最后一个元素。例如:

python
def main(): list = ["abc", 10, 3.14, ["1", 2, 3.0]] list.append("aaa") Log(list) list.pop() Log(list)

输出结果为:

python
['abc', 10, 3.14, ['1', 2, 3.0], 'aaa'] ['abc', 10, 3.14, ['1', 2, 3.0]]

3.3.7 Dictionary(字典)

字典也是 Python 语言常用的一种数据结构,它是存放具有映射关系的数据,定义了键和值之间一对一的映射关系,它是一个无序、可变和有索引的集合。字典的数据用花括号“{}”包括,结构形式如下:

python
def main(): dict1 = { "name": "TOM", "age": 18, "address": { "city": "xxx", "street": "yyy" } } Log(dict1) Log("姓名:", dict1["name"], "年龄:", dict1["age"], "地址,城市:",dict1["address"]["city"], "街道:", dict1["address"]["street"])

可以看到,字典中的数据是一个键名对应一个键值,例如:name 这个键名(keyName)对应 TOM 这个键值(keyValue)。和列表类似,字典也可以嵌套,如上所示:address 这个键名对应的键值也是一个字典。输出结果为:

python
{'name': 'TOM', 'age': 18, 'address': {'city': 'xxx', 'street': 'yyy'}} 姓名:TOM 年龄:18 地址,城市:xxx 街道:yyy

注意:因为字典是通过键来访问值的,所以字典中键名(keyName)必须是唯一的,并且键名必须使用不可变类型。也可以使用内置的函数 keys() 输出所有键名,使用函数 values()输出所有键值,例如:

python
def main(): dict1 = { "name": "TOM", "age": 18, "address": { "city": "xxx", "street": "yyy" } } Log(dict1.keys()) Log(dict1.values())

输出结果为:

python
dict_keys(['name', 'age', 'address']) dict_values(['TOM', 18, {'city': 'xxx', 'street': 'yyy'}])

3.3.8 Python 数据类型转换

Python 提供了几种数据类型转换函数,可以将一种数据类型转变为另一种数据类型。比如:浮点数转换为整数、整数转换为字符串等等。通常情况下不同的数据类型是可以相互转换的,这也意味着:整数可以转换为浮点数、字符串也可以转换为整数等等。

  1. 将 x 转换为 int 类型:ret = int(x)
  2. 将 x 转换为 float 类型:ret = float(x)
  3. 将 x 转换为 string 类型:ret = str(x)
python
def main(): pi = 3.14 Log(int(pi)) strPi = "3.14" Log(float(strPi)) Log(type(str(pi)))

输出结果为:

python
3 3.14 <class 'str'>

3.4 Python 数据运算

计算机里面的数据运算与数学运算类似,数据运算也是有优先级的。但Python的数据运算更具有丰富多样性,支持以下常用数据运算:

  • 算术运算
  • 关系(比较)运算
  • 赋值运算
  • 逻辑运算

编程语言中的运算符有很多,在Python中有很多类型的运算符,包括:算术运算符、关系运算符、赋值运算符、逻辑运算符。

3.4.1 算术运算符

算术运算也就是数学运算,其运算规则与数学运算规则一样。算术运算符就是用来对操作数进行数学运算,主要有:+、-、*、/、%、**、//等运算符。例如:

python
def main(): a = 3 b = 2 Log("加法运算符 + 计算结果:", a + b) Log("减法运算符 - 计算结果:", a - b) Log("乘法运算符 * 计算结果:", a * b) Log("除法运算符 / 计算结果:", a / b) Log("求模运算符 % 计算结果:", a % b) # 计算相除(a/b)时的余数。 Log("幂运算符 ** 计算结果:", a ** b) # 计算 a 的 b 次方。 Log("整除运算符 // 计算结果:", a // b) # 向下取接近除数的整数。

输出结果为:

python
加法运算符 + 计算结果: 5 减法运算符 - 计算结果: 1 乘法运算符 * 计算结果: 6 除法运算符 / 计算结果: 1.5 求模运算符 % 计算结果: 1 幂运算符 ** 计算结果: 9 整除运算符 // 计算结果: 1

3.4.2 关系运算符

关系运算也称比较运算,关系运算符主要是用于对操作数进行数字大小关系比较。主要有:==、!=、>、<、>=、<=等运算符。如果关系运算成立,返回 True(真),反之返回 False(假)。例如:

python
def main(): a = 3 b = 2 c = 2 Log("c:", c, "b:", b, "使用 c == b 判断,两边操作数是否相等,返回:", c == b) Log("a:", a, "b:", b, "使用 a == b 判断,两边操作数是否相等,返回:", a == b) Log("a:", a, "b:", b, "使用 a != b 判断,两边操作数是否不等,返回:", a != b) # a 值为 3,b 值为 2,3 > 2,关系表达式是成立的,返回结果 True,即为真。 Log("a:", a, "b:", b, "使用 a > b 判断,两边操作数大小关系,返回:", a > b) # 2 < 2 不成立,返回 False,即为假。 Log("c:", c, "b:", b, "使用 c < b 判断,两边操作数大小关系,返回:", c < b) Log("c:", c, "b:", b, "使用 c >= b 判断,两边操作数大小关系,返回:", c >= b) Log("b:", b, "a:", a, "使用 b <= a 判断,两边操作数大小关系,返回:", b <= a)

输出结果为:

python
c: 2 b: 2 使用 c == b 判断,两边操作数是否相等,返回:True a: 3 b: 2 使用 a == b 判断,两边操作数是否相等,返回:False a: 3 b: 2 使用 a != b 判断,两边操作数是否不等,返回:True a: 3 b: 2 使用 a > b 判断,两边操作数大小关系,返回:True c: 2 b: 2 使用 c < b 判断,两边操作数大小关系,返回:False c: 2 b: 2 使用 c >= b 判断,两边操作数大小关系,返回:True b: 2 a: 3 使用 b <= a 判断,两边操作数大小关系,返回:True

3.4.3 赋值运算符

赋值运算是把右边的值传递给左边的变量,可以直接传递,也可以经过运算后再传递,比如加减乘除、函数调用、逻辑运算等。赋值运算符主要有:=、+=、-=、*=、/=、%=、**=、//=。例如:

python
def main(): a, b = 3, 2 a = b # 把 b 的值赋值给 a,打印 a,显示 2 Log(a) a, b = 3, 2 a += b # 等价于 a = a + b,打印 a,显示 5 Log(a) a, b = 3, 2 a -= b # 等价于 a = a – b,打印 a,显示 1 Log(a) a, b = 3, 2 a *= b # 等价于 a = a * b,打印 a,显示 6 Log(a) a, b = 3, 2 a /= b # 等价于 a = a / b,打印 a,显示 1.5 Log(a) a, b = 3, 2 # 等价于 a = a % b,打印 a,显示 1 a %= b Log(a) a, b = 3, 2 # 等价于 a = a ** b,打印 a,显示 9 a **= b Log(a) a, b = 3, 2 # 等价于 a = a // b,打印 a,显示 1 a //= b Log(a)

运行结果为:

python
2 5 1 6 1.5 1 9 1

3.4.4 逻辑运算符

Python 中的逻辑运算与高中数学的逻辑运算类似,比如 a 为真命题,b 为假命题,那么“非 a”为假,“a 且 b”为假,“a 或 b”为真。Python 的逻辑运算符有 andornot

and 又称为“与”操作符,假设有 x、y(x、y 可以是表达式,也可以是数值),x and y这样就组成了一个逻辑表达式。如果 x 为 False,那么 x and y 返回 False,否则返回 y 的计算值。例如:

python
def main(): x = 10 y = 20 z = False Log(x and y) # x and y 这个逻辑表达式返回的值为 y 的值,即 20 Log(z and y) # z 值为 False,为假,则 z and y 这个逻辑表达式返回的值为 False

输出结果为:

python
20 False

or 又称为“或”运算符。同样假设有 x、y 两个表达式或者数值,x or y组成一个逻辑表达式。如果 x 为 True,那么 x or y 返回 x 的值,否则返回 y 的计算值。例如:

python
def main(): x = 10 y = 20 z = False Log(x or y) # x 的值为 10,为真,表达式 x or y 的值为 10 Log(z or y) # z 的值为 False,为假,表达式 x or y 的值为 y 的计算值,即 20

输出结果为:

10 20

最后来看一下 not 操作符,not 操作符又称“非”操作符。假设有 x 这个表达式或者数值,not x组成一个逻辑表达式。如果 x 为 True,则 not x 返回 False,如果 x 为 False,则 not x 返回 True。例如:

python
def main(): x = 10 y = 20 z = False Log(not (x or y)) # x or y 为真,所以 not (x or y) 为假 Log(not (z and y)) # z and y 为假,所以 not (z and y) 为真。

输出结果为:

python
False True

在 Python 中,innot in 是用于逻辑判断的另一种方式,可以简单理解为 in 左边的内容是否存在于 in 右边的内容,如果存在返回 True,如果不存在返回 False。例如:

python
def main(): a = "hello FMZ!" b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] c = {"name": "FMZ", "age": 10} Log('a' in a) Log('F' in a) Log(10 in b) Log(0 in b) Log('say' in c) Log('name' in c)

输出结果为:

python
False True False True False True

3.4.5 运算符优先级

Python 的运算优先级是一个很重要的概念,在一个表达式中有多个运算符时,算数优先级决定了先执行哪个运算符。

运算符说明Python运算符优先级
乘方指数运算**7
乘、除、取模、整除乘法、除法、取模、整除*、/、%、//6
加、减加法、减法+、-5
比较运算符比较操作<=、<、>、>=4
等于运算符等于和不等于==、!=3
赋值运算符赋值和复合赋值=、+=、-=、*=、/=2
逻辑运算符逻辑非、逻辑与、逻辑或not、and、or1

上表从高到低列出了运算符的优先级,同一行运算符的优先级是从左到右顺序排列。先执行具有较高优先级的运算,然后执行较低优先级的运算。如果想要改变默认的运算顺序,可以使用圆括号,例如:11 + 2 -5 * (3 + 2 – (5 + 1))中,小括号最内层的 5+1 最先计算。

3.5 Python 数字和字符串

数字和字符串几乎是所有编程语言里面最基本的数据类型,也是通过策略代码实现量化交易的基础。Python语言中有很多处理数字和字符串的函数,这些内置的函数通常能解决大多数策略开发需求。

3.5.1 数字类型转换

Python内部提供了几种强制类型转换的函数,可以将一种数据类型转换为另一种数据类型,其中有些类型是可以相互转换的。例如:整型转换为浮点型,浮点型转换为整型,也可以将部分字符串转换为数字。

python
def main(): Log(int(3.14)) # 将浮点型转换为整型 Log(float(10)) # 将整型转换为浮点型 Log(int('100')) # 将字符串转换为整型

3.5.2 内置数学函数

Math库提供了很多复杂数学运算函数,包括:自然常数、圆周率、绝对值,四舍五入等函数。这些函数并不能直接访问,需要使用“import”导入math模块,通过静态对象调用才能使用。

python
import math # 导入 math 数学库 def main(): a = -10 Log(math.e) # 打印自然常数 Log(math.pi) # 打印圆周率 Log(abs(a)) # 计算 a 的绝对值 Log(math.ceil(math.pi)) # math.ceil(x)返回数值变量 x 的上入整数 Log(math.exp(1)) # math.exp(x) 返回自然常数 e 的 x 次幂 Log(math.fabs(a)) # math.fabs(x)返回 x 的绝对值,返回值为浮点类型 Log(math.floor(math.pi)) # math.floor(x)返回数值变量 x 的下舍整数 Log(math.log(100, 10)) # math.log(x, y) 返回以 y 为基数的 x 的对数 Log(max(a, math.pi)) # 求传入的参数中的最大值,参数可以是列表 Log(min([a, math.pi, 0])) # 求传入的参数中的最小值,参数可以是列表 Log(math.modf(math.pi)) # modf(x)返回 x 的整数部分和小数部分 Log(round(math.pi, 1)) # round(x, n) 计算浮点数 x 的四舍五入值 Log(math.sqrt(100)) # math.sqrt(x)计算 x 的平方根

输出结果:

python
2.718281828459045 3.141592653589793 10 4 2.718281828459045 10.0 3 2.0 -10 3.1

3.5.3 访问字符串中的值

字符串是由多个字符组成,字符与字符之间是有顺序的,而这个顺序号被称为索引。字符串的索引是从0开始,以此类推。例如有一个字符串stringA = “Hello FMZ”,那么它在内存中的实际存储顺序如下:

字符串HelloFMZ!
索引012345678910

如果要选取字符串区间内容,则需要遵循左闭右开的原则,即从“起始”位开始,到“结束”位的前一位结束(不包含结束位本身)。倒数第一个元素的索引是-1。

python
def main(): stringA = "Hello FMZ!" Log(stringA[6:9]) Log(stringA[-1])

输出结果:

python
FMZ !

3.5.4 拼接字符串

在Python中字符串拼接有很多种方式:直接通过加号(+)拼接,或者通过逗号(,)拼接。但如果需要拼接大量字符串时,这两种方法就非常低效了,这时候可以使用Python内置的join()函数进行拼接。

python
def main(): a = "hello," b = "FMZ!" Log(a + b) Log(a, b) Log(' '.join([a, b]))

输出结果:

python
hello,FMZ! hello, FMZ! hello, FMZ!

3.5.5 其他常用函数

除此之外,Python还有一些其他常用的函数用于处理字符串:

  • len()函数:用于返回字符串的字符个数。
  • lower()函数:将字符串中的所有字符转换为小写。
  • upper()函数:将字符串中的所有字符转换为大写。
  • replace()函数:替换字符串中部分字符。
  • split()函数:字符串分割函数。
python
def main(): stringA = "Hello FMZ!" Log(len(stringA)) Log(stringA.lower()) Log(stringA.upper()) Log(stringA.replace("FMZ", "优宽量化")) arr = stringA.split(" ") # 以空格分割stringA Log(arr[0]) Log(arr[1])

输出结果为:

python
10 hello fmz! HELLO FMZ! Hello 优宽量化! Hello FMZ!

3.6 Python 列表和字典

列表和字典都是 Python 语言最常用的数据结构,列表是有序数据的集合,字典是无序数据的集合。列表中每一个元素都有它的索引,字典中每个元素都包含键值对。

3.6.1 列表索引

列表是 Python 中最基本的数据结构,列表中的每个元素都有一个索引,即一个数值,用于标记列表中元素的位置,第一个元素的索引为 0,第二个元素的索引为 1,依次类推。列表中的元素可以是不同类型的数据,例如:

python
def main(): arr = ["Tom", 18, ["12345678@qq.com", 135123456789]] Log("姓名:", arr[0]) Log("年龄:", arr[1]) Log("联系方式,邮箱:", arr[2][0]) Log("联系方式,电话:", arr[2][1])

输出结果为:

姓名:Tom 年龄:18 联系方式,邮箱:12345678@qq.com 联系方式,电话:135123456789

3.6.2 列表切片

通过列表切片,可以获取一个列表中的部分元素。列表切片与字符串类似,也是需要遵循左闭右开的原则,即从“起始”位开始,到“结束”位的前一位结束(不包含结束位本身)。倒数第一个元素的索引是-1。也可以用len()函数获取列表中的元素个数:

python
def main(): arr = [1, 2, 3, 4, 5, 6] Log(arr[1:3]) Log(arr[-1]) Log(len(arr))

输出结果为:

python
[2, 3] 6 6

3.6.3 列表修改删除

列表是可变的数据类型,列表中的元素可以被修改、删除。直接使用赋值操作符就能修改列表中的元素。例如把列表中索引为 1 的元素修改为 22(原本是 2)。

python
def main(): arr = [1, 2, 3, 4, 5, 6] arr[1] = 22 Log(arr)

输出结果为:

python
[1, 22, 3, 4, 5, 6]

Python 提供了 4 种删除列表元素的函数,每一种方法分别适应于不同的场景。包括:del、pop、remove、clear。例如:

python
def main(): arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] del arr[1] # 根据索引值删除元素 Log(arr) arr.pop() # 根据索引值删除元素,默认删除最后一个元素 Log(arr) arr.remove(5) # 根据元素值进行删除 Log(arr) arr.clear() # 删除列表所有元素 Log(arr)

输出结果为:

python
[1, 3, 4, 5, 6, 7, 8, 9, 10] [1, 3, 4, 5, 6, 7, 8, 9] [1, 3, 4, 6, 7, 8, 9] []

3.6.4 二维列表

列表中的元素可以是任何一种数据类型,其中包括列表。如果一个列表中包含列表,那么这个列表就是二维列表。例如:

python
def main(): arr = [[1, 2], [3, 4], [5, 6]] Log(arr[0][0]) # 获取 arr 列表第 1 个元素中的第 1 个元素 Log(arr[1][0]) # 获取 arr 列表第 2 个元素中的第 1 个元素 Log(arr[2][0]) # 获取 arr 列表第 3 个元素中的第 1 个元素

在量化交易中心,二维列表多用于技术指标中。如果要获取二维列表中列表元素里面的值,可以参考上面的例子。输出结果为:

python
1 3 5

MACD 指标一共有 3 个数据,包括:dif 线、dea 线、macd 量柱。如果使用 talib 库中的 MACD 指标计算,返回的则是一个二维数组。第一个元素就是 MACD 指标中的 dif 线的数据,第二个元素是 dea 线的数据,第三个元素是量柱数据。例如:

python
def main(): macd = [[1, 2, 3], [1.1, 2.2, 3.3], [1.11, 2.22, 3.33]] # MACD 值 Log("dif 线:", macd[0]) Log("dea 线:", macd[1]) Log("macd 量柱:", macd[2]) Log("当前 Bar 的 dif 指标值:", macd[0][-1])

输出结果为:

python
dif 线:[1, 2, 3] dea 线:[1.1, 2.2, 3.3] macd 量柱:[1.11, 2.22, 3.33] 当前 Bar 的 dif 指标值:3

3.6.5 列表增加元素

在 Python 中 append 函数用来想列表尾部追加元素,如果所追加的元素是个列表,那么这个列表将作为一个整体来追加。例如:

python
def main(): arr = [1, 2, 3, 4] arr.append("100") Log(arr) arr.append([99, 100]) Log(arr)

输出结果为:

python
[1, 2, 3, 4, '100'] [1, 2, 3, 4, '100', [99, 100]]

注意:列表增加元素后,列表的长度也会自动增加。

3.6.6 列表反向排序

reverse 是列表中一个非常实用的内置函数,它可以让列表中的元素反向排序,该函数可以返回一个逆序序列的迭代器(用于遍历该逆序序列)。例如:

python
def main(): arr = [1, 2, 3, 4] arr.reverse() Log(arr)

输出结果为:

python
[4, 3, 2, 1]

3.6.7 创建字典

字典是也一种可变的数据类型,字典中的键和值是一一对应的,其中键( key)就是数据的名字,值就是数据的内容。字典使用冒号“:” 分隔键( key)值( value)对,然后每个键值对用逗号“,” 分隔。最后使用花括号“{}”包裹起来。例如:

python
dict = {key1 : value1, key2 : value2 , key3 : value3}

字典中键( key)必须是唯一的,值( value)是可以重复的,值也可以是任何数据类型,但是键必须是不可变的数据类型,例如:数值,字符串都可以作为键。字典的创建方式很简单,例如:

python
def main(): boy = { "name" : "tom", "age" : 18, "Email" : "123456789@qq.com" } Log(boy)

输出结果为:

python
{'name': 'tom', 'age': 18, 'Email': '123456789@qq.com'}

3.6.8 访问字典元素

汉语字典可以通过拼音查汉字,Python 的字典访问也是基于这个原理,可以通过键(key)访问字典中的值( value)。具体方法是:在字典变量名后面写中括号“[]”,然后在中括号内写要访问的键名。例如:

python
def main(): boy = { "name" : "tom", "age" : 18, "Email" : "123456789@qq.com" } Log("名字:", boy["name"]) Log("年龄:", boy["age"]) Log("地址:", boy["address"]) # 访问字典中不存在的键,会报错。

输出结果为:

python
名字:tom 年龄:18 Traceback (most recent call last): File "<string>", line 1481, in Run File "<string>", line 15, in <module> File "<string>", line 10, in main KeyError: 'address'

注意:如果访问字典并不存在的键,程序就会报错。

3.6.9 字典添加修改元素

字典是可变的数据类型,这也就意味着字典可以增删改查。那么如何增加和修改字典中的元素呢?和访问字典中键值的方式一样,只不过是对其赋值操作。例如:

python
def main(): boy = { "name" : "tom", "age" : 18, "Email" : "123456789@qq.com" } boy["height"] = "180cm" # 如果字典中没有该键,就创建一个键并赋值 boy["Email"] = "abcdefg@qq.com" # 如果字典中该键存在,就更新该键值 Log(boy)

输出结果为:

python
{'name': 'tom', 'age': 18, 'Email': 'abcdefg@qq.com', 'height': '180cm'}

3.6.10 字典删除元素

Python 字典有 4 种删除元素的方法,可以适应于不同的应用场景。其中一个 del 关键字,del 是全局方法,既能删除单个元素又能删除字典,例如:

python
def main(): boy = { "name" : "tom", "age" : 18, "Email" : "123456789@qq.com" } del boy["age"] # 删除字典中的键值对 Log(boy) del boy # 删除整个字典 Log(boy)

输出结果为:

python
{'name': 'tom', 'Email': '123456789@qq.com'} Traceback (most recent call last): File "<string>", line 1481, in Run File "<string>", line 16, in <module> File "<string>", line 11, in main UnboundLocalError: local variable 'boy' referenced before assignment

上面的例子中,字典的 age 键和键值,都被删除了。使用 del 关键字,如果后面跟的要删除的内容是一个字典( del boy) ,那么删除的就是整个字典 boy。如果要清空一个字典内容,可以直接调用字典的 clear 函数。例如:

python
def main(): boy = { "name" : "tom", "age" : 18, "Email" : "123456789@qq.com" } boy.clear() # 清空字典的操作 Log(boy)

输出结果为:

python
{}

注意:字典值可以是任意 python 对象,但是字典键必须是不可变类型。并且字典中相同的键不允许出现两次,如果创建一个字典时出现两次相同的键,那么只会记录最后一个。

python
def main(): boy = { "name" : "tom", "age" : 18, "name" : "jack", "Email" : "123456789@qq.com" } Log(boy)

输出结果为:

python
{'name': 'jack', 'age': 18, 'Email': '123456789@qq.com'}

3.7 Python 条件语句和循环语句

编程与生活息息相关,比如红灯停,绿灯行就是条件语句。条件语句和循环语句在量化交易也很常用,策略之所以会实时根据行情变化,发现潜在的交易机会,是因为它在循环语句中重复的判断交易信号是否成立。之所以会自动下单交易,是因为它可以根据条件语句执行下单动作。

3.7.1 条件语句

计算机在执行代码时,是按照从上到下顺序,一行一行的执行。但很多时候这种按顺序结构执行代码有很大的局限性。假如有一个策略逻辑是只有在均线金叉时才能买入,这个时候就需要用到 if 条件语句了。

if 语句是选择结构,它就像是一个开关,通过对条件进行判断,然后根据判断结果执行不同的代码,这个条件可以是单一的值,也可以是由运算符组成的复杂语句,只要这个条件能得到一个值,if 语句都能判断它是否成立。如果条件成立,那么就会执行 if 里面的代码块,否则就会跳过 if 语句。

python
def main(): a = 5 b = 10 if a > 6: Log(a) if b > 6: Log(b)

输出结果为:

python
10

通常情况下,if 和 else 可以组合成“如果...否则...” 的条件语句。如果条件成立,那么就会执行 if 里面的代码块,else 里面的代码块将会被跳过不执行。如果条件不成立,那么 if 里面的代码块将会被跳过不执行,然后执行 else 里面的代码块。

python
def main(): a = 5 b = 10 if a > 6: Log(a) else: Log(b)

输出结果为:

python
10

还有一种 if elif else 形式的语句,这种语句依次判断表达式的值,当某一个表达式的值为真时,则执行对应的代码块。如果所有的表达式为假,则执行 else 里面的代码块。

python
def main(): a = 5 b = 10 if a > 100: Log(a) elif b > 100: Log(b) else: Log('a、 b 都小于 100')

输出结果为:

python
a、 b 都小于 100

if 语句同样可以用于嵌套,在嵌套 if 语句中,可以把 if...elif...else 结构放在另外一个 if...elif...else 结构中。

python
def main(): boy = { "name" : "tom", "age" : 18, "Email" : "123456789@qq.com" } if boy["age"] == 20: Log("tom is 20 years old!") elif boy["age"] == 19: Log("tom is 19 years old!") else: Log("tom is not 20 or 19 years old!I don`t know his age.") if boy["Email"] == "123456789@gmail.com": Log("Although I don't know Tom's age, I can email him!") Log("123456789@gmail.com") elif boy["Email"] == "123456789@qq.com": Log("Although I don't know Tom's age, I can email him!") Log("123456789@qq.com") else: Log("I don`t know his email address!")

输出结果为:

python
tom is not 20 or 19 years old!I don`t know his age. Although I don't know Tom's age, I can email him! 123456789@qq.com

3.7.2 循环语句

循环是让计算机重复的做某件事情,Python语言提供了2种循环语句,分别是for循环和while循环。for一般用于有限次数循环,while一般用于不定次数循环,某些条件触发退出循环。

for循环可以遍历指定的次数,通常用于遍历一个有限的数据或者处理有限的任务,例如字符串、列表、字典等。例如使用for循环语句将一个字符串的字符逐个打印出来:

python
def main(): stringA = "abc123" for char in stringA: Log(char) else: Log("打印结束")

输出结果为:

a b c 1 2 3 打印结束

如果数据是无限的,或者需要处理无限重复的任务,可以使用while循环语句。while循环语句在每次开始循环之前,都会先判断条件语句是否为真,只要条件语句为真,就会执行循环体内的代码块。例如:

python
def main(): a = 0 while a < 100: a = a + 1 Log(a)

输出结果为:

1 2 ... 100

3.7.3 break 语句

break语句是循环语句的搭档,当循环时出现break语句,循环就会立刻终止。如果是双层for循环,那么break语句只会终止当前的for循环。例如:

python
def main(): arr1 = [1, 2, 3, 4] arr2 = ["a", "b", "c", "d"] for i in arr1: for j in arr2: if j == "b": break Log("i:", i, " j:", j)

输出结果为:

i: 1 j: a i: 2 j: a i: 3 j: a i: 4 j: a

上面的例子使用了2个for循环,分别遍历arr1和arr2,当在遍历arr2时遇到了break语句,就跳出了当前的for循环,所以arr2中第二个(b)、第三个(c)、第四个(d)这几个元素都不会被打印出来,但是arr1中的元素都打印了出来,说明break语句只是跳出了for j in arr2这个循环。

3.7.4 continue 语句

continue语句有点像break语句,和break语句不同的是,它不是终止整个循环,而是跳过本次循环,并强制执行下一次循环。例如:

python
def main(): arr = ["a", "b", "c", "d"] for i in arr: if i == "c": continue Log(i)

输出结果为:

a b d

上面的输出结果中,"c"这个字符串没有打印。因为在循环体内的if语句判断i == "c"时,执行了continue语句,直接跳过了后面的Log(i)代码,执行了下一次循环。continue语句和break语句类似,都是只能作用于当前循环,不影响外层的循环(如果有的话)。

3.8 Python 日期和时间

量化交易经常需要和时间打交道,特别是对于一些日内策略或者交易频率比较高的策略来说,日期和时间的处理至关重要。Python提供了time、calendar、datetime等模块用于处理日期和时间,其中较为常用的是time、datetime模块。

3.8.1 time 模块

Python语言中处理时间需要使用time模块,导入time模块非常简单,使用import关键字即可。引入time模块以后,就可以调用该模块中的一些函数,做时间数据的处理,例如例子中的time.time()函数,读取当前时间的秒级时间戳。例如:

python
import time # 引入 time 模块 def main(): Log(time.time())

输出结果为:1595984400.0

3.8.2 什么是时间戳

时间戳是指自1970年1月1日(00:00:00 GMT)至当前时间的总秒数,常用的有秒级时间戳和毫秒级时间戳。时间戳具有唯一性,用于验证某个时间点存在的数据。严格来说不管在地球哪个地方哪个时区,任意时间点的时间戳都是相同的。例如:

python
import time # 引入 time 模块 def main(): now = time.time() Log(now) # 打印当前时间戳 Log(type(now))

输出结果为:

python
1598931147.2031229 <class 'float'>

3.8.3 时间戳转换时间

从上面的例子中可以看到,时间戳是一个数字,在商品期货中,所有的数据都是基于时间戳。但如果数据以时间戳形式显示出来,看起来不直观,这不利于观察和分析数据,所以就需要把时间戳转换为传统的时间格式。把时间戳转换为时间,可以使用Python语言time库中的函数转换,也可以使用优宽量化平台的D()函数转换。例如:

python
import time def main(): ts = time.time() # 使用 time.time()获取当前的秒级别时间戳 strTs = _D(ts) # 将时间戳转换为可读的时间字符串 Log("当前时间:", strTs) # 打印当前的可读的时间字符串

输出结果为:

当前时间:2020-07-29 09:00:00

注意:时间戳是不分时区全球统一的,在量化交易中一般不需要考虑时区的问题。

3.9 Python 常用内置函数

3.9.1 len() 函数

Python 的 len() 函数返回对象的元素数量或长度,可以适应于字符串、列表、字典等数据。字符串返回字符数量,列表返回元素数量,字典返回键值对数量。例如:

python
def main(): a = "hello FMZ!" b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] c = {"name": "FMZ", "age": 10} Log(len(a)) Log(len(b)) Log(len(c))

输出结果为:

python
10 10 2

3.9.2 range() 函数

range() 函数返回一个可以迭代的对象,这个对象是给定范围所生成一系列数字,常常与 for 循环语句搭配使用。该函数至少需要一个参数,例如:

python
def main(): for i in range(5): Log(i)

输出结果为:

python
0 1 2 3 4

上面的例子中 range(5) 产生了一个从 0 开始到 4 的数列,遵循左开右闭的原则,从 0 开始(包含 0)到 5 结束(不包含 5)。也可以给 range 传2个参数,第一个参数确定起始数字,第二个参数确定结束数字,同样遵循左开右闭原则。例如:

python
def main(): for i in range(2, 5): Log(i)

输出结果为:

python
2 3 4

range() 函数还可以和 len() 函数搭配使用,通过使用列表索引,遍历一个列表。例如:

python
def main(): arr = ["a", "b", "c", "d"] Log("第一个循环:") for i in arr: Log(i) Log("第二个循环:") for i in range(len(arr)): Log(i, "使用 i 访问列表中元素:", arr[i])

输出结果为:

python
第一个循环: a b c d 第二个循环: 0 使用 i 访问列表中元素:a 1 使用 i 访问列表中元素:b 2 使用 i 访问列表中元素:c 3 使用 i 访问列表中元素:d

在第一个循环中,每次从 arr 列表中取出元素,赋值给 i,然后打印 i,显示的就是列表中的元素。在第二个循环中,每次循环时 i 是列表中元素的索引。

3.9.3 split()函数

split()函数对字符串进行切片分割,返回分割后的字符串列表。下面的例子展示了split()函数的使用方法:

python
def main(): a = "hello FMZ!" b = a.split(" ") Log(b)

输出结果为:

python
['hello', 'FMZ!']

3.9.4 type()函数

type()函数在Python语言中是既简单又实用的对象数据类型查询方法。它是一个内部函数,调用它传入要查询的对象,就能够得到一个返回值,从而得知该对象类型信息。例如:

python
def main(): a = "hello FMZ!" b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] c = {"name": "FMZ", "age": 10} Log(type(a)) Log(type(b)) Log(type(c))

输出结果为:

python
<class 'str'> <class 'list'> <class 'dict'>

3.9.5 isinstance()函数

isinstance()函数常用于判断一个对象是否是一个已知的类型,类似type()。区别在于:type()函数不会认为子类是一种父类类型,不考虑继承关系。isinstance()函数会认为子类是一种父类类型,考虑继承关系。例如:

python
def main(): a = 3.14 b = 4 Log(isinstance(a, float)) Log(isinstance(b, float))

输出结果为:

python
True False

上面的例子中,第一个参数是要判断的对象,第二个参数为要对比的类型。可以看到,变量 afloat 浮点类型对比是相同的,所以 isinstance(a, float) 返回了 True。但变量 bfloat 是不同的,所以返回 False

3.9.6 取整函数

在量化交易中,对于数据取整处理是不可避免的,取整方式包括:向下取整、四舍五入取整、向上取整等等。Python提供了很多浮点数取整的相关函数。

  • int()函数向下取整
  • round()函数四舍五入取整
  • math模块中的ceil()方法向上取整
python
import math def main(): a = 3.14156 Log(int(a)) Log(round(a)) Log(round(a, 3)) Log(math.ceil(a))

输出结果为:

python
3 3 3.142 4

3.10 Python 异常处理

在编写 Python 策略时,避免不了出现错误,理想的情况是在策略启动时,通过 Python自检发现错误。但实际上 Python 并不能主动找出所有的错误,有一些错误只有在运行过程中才能被发现,所有就需要用一种恰当的方式将错误源及信息呈现出来,并对错误进行修正以提高策略的健壮性。

3.10.1 语法错误

语法错误通常是初学者经常遇到的情况,例如少写了括号、布尔值 True 字符 T 需要大写等等。不过这种错误在 Python 启动时,通过对代码的解析会自动终止程序,并报出错误位置和信息。例如:

python
def main() ##应该是`main():` Log(1)

输出结果为:

python
Traceback (most recent call last): File "<string>", line 1481, in Run File "<string>", line 1 def main() ^ SyntaxError: invalid syntax

3.10.2 异常错误

异常错误比较隐蔽,通常在策略运行中才能被发现。例如:除法运算时,除数为 0。把一个值为空值的变量(None),当做字典使用。整型变量和字符串相加使用未定义的变量参与运算。例如:

python
def main(): Log(10 / 0)

输出结果为:

python
Traceback (most recent call last): File "<string>", line 1481, in Run File "<string>", line 7, in <module> File "<string>", line 2, in main ZeroDivisionError: division by zero

3.10.3 异常捕获

为了检索隐藏的异常错误,或者为了避免异常错误的发生,导致正在运行的策略异常停止。可以使用 try…except 捕获异常。当执行 try 后的代码块时,如果发生异常错误,会被异常检测捕获,asException 这个异常类型的错误信息附加到 as 后的 e 这个变量中。例如:

python
def main(): try: Log(10 / 0) except Exception as e: Log('错误', e) Log('hello FMZ')

运行结果为:

python
错误 division by zero hello FMZ

上面的例子,在程序运算 10/0 时,并没有引发程序停止,而是打印了一条日志,并且最后一条日志 Log("hello FMZ") 也执行了。异常捕获不仅可以提示代码错误的原因,还可以防止程序因为异常导致终止运行。

第 4 章 优宽量化语法手册

本章节将详细介绍优宽量化平台提供的内置变量、结构体和内置函数。这些内容是实际编写量化交易程序所必需的基础。通过深入了解这些内容,能够更加灵活地运用优宽量化平台的功能,编写出高效、可靠的量化交易策略。本节内容将为大家提供具体的使用方法、示例代码以及常见问题的解答。

4.1 全局常量和数据结构

在优宽量化交易平台中,有很多API函数,每个函数都有各自的功能,它们返回的数据也不尽相同。通过本节对全局常量和数据结构的学习,可以知道这些函数返回的结果都有哪些意义。

4.1.1 exchange 交易所对象

exchange交易所对象是在编写策略时最常用的,因为绝大多数API函数都是该对象的方法。exchange交易所对象在策略代码中就代指了在创建实盘时或者回测时,添加的交易所。这些已经添加到平台的交易所,在添加时都绑定了交易所的API KEY(访问密钥)或者资金账号、资金密码(对于商品期货)。所以在使用例如:exchange.GetAccount()函数获取账户信息时,可以访问到对应期货账户的信息数据。exchange即添加的第一个交易所对象。

在添加了一个交易所对象之后,可以在策略代码中写入代码,打印该交易所对象的名字、标签信息,回测实盘均可:

python
def main(): Log("实盘机器人页面或者回测页面上,添加的第一个交易所对象名称:", exchange.GetName(), ",标签:", exchange.GetLabel())

image

4.1.2 exchanges 交易所对象列表

学习完了exchange的概念,exchanges的概念就更加容易理解了。exchanges就是一系列的交易所对象放在一个列表中(数组),因为在优宽量化交易平台上一个策略可以设计成多交易所、多账户的架构,所以可以添加多个交易所对象。exchange的数组,包含多个交易所对象,exchanges[0]即是exchange。添加的交易所对象对应策略代码中的exchanges[0]、exchanges[1]、exchanges[2]...,以此类推。同样,在实盘或者回测可以使用以下代码测试,遍历exchanges交易所对象数组,逐个打印交易所对象的名称、标签信息。

python
def main(): for i in range(len(exchanges)): Log("添加的交易所对象索引(第一个为0以此类推):", i, "名称:", exchanges[i].GetName(), "标签:", exchanges[i].GetLabel())

在实盘中,添加两个交易所"国泰君安"、"华安期货",可以看到实盘策略依次输入两个交易所的索引和标签。

image

4.1.3 Order 结构

订单结构由exchange.GetOrder()exchange.GetOrders()函数返回。优宽量化平台定义、封装的订单数据结构,其中属性StatusTypeOffset的值为固定的几种取值。

python
{ Info: {...}, # 请求交易所接口返回的原始数据,回测时无此属性 Id: 123456, # 交易单唯一标识 Symbol: rb2410, # 合约名称 Price: 1000, # 下单价格 Amount: 10, # 下单数量 DealAmount: 10, # 成交数量 AvgPrice: 1000, # 成交均价 Status: 1, # 订单状态 Type: 0, # 订单类型 Offset: 0, # 订单的开平仓方向 ContractType: "" # 订单的合约代码 }
  • Info 属性:接口返回的原始数据即封装之前的数据内容。
  • Symbol属性: 合约名称
  • Id 属性:订单的 ID,用于取消某个订单,查询某个订单时用作参数。
  • Price 属性:订单的委托价格。
  • Amount 属性:订单的委托数量。
  • DealAmount 属性:订单成交部分的数量。
  • AvgPrice 属性:订单的成交均价。
  • Status 属性:订单状态属性,例如挂单状态、完全成交状态、撤销状态。
  • Type 属性:标记订单是买单还是卖单。
  • Offset 属性:在期货交易时,标记订单是开仓单还是平仓单。
  • ContractType 属性:订单的合约代码。

以下表格定义了 Order 结构中的Status属性,分别表示了订单未完成、已完成、已取消、未知状态等 4 个值。

常量名定义
ORDER_STATE_PENDING未完成0
ORDER_STATE_CLOSED已完成1
ORDER_STATE_CANCELED已取消2
ORDER_STATE_UNKNOWN未知状态3

在编写代码时,可以直接使用ORDER_STATE_PENDING判断订单状态,因为ORDER_STATE_PENDING非常容易看明白,判断的状态是挂单状态,而用 0 则非常不直观。

python
# 第一种写法 if order["Status"] == ORDER_STATE_PENDING: Log("订单状态值:", order["Status"]) # 第二种写法 if order["Status"] == 0: Log("订单状态值:", order["Status"])

以下表格定义了 Order 结构中的Type属性,分别表示订单为买单、订单为卖单,当订单为买单时值为 0,当订单为卖单时值为 1。

常量名定义
ORDER_TYPE_BUY订单为买单0
ORDER_TYPE_SELL订单为卖单1

下面的表格定义了 Order 结构中的Offset属性,分别表示订单开仓方向、订单平仓方向,当订单为开仓方向时值为 0,当订单为平仓方向值为 1。

常量名定义
ORDER_OFFSET_OPEN订单为开仓方向0
ORDER_OFFSET_CLOSE订单为平仓方向1

注意GetOrder需要传入订单号参数,获取的是某一个订单的 Order 结构体。而GetOrders不需要传入参数,获取所有未完成的订单。返回值的是 Order 结构体数组。如果当前交易对没有挂单时,调用exchange.GetOrders()返回空数组,即:[]

4.1.4 Position 结构

Position 结构是期货交易中的持有仓位信息,由exchange.GetPositions()函数返回此结构数组。

python
{ Info: {...}, # 请求交易所接口返回的原始数据,回测时无此属性 Symbol: rb2410, # 合约名称 MarginLevel: 10, # 杆杠大小 Amount: 100, # 持仓量 FrozenAmount: 0, # 仓位冻结数量 Price: 10000, # 持仓均价 Profit: 0, # 持仓浮动盈亏 Type: 0, # 持仓方向 ContractType: "quarter", # 持仓的合约代码 Margin: 1 # 仓位占用的保证金 }
  • Info 属性:同上。
  • Symbol: 合约名称
  • MarginLevel 属性:持仓的杠杆数值。
  • Amount 属性:该仓位持仓数量。
  • FrozenAmount 属性:仓位冻结数量。
  • Price 属性:持仓均价,持仓后,加仓会影响该值。
  • Profit 属性:持仓盈亏。
  • Type 属性:仓位类型,多头仓位、空头仓位。
  • ContractType 属性:合约代码。
  • Margin 属性:保证金。

GetPosition()函数返回的是 Position 结构数组,其中的Type属性代表持仓方向,PD_LONG代表多头仓位,PD_SHORT代表空头仓位,PD_LONG_YD代表昨日多头仓位,PD_SHORT_YD代表昨日空头仓位。

常量名定义
PD_LONG多头仓位0
PD_SHORT空头仓位1
PD_LONG_YD昨日多头仓位2
PD_SHORT_YD昨日空头仓位3

注意GetPosition函数获取的是所有持仓品种的持仓信息,如果没有持仓返回空数组,所以引用前要先判断。

4.1.5 Trade 结构

获取所有交易历史(非自己),由exchange.GetTrades()函数返回。这个是整个市场最近时间的成交记录。

python
{ Time: 1567736576000, # 时间(Unix timestamp 毫秒) Price: 1000, # 价格 Amount: 1, # 数量 Type: 0 # 订单类型 }
  • Time 属性:毫秒时间戳,记录市场上这笔成交的时间。
  • Price 属性:市场上这笔成交记录的成交价格。
  • Amount 属性:市场上这笔成交记录的数量。
  • Type 属性:标记这笔成交是买单主动成交,还是卖单主动成交。

注意:商品期货市场不返回 Trade 结构数据。

4.1.6 Ticker 结构

市场行情由exchange.GetTicker()函数返回。在商品期货中,通常情况下每秒返回2个 Tick 数据,通常指的是盘口数据。

python
{ Info : {...}, # 请求交易所接口返回的原始数据,回测时无此属性 Symbol: rb2410, # 合约名称 High : 1000, # 最高价 Low : 500, # 最低价 Sell : 900, # 卖一价 Buy : 899, # 买一价 Last : 900, # 最后成交价 Open : 550, # 开盘价 Volume : 10000000, # 最近成交量 OpenInterest: 500000, # 持仓量 Time : 1567736576000 # 毫秒级别时间戳 }
  • Info 属性:同上。
  • High 属性:一般为24小时内的最高价。
  • Low 属性:一般为24小时内的最低价。
  • Sell 属性:当前的卖一价格。
  • Buy 属性:当前的买一价格。
  • Last 属性:当前的最新成交价。
  • Volume 属性:最新成交量。
  • OpenInterest 属性:持仓量。
  • Time 属性:毫秒级别时间戳,用于标记时间。

4.1.7 Record 结构

标准 OHLC 结构数据包含了开盘价、最高价、最低价、收盘价、成交量、时间等数据,它是组成 K 线的最基本数据,由exchange.GetRecords()函数返回此结构数组。其中每个 Record 结构数据都代表一个 K 线。

python
{ Time: 1567736576000, # K 线时间戳 Open: 1000, # 开盘价 High: 1500, # 最高价 Low: 900, # 最低价 Close: 1200, # 收盘价 OpenInterest: 500000, # 持仓量 Volume: 1000000 # 交易量 }
  • Time 属性:毫秒级别时间戳,对于一个Record结构,其Time属性值为这根K线Bar的周期的起始时间戳。
  • Open 属性:开盘价。
  • High 属性:最高价。
  • Low 属性:最低价。
  • Close 属性:收盘价。
  • OpenInterest 属性:持仓量。
  • Volume 属性:成交量。

4.1.8 Depth 结构

市场深度,由exchange.GetDepth()函数返回。返回值是 Depth 结构体,结构体包含两个结构体数组,分别是 Asks[]和 Bids[],其中每个数组中包含价格 Price、数量 Amount 以及时间戳 Time。

python
{ Asks : [...], # 卖单数组,MarketOrder 数组,按价格从低向高排序 Bids : [...], # 买单数组,MarketOrder 数组,按价格从高向低排序 Time : 1567736576000 # 毫秒级别时间戳 }

Depth 数据结构的 Asks 键,为卖单列表,列表中每个数据均为 MarketOrder 数据。Depth 数据结构的 Bids 键,为买单列表,列表中每个数据均为 MarketOrder 数据。

注意:不同交易所返回的深度数据是不同的,对于上期所和上能源,可以返回5档数据;对于其他交易所,仅支持返回1档数据。

4.1.9 Account 结构

Account 结构是由exchange.GetAccount()函数返回的账户信息,主要包含3个数据:账户余额、账户冻结余额,以及请求交易所接口返回的原始数据。

python
{ Info: {...}, # 请求交易所接口返回的原始数据,回测时无此属性 Balance: 1000, # 账户余额 FrozenBalance: 0, # 账户冻结的余额 Stocks: 0, FrozenStocks: 0, Equity: 1000, #账户权益 UPnL: 0 #持仓盈亏 }
  • Info 属性:交易所接口返回的原始数据,回测时无此属性。。
  • Balance 属性:可用资金数量,CTP商品期货中,该属性为可用钱数。
  • FrozenBalance 属性:冻结资金数量,如果下单后,订单未成交,则冻结该订单用于交易的资金,FrozenBalance 即为冻结的资金数量。
  • Stocks 传统期货、股票证券此属性固定为0。
  • FrozenStocks 传统期货、股票证券此属性固定为0。
  • Equity属性:账户权益,包含:可用资产余额、持仓保证金、持仓盈亏等。
  • UPnL属性:所有持有仓位的持仓盈亏。

4.2 获取 Tick、深度、历史 K 线数据

在商品期货量化交易中,需要使用各种不同类型的数据,如交易所原始 Tick 数据、订单薄深度数据,以及常用的 K 线数据。让我们看看如何使用 API 函数获取这些数据,以及常用的商品期货策略框架。

4.2.1 exchange.GetTicker()

Tick 数据俗称交易快照,是交易所内的数据截面。国内商品期货每秒有 2 个 Tick 数据。exchange.GetTicker()函数用于获取实时 tick 数据,返回 Ticker 结构。在回测系统中,该函数返回的 Ticker 数据中 High、Low 是模拟值,取自当时盘口的卖一价和买一价。在实盘中,则是交易所 Tick 接口定义的一定周期内的最高价和最低价。

python
def main(): exchange.SetContractType("MA888") Log(exchange.GetTicker())

注意:在调用任何访问交易所接口的 API 函数时(如exchange.GetTicker()exchange.Buy(Price, Amount)exchange.CancelOrder(Id)等),都有可能由于各种原因导致访问失败。因此,需要对这些函数的调用做容错处理。例如:

python
def main(): exchange.SetContractType("MA888") ticker = exchange.GetTicker() if not ticker: ticker = exchange.GetTicker()

升级API改动中,exchange.GetTicker()函数增加了symbol参数,可以直接在请求行情数据时指定品种。

  • exchange.GetTicker("rb2410")
    直接查询rb2410合约的行情数据。

4.2.2 exchange.GetDepth()

exchange.GetDepth()函数用于获取交易所订单薄(深度数据),返回值是 Depth 结构体。Depth 结构体包含两个结构体数组,分别是 Asks[] 和 Bids[],其中 Asks 和 Bids 包含以下结构体变量:

  • Price:价格
  • Amount:数量

例如,要获取当前卖二价,可以这样写代码:

python
def main(): exchange.SetContractType("MA888") depth = exchange.GetDepth() price = depth["Asks"][1]["Price"] Log("卖二价为:", price)

注意:商品期货涨停时,卖单卖一的价格是涨停价格,订单量是 0。跌停时,买单买一的价格是跌停价格,订单量是 0。通过判断买一、卖一的订单量数量,可以判断是否涨跌停。

升级API改动中,exchange.GetDepth()函数也增加了symbol参数,可以直接在请求深度数据时指定品种。

  • exchange.GetDepth("rb2410")
    直接查询rb2410合约的深度数据。

4.2.3 exchange.GetRecords()

exchange.GetRecords()除了支持symbol参数直接指定请求的K线数据的品种信息。保留了原有的period参数用来指定K线周期,还增加了一个limit参数用来指定请求时期望的K线长度。同时也兼容旧版本的GetRecords函数只传入period周期参数的调用方式。

exchange.GetRecords()函数的调用方式:

  • exchange.GetRecords()
    不指定任何参数时请求当前合约代码对应的品种的K线数据,K线周期是策略回测界面或者实盘时设置的默认K线周期。
  • exchange.GetRecords(60 * 15)
    仅指定K线周期参数时,请求当前合约代码对应的品种的K线数据。
  • exchange.GetRecords("rb2410")
    仅指定品种信息时,请求指定品种的K线数据,K线周期是策略回测界面或者实盘时设置的默认K线周期。
  • exchange.GetRecords("MA888", 60 * 60)
    指定品种信息,指定具体K线周期请求K线数据。
  • exchange.GetRecords("i888", 60, 1000)
    指定品种信息,指定具体K线周期,指定期望获取的K线长度请求K线数据。
python
def main(): while not exchange.IO("status"): Sleep(1000) Log(exchange.SetContractType("rb888")) records = exchange.GetRecords(PERIOD_H1) Log("第一根k线数据为,Time:", records[0]["Time"], "Open:", records[0]["Open"], "High:", records[0]["High"]) Log("第二根k线数据为,Time:", records[1]["Time"], "Close:", records[1]["Close"]) Log("当前K线(最新)", records[-1], "上一根K线", records[-2])

4.2.4 商品期货策略框架

商品期货策略需要检测与期货公司前置机连接状态,在获取行情之前需要订阅合约,这样才能获取订阅的行情。在优宽量化交易平台上,回测时模拟如同实盘一样的连接机制。

以上我们了解、学习的行情接口因为篇幅有限,为了容易理解,并没有写设置合约、检测与前置机连接状态等代码,那么一个完整的商品期货策略框架是什么样的呢?完整的框架:

python
def main(): while True: if exchange.IO("status"): exchange.SetContractType("rb888") ticker = exchange.GetTicker() depth = exchange.GetDepth() trades = exchange.GetTrades() records = exchange.GetRecords() Log("rb888 ticker Last:", ticker["Last"]) Log("rb888 depth:", depth) Log("rb888 trades:", trades) Log("rb888 records:", records) LogStatus(_D(), "已经连接 CTP ! ") else: LogStatus(_D(), "未连接 CTP ! ")

其中我们陌生的代码也只有:

  • exchange.IO("status")
  • exchange.SetContractType("rb888")
  • LogStatus(_D(), "已经连接 CTP ! ")

exchange.IO("status")函数可以判断当前是否和期货公司前置机连接,如果连接返回 1,如果非连接状态返回 0。

exchange.SetContractType("rb888")函数调用是把当前合约设置为 rb888 并订阅该合约,rb888 是螺纹钢主力合约。如果想使用指数合约,可以用 rb000。

LogStatus(_D(), "已经连接 CTP ! ")函数的作用是在机器人状态栏上显示时间信息和文字,_D()函数返回当前时间的字符串。该策略代码会不停循环执行打印行情数据。

4.3 获取和取消订单、获取当前挂单

有时下单之后,可能会因为行情或者价格的原因,导致订单不能完全成交或者订单只成交了一部分。所以就需要下单之后了解订单状态,以及对未成交的订单撤单处理。

4.3.1 exchange.SetContractType(ContractType)

在商品期货中要想获取行情和下单交易,首先要先订阅合约代码,才能进行下一步操作。exchange.SetContractType(ContractType)函数用于设置合约类型,参数值:string类型。如同我们上节课学习的,商品期货策略框架中设置合约,并且订阅该合约。我们一起来看一个新例子:

python
def main(): while True: if exchange.IO("status"): ret = exchange.SetContractType("MA888") Log("订阅的合约的详细信息: ", ret) break else: LogStatus(_D(), "未连接")

exchange.SetContractType("MA888")函数返回合约的详细信息,赋值给ret变量。输出ret变量结果:

python
{ 'CombinationType': 48, 'CreateDate': 0, 'DeliveryMonth': 4, 'DeliveryYear': 0, 'EndDelivDate': 0, 'ExchangeID': 'CZCE', 'ExchangeInstID': 'MA005', 'ExpireDate': 0, 'InstLifePhase': 49, 'InstrumentID': 'MA005', 'InstrumentName': 'MA 连续', 'IsTrading': 1, 'LongMarginRatio': 0.07, 'MaxLimitOrderVolume': 1000, 'MaxMarginSideAlgorithm': 48, 'MaxMarketOrderVolume': 1000, 'MinLimitOrderVolume': 1, 'MinMarketOrderVolume': 1, 'OpenDate': 0, 'OptionsType': 0, 'PositionDateType': 50, 'PositionType': 50, 'PriceTick': 1, 'ProductClass': 49, 'ProductID': 'MA', 'ShortMarginRatio': 0.07, 'StartDelivDate': 0, 'StrikePrice': 0, 'UnderlyingInstrID': '', 'UnderlyingMultiple': 1, 'VolumeMultiple': 10 }

可以看到有不少信息在我们写策略时是可以用的,比如合约乘数即一手合约是多少商品,例如MA甲醇,一手是10吨。

注意:当前合约设置为MA888之后,就可以获取当前MA主力合约的行情,对当前主力合约下单等操作。我们在所有操作前首先要确保和期货公司前置机(服务器)连接,再者要明确当前操作哪个合约。

4.3.2 exchange.SetDirection(Direction)

SetDirection函数可以设置期货下单方向,参数常用的有四种:buyclosebuysellclosesell。商品期货多出closebuy_today,与closesell_today,指平今仓,默认为closebuy/closesell为平昨仓。

  • exchange.SetDirection("buy") 买入开多仓
  • exchange.SetDirection("sell") 卖出开空仓
  • exchange.SetDirection("closebuy") 卖出平多仓
  • exchange.SetDirection("closesell") 买入平空仓
  • exchange.SetDirection("closebuy_today") 卖出平今日多仓
  • exchange.SetDirection("closesell_today") 买入平今日空仓

4.3.3 exchange.Buy(Price, Amount)

下买单函数,第一个参数为下单价格,第二个参数为下单量。下单成功后返回一个订单ID。测试例子:

python
def main(): while True: if exchange.IO("status"): # 如果交易所连接正常,则执行交易操作 ret = exchange.SetContractType("MA888") ticker = exchange.GetTicker() exchange.SetDirection("buy") id = exchange.Buy(ticker.Buy, 1) Log(id) break # 执行完交易操作后退出循环 else: # 如果交易所未连接,则记录当前时间并打印状态消息 LogStatus(_D(), "未连接")

输出结果为:

image

以上例子用当时的行情中的买一价作为下单价格,下单量1手,下了一个开多仓订单。我们可以看到如果开多仓,exchange.Buy(ticker.Buy, 1)是和exchange.SetDirection("buy")配合使用的,那么下单函数和exchange.SetDirection()函数都有哪些组合呢?

下单函数SetDirection参数设置方向备注
exchange.Buy"buy"买入开多仓
"closesell"买入平空仓
"closesell_today"买入平今日空仓
exchange.Sell"sell"卖出开空仓
"closebuy"卖出平多仓
"closebuy_today"卖出平今日多仓

4.3.4 exchange.Sell(Price, Amount)

下卖单,返回订单编号,可用于查询订单信息和取消订单。期货下单时必须注意交易方向是否设置正确。

python
def main(): while True: if exchange.IO("status"): ret = exchange.SetContractType("MA888") ticker = exchange.GetTicker() exchange.SetDirection("sell") id = exchange.Sell(ticker.Sell, 1) Log("开空仓订单 ID: ", id) break else: LogStatus(_D(), "未连接")

4.3.5 exchange.CreateOrder()

exchange.CreateOrder()函数用于下单。该函数最大的功能是直接在该函数的参数中指定下单的品种、方向。这样就不再依赖系统当前设置的交易对、合约代码、交易方向等设置了。

在多品种交易下单场景中、并发场景中极大程度的降低了设计复杂度。exchange.CreateOrder()函数的四个参数分别是symbol、side、price、amount。用来指定订单的合约代码、方向、价格、数量。

python
def main(): while not exchange.IO("status"): Sleep(1000) id = exchange.CreateOrder("rb2410", "buy", 3500, 1) Log(id)

4.3.6 exchange.CancelOrder(orderId)

exchange.CancelOrder(orderId)函数可以根据订单ID取消订单。如下面的代码:首先订阅了MA888合约,然后获取Tick行情,接着设置下单方向,并使用Sell函数下单。变量id接收了下单后返回的订单编号,最后使用CancelOrder函数传入id参数来取消这个订单。在撤单后加break是为了让撤单后就跳出循环,否则会不停下单撤单。

python
def main(): while True: if exchange.IO("status"): ret = exchange.SetContractType("MA888") ticker = exchange.GetTicker() exchange.SetDirection("sell") id = exchange.Sell(ticker.Sell, 1) Log("开空仓订单 ID: ", id) Sleep(5000) exchange.CancelOrder(id) break else: LogStatus(_D(), "未连接")

输出结果为:

image

4.3.7 exchange.GetOrders()

获取所有未完成的订单。返回值: Order结构体数组。当交易所对象exchange代表的账户当前没有挂单时,调用exchange.GetOrders()返回空数组,即:[]。

python
def main(): contractTypeList = ["MA888", "rb888", "i888"] while True: if exchange.IO("status"): for i in range(len(contractTypeList)): ret = exchange.SetContractType(contractTypeList[i]) ticker = exchange.GetTicker() exchange.SetDirection("sell") id = exchange.Sell(ticker.Sell, 1) Log(contractTypeList[i], "开空仓订单 ID: ", id) orders = exchange.GetOrders() for i in range(len(orders)): Log(orders[i]) break else: LogStatus(_D(), "未连接")

三种合约逐个切换,下单。然后调用exchange.GetOrders()函数获取当前挂单。然后逐个打印,回测运行,输出结果为:

image

三个订单的Status返回的值都是0,代表“未完成”,从上面的例子中我们可以看出,GetOrders函数的返回值是不区分当前设置的合约的。它返回的是所有未完成的订单。

升级API改动中,exchange.GetOrders()函数也增加了symbol参数,可以直接指定查询当前未完成订单(挂单)的合约代码。

  • exchange.GetOrders("rb888")
    查询rb888合约的所有未完成订单。

4.3.8 exchange.GetOrder(orderId)

根据订单号获取订单详情,参数值:orderid为要获取的订单号,string类型或数值类型。返回值:Order结构体。

python
def main(): while True: if exchange.IO("status"): ret = exchange.SetContractType("MA888") ticker = exchange.GetTicker() exchange.SetDirection("sell") id = exchange.Sell(ticker.Sell, 1) Log("开空仓订单 ID: ", id) Sleep(5000) exchange.CancelOrder(id) Sleep(5000) Log(exchange.GetOrder(id)) break else: LogStatus(_D(), "未连接")

上面的例子是下单之后获取订单的id,然后使用CancelOrder取消这个id的订单,最后使用GetOrder函数获取这个id的当前订单状态,可以看到打印出的订单信息,其中Status属性为2(“已撤销”)。输出结果为:

image

4.3.9 exchange.GetHistoryOrders

exchange.GetHistoryOrders()函数用于获取当前交易日内的所有合约的历史订单,支持查询指定合约的历史订单。

exchange.GetHistoryOrders()函数有两种调用形式:

  • exchange.GetHistoryOrders()
    当不传任何参数时,获取所有合约的历史订单。
  • exchange.GetHistoryOrders("rb2410")
    当指定具体合约代码时,获取具体合约的历史订单。
python
def main(): while not exchange.IO("status"): Sleep(1000) exchange.SetContractType("rb888") exchange.SetDirection("sell") order1 = exchange.Sell(99999, 1) # 准备撤销订单 order2 = exchange.Sell(exchange.GetTicker().Buy - 5, 1) # 成交订单 exchange.CancelOrder(order1) #不设置合约参数 orders = exchange.GetHistoryOrders() Log(orders) #设置合约参数 rborder = exchange.GetHistoryOrders('rb888') Log(rborder)

注:

回测系统和实盘系统该函数返回结果有所区别:

  • 回测系统:返回策略运行期间所有完成订单,可使用since参数模拟本交易日生成订单;
  • 实盘系统:仅返回本交易日完成订单。

4.4 IO 扩展函数

4.4.1 IO 函数切换行情模式

商品期货行情是推送机制,在优宽量化交易平台,我们可以使用exchange.IO()函数切换行情模式。一共有三种模式可以切换。

  • exchange.IO("mode", 0): 立即返回模式,如果当前还没有接收到交易所最新的行情数据推送,就立即返回旧的行情数据,如果有新的数据就返回新的数据,设置为该模式后,行情接口调用时会立即返回,用于非阻塞的策略架构设计,例如多品种策略。
  • exchange.IO("mode", 1): 缓存模式(默认模式),如果当前还没有收到交易所最新的行情数据(同上一次接口获取的数据比较),就等待接收然后再返回,如果调用该函数之前收到了最新的行情数据,就立即返回最新的数据。设置该模式后,在没有收到最新行情时,会阻塞在该函数。通常用于单品种的交易策略,因为只用处理一个合约的行情,处理最新行情即可,其他时间可以阻塞等待。
  • exchange.IO("mode", 2): 强制更新模式,进入等待一直到接收到交易所下一次的最新推送数据后返回。这种模式,只使用最新获取的行情数据,即强制等待下一次数据推送过来。

注意:一般情况下使用默认的缓存模式。

4.4.2 IO 函数判断与期货公司前置机连接状态

exchange.IO("status")

这个函数调用我们应该并不陌生了,在之前的课程中,我们已经使用过了,该函数非常简单,使用时判断其返回值即可,返回值为真,代表和期货公司前置机服务器连接成功,返回值为假,代表和期货公司前置机服务器未连接。

4.4.3 IO 函数获取交易所所有合约

IO函数的instruments参数可以获取交易所所有合约,返回交易所所有合约的列表,只支持实盘,完整的查询范例:

python
def main(): while not exchange.IO("status"): LogStatus("正在等待与交易服务器连接, " + _D()) Log("开始获取所有合约") instruments = _C(exchange.IO, "instruments") Log("合约列表获取成功") length = 0 for i in range(len(instruments)): length += 1 Log("合约列表长度为:", length)

类似的调用还有:

  • exchange.IO("products")返回交易所所有产品的列表,只支持实盘。
  • exchange.IO("subscribed")返回已订阅行情的合约,格式同上,只支持实盘。
  • exchange.IO("settlement")结算单查询,只支持实盘。

4.4.4 exchange.IO("api", …)

优宽量化的CTP(商品期货)终端提供了完整的全API实现,当优宽量化平台的API满足不了你需要的功能时。可以用exchange.IO函数进行更深层的系统调用,完全兼容官方的Api名称。CTP的IO直接扩展函数调用请求,将会在收到第一个isLast标记为true的响应包后返回。

注意:该方法不支持回测和模拟交易,只支持实盘交易。

  • 查询投资者信息:
python
def main(): while not exchange.IO("status"): LogStatus("正在等待与交易服务器连接, " + _D()) Log(exchange.IO("api", "ReqQryInvestor"))
  • 修改密码:
python
def main(): Sleep(6000) exchange.IO("api", "ReqUserPasswordUpdate", {"BrokerID": "9999", "UserID": "11111", "OldPassword": "oldpass", "NewPassword": "newpass"})
  • 查询结算单:
python
def main(): while not exchange.IO("status"): LogStatus("正在等待与交易服务器连接, " + _D()) r = exchange.IO("api", "ReqQrySettlementInfo", {"TradingDay": "20190506"}) s = '' for i in range(len(r)): for ii in range(len(r[i])): if r[i][ii]["Name"] == "CThostFtdcSettlementInfoField": s += r[i][ii]["Value"]["Content"] Log(s)

4.4.5 exchange.IO("wait")

exchange.IO("wait")函数可以使程序在有最新事件时进行响应,执行程序逻辑,在没有新事件触发时,该函数会阻塞,可以实现回调机制的策略设计。当前交易所有任何品种更新行情信息或订单成交时才返回,返回EventTick/OrderEvent结构。

只支持商品期货实盘。在使用exchange.IO("wait")时,必须至少已经订阅了一个当前处于交易状态的合约(已经交割的过期合约,不会再有行情数据),否则会阻塞在该函数(由于没有任何行情、订单更新)。

一个简单实现回调机制的例子:

python
def on_tick(symbol, ticker): Log("symbol:", symbol, "update") # 数据结构: https://www.youquant.com/api#ticker Log("ticker:", ticker) def on_order(order): Log("order update", order) def main(): while not exchange.IO("status"): Sleep(10) exchange.IO("mode", 0) _C(exchange.SetContractType, "MA001") while True: e = exchange.IO("wait") if e: if e.Event == "tick": on_tick(e['Symbol'], e['Ticker']) elif e.Event == "order": on_order(e['Order'])

以上我们通过一个例子来说明exchange.IO("wait")的使用方法,该方法的特点是当前交易所有任何品种更新行情信息或订单成交时才返回,所以非常适合用于单品种、多品种的回调机制策略设计。

注意:该方法不支持回测和模拟交易,只支持实盘交易。

4.5 账户API获取账户和持仓信息

账户信息和持仓信息关乎着策略逻辑,是策略逻辑的必须条件,在优宽量化平台中,可以使用GetAccount函数获取账户信息,用GetPosition函数获取持仓信息。

4.5.1 exchange.GetAccount()

exchange.GetAccount()函数返回交易所账户信息。通常使用返回的数据中的Balance属性,即账户可用资金,以及FrozenBalance属性,即挂单冻结的资金。如果需要使用其他数据计算,例如当前总权益、保证金等,这些数据保存在Info属性中,Info属性内保存的数据为CTP接口返回的原始数据。Info属性仅实盘有效,回测时无此属性。

下面我们一起来看一个简单的例子:

python
def main(): while True: if exchange.IO("status"): exchange.SetContractType("rb888") account = exchange.GetAccount() Log("挂单前↑ ") Log("账户可用资金, Balance", account["Balance"]) Log("账户挂单冻结资金, FrozenBalance:", account["FrozenBalance"]) ticker = exchange.GetTicker() exchange.SetDirection("buy") exchange.Buy(ticker.Buy - 10, 1) account = exchange.GetAccount() Log("挂单后↑ ") Log("账户可用资金, Balance", account["Balance"]) Log("账户挂单冻结资金, FrozenBalance:", account["FrozenBalance"]) LogStatus(_D(), "已经连接 CTP ! ") break else: LogStatus(_D(), "未连接 CTP ! ")

例子中,设置当前操作的合约为rb888即螺纹钢主力合约,在下单前,获取一次账户资产信息,打印可用资金,打印挂单冻结资金。然后获取行情,设置交易方向为开多仓,根据行情当前的买一价格,下单一手多单螺纹钢合约。然后再次获取当前账户资产信息并打印。然后为了方便观察,使用break语句跳出循环,策略程序执行完毕。

image

4.5.2 exchange.GetPosition()

exchange.GetPosition()函数用于获取当前持仓信息,返回值为position结构体数组。position结构体数组包括:交易所接口应答的原始数据、杠杆大小、持仓量、仓位冻结、持仓均价、持仓浮动盈亏、持仓方向、合约代码、仓位占用保证金等等。
注意:返回的数组中包含当前交易所对象绑定的账户所有的持仓,并非当前设置的合约的持仓数据。

我们一起来看一个例子:

python
def main(): ctList = ["rb888", "i888", "MA888", "pp888",] while True: if exchange.IO("status"): for i in range(len(ctList)): ret = exchange.SetContractType(ctList[i]) t = exchange.GetTicker() exchange.SetDirection("sell") exchange.Sell(t.Buy - 10, 1, "合约:", ctList[i], "->", ret["InstrumentID"]) orders = exchange.GetOrders() Log("orders length:", len(orders), "orders:", orders) pos = exchange.GetPosition() for i in range(len(pos)): Log(pos[i]) break else: LogStatus(_D(), "未连接 CTP ! ")

我们把要操作的合约代码写在一个数组(列表)中,然后通过一个for循环去进行每一个合约的下单操作。下单价格为当前买一价格减去10元,下开空仓的订单,由于价格比当前买一还低10元,所以马上就成交了。然后我们使用exchange.GetOrders()函数,获取当前所有挂单,并且打印。用于观察订单是不是还处于未成交状态,如果exchange.GetOrders()函数返回的是一个空数组即:[],说明订单都已经成交了。然后调用exchange.GetPosition()函数,获取当前所有持仓,并且遍历持仓数据数组,逐个打印持仓信息。

image

细心的同学可能发现,为何此处exchange.Sell函数传入了6个参数。这里我们讲解一下优宽量化交易平台可以输出日志的函数的特性,下单函数就是一个可以输出日志的函数,可以看到当前例子的运行截图中,有下单日志打印,所有可以产生日志的函数,都是可以在必要参数后增加一些附带参数,用于打印一些附带说明信息。例如本例中,下单日志中附带了「合约:pp888 -> pp2305」这样的信息。就是要说明当前合约代码设置的为pp888,实际映射到具体交易的合约为pp2305,在回测时间2023-04-12时,pp2305为主力合约。从例子代码中可以看到这个映射合约可以从exchange.SetContractType("pp888")函数返回时,返回的数据中获取到。注意:映射合约支持商品指数(pp000)映射到pp2305,也支持商品主力连续(pp888)映射到pp2305。

4.5.3 exchange.GetPositions()

为了更加贴合函数命名语义,增加了新的获取持仓函数:exchange.GetPositions()

exchange.GetPositions()函数有两种调用形式:

  • exchange.GetPositions()
    当不传任何参数时,获取所有合约的持仓数据。
  • exchange.GetPositions("rb2410")
    当指定具体合约代码时,获取具体合约的持仓数据。
python
def main(): while not exchange.IO("status"): Sleep(1000) info = exchange.SetContractType("rb888") ticker = exchange.GetTicker() exchange.SetDirection("buy") exchange.Buy(ticker["Last"] + info["PriceTick"] * 20, 2) #不设置合约参数 position = exchange.GetPositions() if len(position) > 0: Log("Amount:", position[0]["Amount"], "FrozenAmount:", position[0]["FrozenAmount"], "Price:", position[0]["Price"], "Profit:", position[0]["Profit"], "Type:", position[0]["Type"], "ContractType:", position[0]["ContractType"]) #设置合约参数 rbposition = exchange.GetPositions('rb888') Log(rbposition)

4.6 常用日志信息函数

日志可以记录量化交易中策略运行状态信息,同时还可以监控策略中指定的事件,还可以通过日志检查错误发生的原因。

4.6.1 Log(…)

Log()函数用于打印日志信息,参数可以传入多个,参数可以传入任意类型。支持使用十六进制颜色代码着色。支持消息推送。

python
def main(): Log("优宽量化你好 !@")

Log()函数最后一个参数写"@"即可实现该条日志信息推送,可以推送到优宽量化APP、邮箱、监听WebHook的服务程序。

在编写策略时,Log()函数通常用于打印一些提示信息、输出数据。也可以用于策略程序调试、逻辑流程分析。Log支持打印base64编码后的图片。

python
def main(): Log("`data:image/png;base64,AAAA`")

Log支持直接打印Python的matplotlib.pyplot对象,只要对象包含savefig方法就可以打印。

python
import matplotlib.pyplot as plt def main(): plt.plot([3, 6, 2, 4, 7, 1]) Log(plt)

Log函数支持语言切换,Log函数输出文本,会根据平台页面上语言设置自动切换为对应的语言。

python
def main(): Log("中文")

在完整的商品期货框架中使用Log()函数:

python
def main(): ctList = ["rb888", "i888", "MA888", "pp888",] while True: if exchange.IO("status"): for i in range(len(ctList)): Log("遍历 ctList,当前合约为: ", ctList[i]) break else: LogStatus(_D(), "未连接 CTP ! ")

4.6.2 LogProfit(Profit)

LogProfit函数用来在系统日志打印一条收益信息,并且会自动在收益曲线图表上打印一个收益点。该函数同样为可以打印日志的函数,可以在必要参数后传附带参数,用于显示一些打印收益时需要同时记录的信息,例如在打印收益时,同时输出当前账户资产信息,可以用于核对记录。因为打印的收益数据是需要在策略里面主动计算的,并非系统自动计算的。所以需要注意的是如果你写的收益算法不对,打印的收益信息也就是没有意义的错误信息,此时附带一些当时的资产数据,方便核算。LogProfit函数如果以字符&结尾,只绘制收益图表,不打印收益日志。

4.6.3 LogStatus(Msg)

LogStatus(Msg)函数是在设计、编写策略时很重要的一个函数,用来控制策略机器人页面状态栏的显示。参数Msg不保存到日志列表里,只更新当前机器人的状态信息,在日志上方显示,可多次调用,更新状态。LogStatus函数有非常多的功能,可以在状态栏上显示各种数据,显示表格,显示图片。比较常用的是显示当前时间,显示策略的相关数据信息。

例如,在状态栏显示表格,写入一些信息数据:

python
import json def main(): while True: if exchange.IO("status"): tab1 = { "type": "table", "title": "行情数据", "cols": ["项目", "数据"], "rows": [] } tab2 = { "type": "table", "title": "账户数据", "cols": ["项目", "数据"], "rows": [] } tab3 = { "type": "table", "title": "持仓数据", "cols": ["项目", "数据"], "rows": [] } exchange.SetContractType("rb888") t = exchange.GetTicker() a = exchange.GetAccount() p = exchange.GetPosition() tab1["rows"].append(["tick 数据", json.dumps(t)]) tab2["rows"].append(["账户数据", json.dumps(a)]) tab3["rows"].append(["持仓数据", json.dumps(p)]) LogStatus(_D(), "\n`" + json.dumps(tab1) + "`\n" + "`" + json.dumps(tab2) + "`\n" + "`" + json.dumps(tab3) + "`") else: LogStatus(_D(), "未连接") Sleep(1000)

回测运行结果为:

image

注意:编写设计策略时不用刻意追求UI显示方面要多么华丽,把需要显示的信息显示正确即可。更多的LogStatus函数用法、例子可以参考API文档。

4.6.4 Chart(…)

Chart自定义图表画图函数,专门用于绘制各种类型的交互式图表。它支持折线图、区域图、柱状图、饼图等多种类型的图表,并提供了丰富的工具和选项,可以满足各种需求。

我们来讲下Chart函数的使用方法,在使用Chart画图函数时,我们需要使用数据和选项的配置对象来控制图表的显示和交互效果。

选项配置对象

选项配置对象用于控制图表的显示效果、交互效果等。以下是选项配置对象的常见属性:

  • chart: 表示图表的整体配置选项,包括类型(如折线图、柱状图、饼图等)、背景色、边框等。
  • title: 表示图表的标题配置选项,包括文本、样式等。
  • subtitle: 表示图表的副标题配置选项,包括文本、样式等。
  • legend: 表示图例(Legend)的配置选项,包括位置、样式等。
  • tooltip: 表示提示框(Tooltip)的配置选项,包括触发方式、内容格式等。
  • plotOptions: 表示系列(Series)的配置选项,包括类型、颜色、标签等。

下面我们示范一下,首先我们可以定义选项配置对象:

python
chart = { # 标记是否为一般图表,有兴趣的可以改成false运行看看 "__isStock": True, # 缩放工具 "tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"}, # 标题 "title": {"text": ""}, # 坐标轴横轴即:x轴,当前设置的类型是:时间 "xAxis": {"type": ""}, # 坐标轴纵轴即:y轴,默认数值随数据大小调整 "yAxis": { # 标题 "title": {"text": ""}, # 是否启用右边纵轴 "opposite": False } }

该选项配置对象包含了对图表的完整配置。其中__isStock属性表示是否为一般图表,选择 true 为 Highstocks , Highstocks 是一个专门用来创建交互式股票图表和金融图表库。它是 Highcharts 图表库的一部分,并提供了更多的功能,包括支持股票指标、数据区域缩小、鼠标拖拽和滚轮缩放等。 tooltip 属性定义了缩放工具的格式, title 属性定义了图表的标题, xAxis 属性定义了X轴的配置, yAxis 属性定义了Y轴的配置。

数据配置对象

数据配置对象用于控制图表的数据源。其中,最重要的属性是 series ,它是一个数组,每个元素表示一个系列(Series)的数据。数据配置对象:

python
series = [ {"name": "", "id": "", "data": []}, ]

该数据配置对象包含name、id、data属性。 name 属性用于图例显示的名称, id 属性为数据系列的唯一标识符, data 属性则为数据系列的数据。

双均线实例

我们将数据配置对象和选项配置对象的属性合并到一个名为 chart 的对象中。这个对象包含了所有的配置选项,从而实现了用一个对象控制整个图表的效果。这里我们举例一个双均线画图的配置。

python
chart = { # 标记是否为一般图表,有兴趣的可以改成false运行看看 "__isStock": True, # 缩放工具 "tooltip": {"xDateFormat": "%Y-%m-%d %H:%M:%S, %A"}, # 标题 "title": {"text": "均线"}, # 坐标轴横轴即:x轴,当前设置的类型是:时间 "xAxis": {"type": "datetime"}, # 坐标轴纵轴即:y轴,默认数值随数据大小调整 "yAxis": { # 标题 "title": {"text": "均线"}, # 是否启用右边纵轴 "opposite": False }, # 数据系列,该属性保存的是各个数据系列(线,K线图,标签等...) "series": [ # 索引为0,data数组内存放的是该索引系列的数据 {"name": "line1", "id": "线1,五日均线", "data": []}, # 索引为1,设置了dashStyle:'shortdash'即:设置虚线 {"name": "line2", "id": "线2,十日均线", "dashStyle": "shortdash", "data": []} ] }

在 chart 对象设置完成以后,接着我们需要往里面添加数据。

python
import time def main(): # 调用 Chart 函数,初始化图表 obj_chart = Chart(chart) # 清空 obj_chart.reset() while True: exchange.SetContractType("rb888") # 获取本次轮询的时间戳,即一个毫秒的时间戳。用来确定写入到图表的X轴的位置 now_time = int(time.time() * 1000) # 获取行情数据 r = exchange.GetRecords() # 五日均线 ave_5 = TA.MA(r, 5) # 十日均线 ave_10 = TA.MA(r, 10) # 用时间戳作为 X 值,均价作为 Y 值传入索引 0 的数据序列 obj_chart.add(0, [now_time, ave_5[-1]]) # 同上 obj_chart.add(1, [now_time, ave_10[-1]])

首先,通过Chart(chart)函数初始化了一个图表对象ObjChart,其中chart是一个选项配置对象,用于配置图表的各项属性。然后,通过obj_chart.reset()函数清空了图表中已经存在的数据。接下来,通过一个无限循环while True来实现不断获取并更新行情数据。在每次循环中,调用exchange.GetRecords()函数获取当前品种的K线行情数据,并用TA.MA()计算出五日和十日均线的值。

然后,通过int(time.time() * 1000)获取当前时间戳作为X值,将五日和十日均线的值作为Y值,调用obj_chart.add()函数把这些数据加入到图表中。其中,第一个参数0和1分别对应了选项配置对象中的两个数据序列,即五日均线和十日均线。第二个参数是一个包含X值(也就是时间)和Y值(就是最新时刻的均线值)的数组。

总之,这段代码通过数据配置对象和选项配置对象来定义图表,然后不断获取K线数据,计算均线,将计算结果添加到图表上,最终实现了两条均线的效果。

image

4.6.5 KLineChart()

使用 Python 编写策略时,设计策略图表的显示是非常重要的。对编程不熟练或者对优宽量化平台使用的图表库不熟悉的小伙伴,经常会苦恼于自定义图表上画图的代码设计。我们在上个阶段,使用Chart函数进行图表设置的时候,在图表参数设置,数据导入等阶段,都需要编写复杂的代码。那么有没有一种画图方法,结构更加清晰,而且只用编写少量的代码,又可以画出丰富内容的策略图表呢?

Pine语言相信大家都听说过,它是一门高度封装的专门为交易而生的语言。Pine语言有丰富画图功能,它的画图方式非常的简单,并且功能也十分的强大。如果可以把Pine语言的画图接口接入到Python语言的策略中进行使用,那么就极大得方便了我们设计策略的画图功能。于是优宽量化平台基于这种需求,升级了自定义画图功能,扩展了使用KLineChart函数进行自定义图表画图的方式。大家可以对比一下chart函数,KLineChart对于编程基础薄弱的我们,确实是一个更为友好地选择。

python
def main(): c = KLineChart() while True: if exchange.IO("status"): exchange.SetContractType("rb888") bars = exchange.GetRecords() if not bars: Sleep(1000) continue for bar in bars: c.begin(bar) c.plot(bar.Volume, "volume") c.close() Sleep(1000)

话不多说,让我们来尝试一下。可以看到下面的代码,相对于chart函数需要先设置一系列选项和参数的配置选项的图表对象,KLineChart非常简单和直观。你只需要在主函数中调用KLineChart()函数创建一个图表对象c就可以了。接着,你可以利用while循环不断地获取交易数据并绘制图表。

画图操作要从begin()函数开始,close()函数结束。begin、close函数都是图表对象的方法。我们还使用plot()方法将成交量指标画在图表上。其中,第一个参数bar.Volume表示成交量数据,第二个参数 volume 表示成交量指标的名称。在这个例子中,我们只绘制了一个成交量指标,但是你可以通过添加更多的plot方法来绘制其他的指标线。

最后,通过Sleep函数设置程序每隔1秒钟执行一次,以保证实时更新数据。我们看一下回测结果,可以看到,交易量图表呈现了出来。总之,利用KLineChart函数,我们可以在不编写复杂的代码的情况下,轻松地创建自定义的策略图表。

image

函数KLineChart()支持指标的主图和副图呈现,在具体指标画图函数里,需要添加参数overlay = True/False,当设置为True的时候,指标会同时呈现在主图上,否则会设置在副图上,可以可以更清晰的进行指标的呈现。与Chart函数相比,KLineChart更加简单、直观,也更加强大,支持绘制多种图形和指标线。需要注意的是策略自定义画图只能使用KLineChart()方式或者Chart()方式中的一种。

4.6.6 LogReset()

日志清除函数,该函数可以清除实盘或者模拟盘时实盘的日志数据。可以传入一个参数,指定保留最近多少条日志,清除其它日志。

python
def main(): LogReset(10)

4.6.7 EnableLog(IsEnable)

打开或者关闭订单信息的日志记录。参数值:isEnable为bool类型。IsEnable设置为false则不打印订单日志,不写入实盘数据库。

python
def main(): EnableLog(False)

4.7 常用内置函数

作为一个成熟的量化交易软件,优宽量化提供了很多方便易用的内置函数,节省了不少开发应用的时间,这里主要介绍常用内置函数及其用法。

4.7.1 Sleep(Millisecond)

Sleep()函数在商品期货策略中使用并不是很多,由于商品期货行情是推送机制,并不需要在策略程序中使用Sleep()函数强制等待一定时间,只需在必要的地方使用,例如在检测和期货公司前置机连接状态时(调用exchange.IO("status")函数)使用Sleep()函数,避免循环中无耗时引起设备CPU占用过高。Sleep()函数的参数为毫秒数。

4.7.2 GetCommand()

获取策略交互界面发来的命令并清空,没有命令则返回null,返回的命令格式为“按钮名称:参数”,如果没有参数,则命令就是按钮名称。

GetCommand()函数是一个非常重要的函数,策略的交互设计依赖于此函数。后面我们将在策略优化中使用实例进行具体讲解。

4.7.3 IsVirtual()

IsVirtual()函数用来判断当前策略运行为机器人运行还是回测运行。回测系统运行返回true,实盘或者模拟盘机器人运行返回false。

python
def main(): while True: if exchange.IO("status"): exchange.SetContractType("MA888") # 返回True exchange.GetTicker() LogStatus(_D(), "已经连接", "IsVirtual():", IsVirtual()) else: LogStatus(_D(), "未连接", "IsVirtual():", IsVirtual()) Sleep(1000)

可以看到策略回测时,状态栏上打印的IsVirtual()函数返回值为True。

4.7.4 _G(K, V)

可保存的全局字典,回测和实盘均支持,回测结束后,保存的数据被清除。KV表,永久保存在本地文件,每个机器人单独一个数据库,重启或者托管者退出后一直存在,K必须为字符串,不区分大小写,V可以为任何可以JSON序列化的内容。后面我们将在策略优化中使用实例进行具体讲解。

4.7.5 _D(Timestamp, Fmt)

_D(Timestamp,Fmt)函数在策略编写时也是经常用到的,例如打印当前时间字符串,就可以直接写作:Log(_D())。参数值:Timestamp为数值类型,Fmt为string类型,Fmt默认为:yyyy-MM-dd hh:mm:ss,返回值:string类型。

注意:不传任何参数就返回当前时间,例如:_D(),传入参数_D(1478570053),返回指定时间戳的字符串,默认格式为yyyy-MM-dd hh:mm:ss

在使用Python编写策略时需要注意,Timestamp参数为秒级别时间戳。其它语言调用该函数时,Timestamp参数为毫秒级别时间戳。

4.7.6 _N(Num, Precision)

_N(Num, Precision),格式化一个浮点数。参数Num为number类型,Precision为整型number。返回值:number类型。例如:_N(3.1415, 2)返回3.14。

4.7.7 _C(function, args…)

该函数会一直调用指定函数到成功返回,比如_C(exchange.GetTicker),默认重试间隔为3秒,可以调用_CDelay(...)函数来控制重试间隔,比如_CDelay(1000),指改变_C函数重试间隔为1秒。
注意_C函数可以对下面的函数进行容错调用。

  • exchange.GetTicker()
  • exchange.GetDepth()
  • exchange.GetTrade()
  • exchange.GetRecords()
  • exchange.GetAccount()
  • exchange.GetOrders()
  • exchange.GetOrder()
python
def main(): ticker = _C(exchange.GetTicker) _CDelay(2000) depth = _C(exchange.GetDepth) Log(ticker) Log(depth)

对于有参数的函数,使用_C(...)容错时:

python
def main(): records = _C(exchange.GetRecords, PERIOD_D1) Log(records)

4.7.8 _Cross(Arr1, Arr2)

Cross函数可以判断2个数组的交叉状态,返回数组arr1与arr2的交叉周期数。正数为上穿周期,负数表示下穿的周期,0指当前价格一样。它需要传入两个参数,并且这两个参数都必须是数组。在实际应用中,这个函数可以很方便判断两条均线是否金叉死叉。

python
arr1 = [1,2,3,4,5,6,8,8,9] arr2 = [2,3,4,5,6,7,7,7,7] def main(): Log("_Cross(arr1, arr2): ", _Cross(arr1, arr2)) # 3 Log("_Cross(arr2, arr1): ", _Cross(arr2, arr1)) # -3

可以看到_Cross(arr1, arr2)返回4,从序列中可以观察到,arr1上穿arr2已经3个BAR(倒数第3个位置”8>7“),所以返回3;与此相反,arr2下穿arr1已经3个BAR,返回-3。

4.8 常用指标函数

技术指标是以原始数据(开盘价、最高价、最低价、收盘价、成交量等)为基础,通过一定的数学计算得出的结果。优宽量化把常用的技术指标封装成一个个函数,编写策略时无须重新计算,从而提高策略开发效率。

4.8.1 内置的 TA 指标库

优宽量化 TA 指标库,优化了常用指标算法,支持 JavaScript、Python、C++。源码地址:https://www.youquant.com/bbs-topic/409。TA 库指标函数使用非常简单,以 TA.KDJ 指标为例:

python
def main(): exchange.SetContractType('rb888') r = exchange.GetRecords(PERIOD_M15) kdj = TA.KDJ(r, 9, 3, 3) Log("k:", kdj[0][-1], "d:", kdj[1][-1], "j:", kdj[2][-1]) # k: 18.870677786625926 d: 38.1501384991229 j: -19.68824363836803

参数为 K 线数据,指标参数。因为 KDJ 指标是一个二维数组,所以这里我们首先第一维定义不同的指标,第二维选择具体的时刻,可以看到返回结果为最新的指标数据。

返回的指标数据根据不同指标有所不同。MACD,KDJ 这类多线组成的指标,指标数据是一个二维数组。RSI,ATR,MA 这类指标是一条线,返回的是一维数组。

4.8.1 talib 指标库

与 TA 指标库类似,talib 指标库有众多交易指标,不过在使用之前需要首先进行引用。具体包含的指标可到进行查询优宽量化语法手册

python
import talib def main(): exchange.SetContractType('rb888') records = exchange.GetRecords() ret = talib.MACD(records.Close) Log("dif:", ret[0][-1], "dea:", ret[1][-1], "macd:", ret[2][-1]) # dif: -71.44652041129393 dea: -46.58286138984912 macd: -24.86365902144481

4.9 内置模板类库

俗话说站在巨人肩膀上,才能看得更远。有时候你想费尽精力实现一个功能,可能早已有极好的解决方法,如果可以用现成的,那么节省下来的时间是不是可以用在策略逻辑上呢?内置模板类库提供了大量的可复用的代码模块。

4.9.1 模板类库

优宽量化交易平台支持把一些常用的、可复用的代码模块封装成独立的库(在优宽量化交易平台上叫做模板类库)。这样可以提高策略开发速度,不用编写重复的代码,降低策略交易部分和策略逻辑部分的耦合度,便于策略维护、优化、扩展。

创建一个模板类库和创建一个策略操作相同,在「策略库」中点击「新建策略」跳转到创建策略页面,区别是策略类型选择「模板类库」。然后给模板起个名字,保存即可。模板类库设计主要有两个方面:第一是需要设计导出函数,导出函数为模板类库的接口函数,即这个模板类库提供哪些功能,通过调用导出函数去使用这个模板类库提供的功能。第二是需要设计这个模板类库的参数,和普通策略一样,模板类库也可以设置参数,用于使用时动态设置一些数值等参数数据。

4.9.2 画线类库

画线类库简化了策略图表画线的逻辑,使其更加简便易用。使用这个类库,可以直接调用封装好的函数来绘制图表上的各种线条。

主要特点包括:

  • 支持画多条线:可以轻松地在图表上绘制多条线,展示更多的数据。
  • 支持K线图:除了常规的线条外,还可以使用该类库绘制K线图,展示开盘价、收盘价、最高价和最低价等信息。
  • 支持flag小图标:除了线条外,还可以在图表上添加flag小图标,用于标注重要的时间点或事件。
  • 可扩展性强:也可以根据需要添加更多的图形支持,定制化图表展示。

这个类库的出现大大简化了策略编写过程中对图表的处理,使得开发者能够更加专注于策略逻辑的实现,提高了编程效率。

这里我们举例双均线画图的例子,ext.PlotLine 函数是 Python 版画线类库的画线函数, 用来在图表上画出一条曲线。
ext.PlotRecords 函数是画出 K 线,通过订阅合约,获取K线,计算均线,然后我们使用画线类库分别进行K线图,两条均线的画图。

python
def main(): while True: exchange.SetContractType("MA888") r = exchange.GetRecords() if len(r) < 10 : continue ma1 = TA.MA(r, 5) ma2 = TA.MA(r, 10) Log("五周期均线", ma1[len(ma1)-1], '#FF0000') Log("十周期均线", ma2[len(ma2)-1], '#00FF00') ext.PlotRecords(r, "RB") ext.PlotLine("五周期均线",ma1[len(ma1)-1]) ext.PlotLine("十周期均线",ma2[len(ma2)-1]) Sleep(1000 * 60 * 60)

image

4.9.3 交易类库

Python 版商品期货交易类库,这个模板类库移植自优宽量化交易平台 JavaScript 版商品期货交易类库。 主要功能是对于商品期货交易开仓平仓的操作。 可以学习该模板的设计思路,模板地址: https://www.youquant.com/strategy/24288

在量化交易中,时间是至关重要的,因为市场瞬息万变。一个好的交易函数可以快速响应市场信号并执行交易操作,从而提高交易的效率和准确性。为了实现这一目标,优宽量化平台封装了一些常用功能,例如开仓平仓、CTA函数、判断交叉等,并推出了实盘交易类库,方便进行交易的操作。通过使用交易类库,大家可以从繁琐的交易参数设置细节中解脱出来,更加专注于策略本身的编写,提升策略的执行效率。交易类库的使用方法很简单,作为一个内置的模版类库,大家可以直接勾选使用。当然如果大家想更好的构建自己的交易类库,也可以从这个公开的类库源码开始,这份代码附有注释,大家都可以参考学习下。今天我们介绍下这个交易类库的使用方法。

CTA函数

首先我们介绍一下CTA函数。它相当于一个策略编写的整体框架。CTA函数用于实现基于交易所行情和持仓信息的自动交易策略。该函数可以根据一系列设定的规则和条件执行买入和卖出操作。该函数接受三个参数:contractType(合约类型)、onTick(回调函数)和interval(轮询间隔)。

这里面我们讲一下onTick(st),它是一个回调函数。当每次获取到行情时会调用该函数。回调函数接收一个包含以下属性的对象作为参数:

  • records:行情数据列表。
  • symbol:当前交易合约代码。
  • detail:当前交易合约详细信息。
  • account:交易账户信息。
  • position:当前交易持仓信息。
  • positions:所有交易持仓信息。

这里的回调函数应返回一个整数,表示需要执行的买卖操作数量,大于0表示买入,小于0表示卖出,等于0表示不进行任何操作。

python
def main(): # 测试CTA函数 def callBack_CTA(st): if len(st["records"]) < 20: return emaSlow = TA.EMA(st["records"], 20) emaFast = TA.EMA(st["records"], 5) cross = ext.Cross(emaFast, emaSlow) if st["position"]["amount"] <= 0 and cross > 2: Log("金叉周期", cross, "当前持仓:", st["position"]) return 2 if st["position"]["amount"] < 0 else 1 elif st["position"]["amount"] >= 0 and cross < -2: Log("死叉周期", cross, "当前持仓:", st["position"]) return -2 if st["position"]["amount"] > 0 else -1 ret = ext.CTA("FG888", callBack_CTA)

这样讲解可能不太直观,我们举例示范一下。这是使用CTA库来进行金叉和死叉信号进行交易的例子。首先,调用了$.CTA函数,参数为玻璃主力期货合约,和一个回调函数。在我们的回测测试中,经常使用期货代码+888,表示该品种的主力合约,如果我们不使用交易类库,直接将期货888代码,带入实盘运行,会出现找不到该合约的错误提示。交易类库经过内置函数的映射,可以直接匹配888代码,为当前策略周期具体的主力合约代码。另外,CTA函数也可以映射到别的合约,如果我们设置i000/rb888,就是根据铁矿石的指数的K线,来交易螺纹钢的主力连续。另外,不同的主力合约是有一定的期限的,在策略运行的过程中,为确保策略长期的平稳运行,经常需要对主力合约进行更换。为解决移仓换月的麻烦,交易类库内置添加了,主力合约的自动移仓功能。

通过源码中可以看到,st包含了期货合约的综合数据,包括k线,账户和仓位等信息。通过st.records.length判断市场数据的记录数量是否大于等于20,如果不满足条件,则直接返回,不进行后续的逻辑判断和操作。如果市场数据记录数量满足要求,接下来使用TA.EMA函数计算了慢速(20周期)和快速(5周期)的指数移动平均线(EMA)。

通过$.Cross函数判断快速EMA和慢速EMA是否发生交叉。cross函数中需要两个数组进行交叉信号的判断,返回结果是上穿的周期数。正数为上穿周数,负数表示下穿的周数,0指当前价格一样。这里为了防止最新的k线没有走完,所以我们设定的阈值绝对值为2。如果快速EMA大于慢速EMA的时候交叉值大于2,则表示发生了金叉;如果快速EMA小于慢速EMA的时候交叉值小于-2,则表示发生了死叉。

大家也许会好奇,没有常规的开仓或者平仓语句,交易是怎样进行的呢,其实这就是我们刚才提到的return返回值进行下单。根据当前持仓情况和交叉的方向,通过return语句针对仓位进行不同的交易操作。CTA函数中交易的操作是通过return语句开展的,交易操作参数为n。如果当前持有的仓位st["position"]["amount"]为0,n就直接代表着开多(n为正)或者开空(n为负)的手数。如果当前的仓位st["position"]["amount"]不是0,进行的交易操作需要和持有的仓位进行比较,然后进行开平仓的操作。我们举例讲解下,比如现在st["position"]["amount"]为-3,代表持有三手的空仓,如果这时候n为2,就意味着需要平掉2手的空仓,还剩下1手的空仓不进行操作;而如果n为4,则代表需要全部平掉3手的空仓,然后再开一手多仓。所以在本例中,我们使用的就是这样的方法来进行对应开平仓的操作。这里面的逻辑确实有点复杂,大家可以去源码中进行查阅,加深一下理解。

在我们的例子中,策略的开平仓的逻辑是这样的,针对于金叉,如果当前仓位是空头或者没有的仓位,st["position"]["amount"]小于等于0,这个时候需要进行不同的交易操作。这里使用了一个三元表达式,如果是空头仓位(st["position"]["amount"]小于0),金叉出现应该首先平掉空仓,然后开一手多仓,所以这里的数字要填写为2;如果没有仓位(st["position"]["amount"]等于0),对应的数字是1,就是直接开一手多头仓位。针对于死叉也是一样,具有多头仓位的时候,首先需要平掉多头,然后再开一手空仓,数字填写为-2;没有仓位,直接开一手空头,填写数字为-1。

我们看一下回测结果,可以看到伴随金叉和死叉指标,多头和空头不断的对调开平仓,符合策略的交易逻辑。另外,CTA还很贴心的为我们添加了状态栏的展示,可以看到这里包括持仓状态和账户信息,方便实时了解策略的运行状态。状态栏的展示是使用LogStatus函数设置的,后面会为大家进行讲解。

除此之外,CTA函数还可以进行多品种的操作,这里我们设置参数为三个品种,FG888,MA888,SA888注意品种之间不要有空格。可以看到这个策略对着三个品种实时运行了起来,状态栏可以实时显示这三个品种的持仓状态和盈亏。总体来说,这段代码使用了CTA函数进行了简单的均线交叉策略的编写,希望从这个例子中,大家可以学习认识下CTA函数。

单品种操作

交易类库具有2种模式,第一种是针对单品种的操作:

python
obj = ext.NewPositionManager()

这里的obj为创建的单品种控制对象。请注意哈,这里的单品种操作不是指的是只能针对于单一的期货品种,而是按顺序的操作。

通过使用NewPositionManager对象提供的方法,可以方便地管理仓位和进行交易操作。可以传入一个可选的参数e,表示交易所对象,如果没有传入该参数,则默认使用当前交易所对象(exchange)。创建NewPositionManager对象后,可以使用其提供的方法进行仓位和交易管理。

  • GetAccount: 获取交易账户信息。
  • GetPosition方法:根据合约类型、方向和持仓信息列表,获取特定合约的持仓信息。
  • OpenLong方法:开多仓,传入合约类型和数量。
  • OpenShort方法:开空仓,传入合约类型和数量。
  • Cover方法:平仓,传入合约类型和数量。
  • CoverAll方法:平掉所有仓位。
  • Profit方法:计算特定合约的盈利情况。
python
def main(): # 测试 IsTrading Log("now time", _D(), "isTrading('MA405'): ", ext.IsTrading("MA405")) Log("now time", _D(), "isTrading('SR405'): ", ext.IsTrading("SR405")) Log("now time", _D(), "isTrading('jd2405'): ", ext.IsTrading("jd2405")) # 测试 NewPositionManager 导出函数 生成对象 obj = ext.NewPositionManager() Log(obj.Account()) # 测试 OpenLong 、 OpenShort 、GetPosition open_long = obj.OpenLong("MA405", 2) open_short1 = obj.OpenShort("SR405", 3) open_short2 = obj.OpenShort("jd2405", 4) Log("open_long:", open_long) Log("open_short1:", open_short1) Log("open_short2:", open_short2) Sleep(1000 * 60) positions = obj.GetPosition("MA405", PD_SHORT) Log("get MA405 PD_SHORT:", positions) positions = obj.GetPosition("MA405", PD_LONG) Log("get MA405 PD_LONG:", positions) # 读取平仓前的持仓信息 positions = exchange.GetPosition() Log("平仓前:", positions) # 测试 CoverAll obj.Cover("MA405") # 测试 CoverAll obj.CoverAll() # 读取 Cover、CoverAll 后的 持仓信息 positions = exchange.GetPosition() Log("平仓后:", positions) Log("实时收益:", obj.Profit())

我们举例示范一下:首先,代码通过 ext.NewPositionManager() 创建了一个名为obj的单品种控制对象对象。然后,代码使用 Log 函数输出了$.IsTrading()的结果。$.IsTrading(symbol) 是交易类库中一个用于判断指定交易品种是否正在交易中的函数。IsTrading支持判断各种期货品种的交易时间段,比如夜盘或者周末假期等,可以看到当设置时间为早上8点数未开盘时间,该函数返回结果False

接下来,代码调用 obj.OpenLong 方法开多仓,obj.OpenShort 方法开空仓。相对于我们前面讲过的Exchange.buy或者Exchange.sell函数,这里对交易的代码细节进行了一些优化。这里我们不需要填写任何的价格,只需要填写品种和数量就可以。在模版参数里也有滑价点数,大家也可以根据需要调整下。请注意,不同的品种的跳动点数是不同的,比如大多数品种是一个单位的跳动,而有个别品种,比如铁矿石,跳动单位是0.5。另外,目前交易类库里的交易函数是不支持限价单的,如果需要限价单策略,可以使Exchange.buy或者Exchange.sell等交易函数。

紧接着,代码使用了GetPosition()方法 用来获取指定交易品种和持仓类型的仓位信息。其中,PD_SHORTPD_LONG 是表示空头和多头仓位。因为MA405品种目前是多头的仓位,所以空头仓位信息返回值将会是null。最后,代码调用 obj.Coverobj.CoverAll 方法进行平仓操作。随后使用obj.Profit方法输出了仓位操作后的盈利情况。

点击开始回测,我们来看一下回测结果,符合我们的预设。这就是一个使用交易类库下单的简单例子。相对于前面我们讲过的交易函数,可以发现使用交易类库可以更加方便快捷的下单,省去了交易参数设置的烦恼。

多品种操作

第二种是针对多品种的操作。q = ext.NewTaskQueue(),q为创建的多品种队列控制对象。这里的多品种并不是意味着只能操作多个品种,而是多种操作同时处理,有点类似于并联的概念。这里涉及到了异步处理的概念。异步处理是一种编程模式,用于处理可能需要耗时的操作,在程序化交易中,市场信号的判断是很迅速的,然而价格的快速波动造成下单成交需要耗费更多的时间。在异步处理中,程序可以继续执行其他任务而不必等待比较耗时操作的完成。

这里给大家稍微解释一下同步处理和异步处理的不同。传统的同步处理方式会导致阻塞(blocking):就是当一个任务执行时,程序会一直等待该任务完成后再执行下一个任务。这可能导致程序运行效率低下,特别是在需要等待I/O操作完成时。而异步处理采用了非阻塞(non-blocking)方式,使得在执行比较耗费时间的操作时,程序可以同时执行其他任务。异步操作通常是通过回调函数等机制来实现的。

在异步处理中,当一个耗时任务启动后,程序立即将控制权返还给调用方,让其继续执行其他任务。然后,当耗时任务完成时,会触发相应的回调函数,用来处理任务的执行结果。由于异步处理不会阻塞主要流程,可以提高程序的性能和响应能力。相对于单品种串联的操作,异步处理可以较好的实现同时下达任务,各个任务在满足各自条件的情况下执行,所以各任务之间不会阻塞。因此,对于多品种或者多操作的复杂交易的场景,使用任务队列NewTaskQueue是非常适合的。我们来举例示范一下。

python
def main(): q = ext.NewTaskQueue() q.pushTask(exchange, "MA888", "buy", 3, lambda task, ret: Log(task["desc"], ret, q.pushTask(exchange, "MA888", "closebuy", 1, lambda task, ret: Log(task["desc"], ret, "#FF0000")) if ret else "", "#FF0000")) while True: q.poll() Sleep(1000)

我们来举例示范一下。这段代码是一个示例,展示了如何使用交易类库中的任务队列来处理多个非阻塞的交易任务。整体来说,这段代码中的异步处理体现在使用了任务队列ext.NewTaskQueue()以及q.pushTask()方法来执行非阻塞的交易任务。

首先调用ext.NewTaskQueue()创建了一个任务队列q。然后使用q.pushTask()方法将任务添加到队列中。其中,每个任务包含了交易相关信息和回调函数,用于处理任务完成后的返回结果。pushTask具体的参数是这样的:其中exchange是交易所,symbol是期货合约,action是对应的操作,amount是操作的手数,而onFinish是回调函数。

python
pushTask(exchange, symbol, action, amount, onFinish)

通过设置回调函数,如果当前任务成功执行,则继续调用q.pushTask()添加下一个任务到队列中,并指定相应的回调函数。在任务队列设置完成以后,while循环检查队列中是否还有待执行的任务。如果队列不为空,调用q.poll()从队列中提取并执行下一个任务。在执行任务期间,使用Sleep(1000)进行延迟等待,以便给交易操作留出时间。

通过使用任务队列和回调函数,可以实现交易任务的非阻塞执行。这种方式允许在等待交易结果的同时进行其他操作,提高了交易过程的效率和灵活性。了解到代码的整体思路以后,我们来看下具体的交易是怎样实现的:

q = ext.NewTaskQueue():创建一个新的任务队列对象q,用于存储交易任务队列;

pushTask() 函数中,首先通过调用 q.pushTask() 方法将一个买入任务添加到任务队列中。这个买入任务包含了交易相关的信息,交易品种("MA888")、交易类型(例如 "buy")、交易数量,以及一个回调函数。

当买入任务成功执行完成后,回调函数会被调用。在这个回调函数中,首先会记录买入交易的描述和执行结果。然后,如果买入交易成功(即 ret 为真值),则会再次调用 q.pushTask() 方法,将一个卖出任务添加到任务队列中。这个卖出任务也包含了交易相关的信息,以及一个回调函数。这样,在买入交易成功后,程序会立即添加一个卖出任务到任务队列中,以便在买入交易执行完成后立即执行卖出操作。

while True:在任务队列定义完成以后,使用while循环,不断检查任务队列q是否还有未完成的任务。

q.poll() 使用poll函数从任务队列q中取出一个任务,并执行该任务。执行任务后,任务会被移除队列。

Sleep(1000) 暂停1秒,让程序等待一段时间再继续下一次循环。这样可以确保任务的逐步执行,而不是立即执行完所有任务。

通过使用任务队列,可以实现非阻塞的交易任务处理。当一个任务完成时,会触发回调函数,并可以在回调函数中继续推入下一个交易任务。同时,通过循环和暂停的方式,确保任务按序执行。我们来看下回测结果,可以看到,第一步收到任务对甲醇主力合约开多仓,数量为3,返回任务的描述task.desc和任务结果ret,包括仓位的信息。第二步收到任务,平掉一手多仓。返回结果同样显示任务的描述和结果。

大家是不是有些疑问,我们这里使用了一个异步处理的方式实现了一个按顺序的操作,其实这个例子是让大家理解任务队列的交易处理和回调函数的底层逻辑,尝试实现异步的处理。请注意,这段代码只是为了教学示范作用,具体的使用方式可能需要根据具体的交易类库进行调整和修改。另外,Python语言还具有很多异步处理的设置,大家可以根据自己的经验进行更多异步尝试。

交易优化模块

image

这段配置是针对交易优化模块的设定,其中包括了以下几项:

  • 滑价:滑价是指在进行交易时,允许价格在一定范围内波动,以确保交易能够成功成交。在这里,滑价被设置为1,表示允许价格在当前市场价格上下波动一个单位。

  • 轮询间隔:这是指程序在执行轮询或检查状态时的时间间隔。在这里,轮询间隔被设置为500毫秒,意味着程序每隔500毫秒就会执行一次轮询操作。

  • 在状态栏显示持仓信息:这个设置确定是否在程序的状态栏中显示当前持仓信息。设置为true表示持仓信息会显示在状态栏中,这样用户可以方便地了解当前持仓情况。

  • 账户与持仓同步周期:这是指账户信息与持仓信息之间同步的时间间隔。设置为5表示程序每隔5秒就会更新一次账户与持仓信息,以确保信息的及时性和准确性。

一个好的交易函数可以集成风险管理工具和技术指标,帮助我们有效控制风险。通过在交易函数中实现止损、止盈、资金管理等功能,我们可以最大限度地降低交易风险,并保护资金免受不利市场波动的影响。在优宽量化平台,我们也有一些优秀的交易类库,比如止损类库,大家都可以学习参考下。

第 5 章 优宽量化实用功能介绍

第 5 章为大家介绍了优宽量化平台一系列实用功能,包括交易终端、数据探索模块、自定义数据源和本地回测引擎。我们将探讨如何利用这些功能提升交易效率,增强数据分析能力,以及获得更好的量化体验。

5.1 交易终端

一个好的交易软件对于金融交易的重要性不言而喻。在期货市场中,交易软件是投资者进行交易的核心工具,其质量和性能对投资者的交易成果具有显著的影响。怎么样是一个好的交易软件呢?一个好的交易软件的界面设计应简单明了,操作流程简明易懂,以便投资者快速上手并提高交易效率。在同时,一个好的交易软件具备丰富的功能,包括实时报价、图表分析、交易下单、止损设置、风险管理等,以满足投资者的各种需求。在市面上,一个好用的软件通常需要额外的付费;而免费的软件提供的功能较少,并且没有教程也很难理解具体的使用方法。另外,有不少用户反应,在MAC的电脑端平台,没有好用的期货交易软件,需要安装Windows虚拟机才能进行期货交易。为解决这些痛点,优宽量化平台,作为一个开放的金融量化平台,经过研发工作,优宽量化新版的交易终端终于上线了,网页端和手机APP端都支持,功能更加丰富,使用更加方便。大家可以体验一下,欢迎各位小伙伴提供反馈意见,我们会不断根据大家的需求进行交易终端的持续优化。

首先,我们介绍一下交易终端这个模块,大家可能没有使用过。优宽量化的交易终端最初的时候只有一个简单的交易界面,只是为程序化交易者临时使用。但是除了全自动的量化交易,有不少朋友都是手动交易,或者半自动交易的爱好者。随着我们交易经验的增加,普通的交易功能已经不能满足我们的需求。例如,我们了解不同品种之间具有高度的相关性,在我们交易的时候,经常需要查看多个市场或者品种的信息和指标作为参考,如果手动的点击品种进行实时价格的查看确实比较麻烦,也无法处于同一个页面中进行操作。优宽量化平台利用框架灵活的优势,开发出了全新的增强版交易终端,方便大家进行不同类型的交易,比如查看多品种合约,实时筛选品种,高频套利等需求。今天,我们就来展示一下新版交易终端具体的使用方法。

image

5.1.1 托管者-交易所-交易品种分组绑定

进入优宽量化平台,点击交易终端,就可以进入新版界面。我们来介绍一下新版交易终端的各项功能,第一个功能是托管者-交易所-交易品种分组绑定。这个功能是交易终端的核心功能。分组绑定是将交易所,托管者和目标品种进行一起的绑定,然后在每个功能模块里就可以选择不同的分组,就对应到了不同交易所的不同品种,可以及时的进行相应的操作。点击每个模块右上角一个带颜色和数字的小方块,就可以进入不同分组的选择。数字代表了分组id,颜色便于直观确定属于那一分组。点击进入分组详情可以对该分组进行设置。

image

我们举例示范一下,首先我们进行分组绑定,点击这里的设置分组,首先设置第一组,然后点击这里选择托管者,开户公司,和不同交易所下的各类期货品种。如果哪些合约是我们的经常做的合约,可以点前面的星号进行收藏,在自选分组里可以看到,这样分组绑定就完成了。对于非主力合约,我们需要搜索合约的名称可以进行绑定。这样子绑定分组,各个分组对应的模块也是使用相同的交易所,托管者和交易品种,如果这里我们改为燃油,可以看到对应的图像模块和盘口数据模块对应的品种都进行了更换。这就是分组绑定的设置。

大家可以对常用交易所和交易品种提前设置好分组。分组完成后这些数据就会保存,下次进入还可以直接使用。

接下来我们可以添加其他分组,例如对于黑色系,第二组和第三组我们还可以加上铁矿石和热卷进行绑定。分组绑定是十分方便的,通过绑定不同交易所和不同品种,我们在各个模块里就可以改变这里的分组,实现多个交易所,多个分组价格信息的实时展示和交易。大家在这里这可能会有疑问,当需要进行分组切换的时候,还需要一个个点击,也挺麻烦的。当然我们也考虑到了这个问题,怎么进行多个品种图像的呈现和交易呢,这就涉及到了交易信息插件化自由布局。

5.1.2 交易信息插件化自由布局

这是交易终端的另一个核心功能,我们可以自定义交易信息插件化自由布局。

交易终端的布局并不是固定不变的,我们可以设置这里的模块选择,大小和位置。这里布局中的一个个模块其实是交易插件,交易终端是支持各种插件的,点击右上角的拼图图标,就可以进行设置和操作。在我们交易的时候,一般需要的信息,比如K线数据、订单簿、成交订单流、账户信息、持仓信息、订单等,交易终端把交易界面各种信息展示做成单独的模块插件,大家可以按需添加使用,和删除。并且每个插件可以拖动和调整大小。结合前面的分组绑定功能,灵活性达到最大,这就意味着我们可以根据自己的需要搭建出来私人订制的交易系统。

image

我们展示一下一个自定义布局的搭建过程。点击齿轮按钮,可以看到这里的布局选择,可以看到这里的系统的示范布局,我们也可以自定义布局。点击这里的加号就可以新建一个布局。点击这里的编辑铅笔按钮,就可以重新命名或者删除布局。这里我们将这个布局命名为黑色系多品种。需要注意的是,这里不同布局里绑定的品种是不一致的,所以我们新建一个布局后,需要设置我们需要绑定的品种。在这里设置好绑定分组,接下来我们就可以进行页面布局了。根据我们的需要,删除不需要的模块,直接点击右上角的叉号。一个模块是可以重复添加的,,模块的大小和位置我们也可以自主调整。当我们初步设置好布局以后,点击这里的分组选择进行各个模块品种的选择。这样一个可以实时查看多品种信息,并进行交易的界面布局就设置好了。另外,这里的颜色模式,红涨绿跌和绿涨红跌,可以根据我们的需要进行选择;布局呢,还具有导入和导出的功能,文件格式是json文件,这样呢,我们就可以实现布局的保存。

image

确实很方便,大家可以根据自己的交易习惯,设置不同的交易布局,对于短线高频选手,盘口数据是必须的;而对于趋势选手,长线的趋势展示可以安排上。所以想象一下,配合一个大屏显示器,同时盯着多品种的行情信息,开着多个子账号随时交易,不需要切换浏览器标签,也不需要切换账户,确实非常的方便和实用。

image

5.1.3 工具插件

第三个特性核心功能是自定义功能模块,也就是交易插件的使用。刚才我们展示的可以实现各种需要的功能,这其实是通过交易插件来实现的。点击这里的拼图按钮,可以看到这里有系统插件和我们可以自定义的插件。这里的系统插件是优宽量化为我们装备的,这些插件对应了一个基本交易软件应该必备的模块,我们在原有的基础上进行了更具人性化的完善,图表模块实时观察k线的走势,我们可以选择不同的K线周期,还可以选择主图和副图呈现的指标进行实时趋势的判断;深度模块查看实时交易所返回的tick盘口数据;交易模块进行具体交易的操作,包括限价单和市价单下单操作,另外平仓这里还具有平今和平昨的选项;订单可以查看实时的挂单,账户的资产,和持有的仓位;逐笔交易查看实时的交易记录;另外还有系统插件里的工具,可以看到优宽量化官方准备的一些小程序插件。比如酷炫的订单流展示图,还有今日的期货交易结算单,各个品种的交易时间,网格交易,以及一键查询各个合约代码,委托查询和成交明细,都可以帮助我们进行期货的信息查询和交易操作的帮助,非常建议大家尝试使用下。另外这里我们新增了调试工具,我们可以JS语言进行一些实时信息的获取和交易逻辑的测试,例如这里我们查询一下主力螺纹钢合约的信息,这样呢可以进行目标品种的实时策略调试和运行。

image

除了使用官方的插件,大家也可以自己编写插件,定义自己想要的功能。插件的编写方法我们后续将为大家进行介绍。

5.1.4 其他细节

对于交易终端的各个模块的细节,我们在结合用户需求的情况下,也进行了很多的完善。例如这里的对于行情信息,我们可以点击刷新按钮进行模块数据的刷新,点击这个按钮,模块里的信息可以设置是否自动刷新,以及刷新的周期。另外,在某些插件运行卡顿的时候,我们可以选择重启终端进程。

以上的讲解并不能涵盖所有的功能,大家如果感兴趣的话,可以来这里切身体验一下。如果大家体验有问题或者哪些功能需要加强,都可以提出来,我们的工程师会尽力的去完善,帮助大家获得更好的交易体验。

5.2 交易插件

优宽量化身为量化交易平台,主要是为了服务程序化交易者。但也提供了简洁好用交易终端,通过不同板块和功能的组合,我们可以自定义属于自己交易界面,打造专属于自己的量化交易辅助系统。

我们经常看到类似文华财经,MT4等具有一键下单,多种功能的止盈止损功能等辅助交易模块,然而这种类似方便的功能需要我们额外的付费(年费通常在7000元左右)。优宽量化平台为了完善交易终端的体验,现在增加了插件功能。有时候,我们需要一个小功能来辅助交易,比如一键平仓、一键对冲、阶梯挂单、冰山委托等操作,这种类似的功能并不太需要经常查看执行日志,所以新建一个实盘有些繁琐,我们直接在终端点击一下插件,就能够立即实现相应的功能,这样可以大大方便手动交易。这些自定义的交易插件,我们自身可以编码实现。并且这个交易终端是定制化的,这就意味着我们可以根据我们的交易习惯,自定义交易功能辅助面板。

本节课,我们来学习交易插件的使用和编写。这个界面是为了快速的进行跨期对冲交易布置好的页面,原始的页面可以通过点击重置页面布局,进行恢复。这个就是初始的界面布局。如果我们想添加交易插件,通过右上角的拼图按钮,我们就可以点击交易插件进行使用。比如,我们点击近远月差价显示的插件,它就会展示在交易终端页面,输入目标合约,点击执行就会出现目标合约的跨期差价图像。需要注意的是,插件不会显示出日志,但是可以返回显示表格。交易终端插件运行时长最长为3分钟,超过3分钟自动停止运行,但是当我们需要进行一些时间较短的交易操作,或者当前的状态查看,3分钟还是足够的;并且也可以用来测试我们的实盘策略,新鲜的策略通过交易插件测试完成后,可以应用于实盘进行成熟的仿真或者实盘交易。

在插件里面是可以设置参数,没有参数的插件可以直接运行。插件的摆放位置,大小都是可以调整的,关闭插件点击右上角x号就可以,我们可以根据我们的交易习惯,进行不同功能模块的编写和摆放。

我们来稍微了解下插件的原理,插件其实相当于立即运行的实盘,功能和调试工具相同,所以插件运行直接对接真实市场的,在交易终端所选的托管者、交易对、K线周期就是默认的相应参数。交易插件会发送一段代码到交易终端页面的托管者进行执行,并且支持返回图表和表格,交易插件和调试工具都是免费使用的。

我们来看下插件是怎样编写的,在新建策略页面,设置策略类型为:交易插件。语言编写支持JavaScript、Python、C++、My语言。插件的main函数return的结果会在运行结束后,在终端弹出,支持字符串、画图和表格。因为插件执行看不到日志,可以将插件的执行结果return返回。插件的用途很广泛的,在很多时候手动交易需要很多重复执行的操作,其实这些操作都可以用插件实现。今天,我们讲解的插件使用,是辅助手动期货跨期对冲的插件套装。

期货跨期对冲是很常见的策略,由于频率不是很高,我们经常会手动操作,需要在分析差价走势的基础上,一个合约做多,一个合约做空。如果在期货软件上,我们可能需要复杂的分析和操作,而在交易终端使用插件,将大大节省我们的精力。当我们想进行手动的对冲交易的时候,我们可以调整下布局,将我们需要的插件摆放到合适的位置。

为方便进行跨期对冲,我们编写了三个插件,第一个是可以查看最新的跨期合约差价,这里我们填写目标合约,点击执行就会呈现最新以秒为单位的差价,并且这里还有有均线的显示,可以用来判断实时差价的偏离。当我们通过差价显示认为入场的时机到,我们就要迅速的进场进行交易,在没有辅助功能的期货软件中,我们需要手动的找到两个目标品种,然后填写价格进行相反方向的开仓,然而有时候,入场的最佳时间是很短暂的,当我们手忙脚乱开仓后,可能最佳的入场点已经过去;而使用我们的交易插件,当事先设置好交易品种,数量和方向(这里的reverse,代表是正套还是反套),一键点击执行就可以完成双向开仓的操作,这里点击一下仓位;可以显示目前的仓位状态;最后,当价差回归正常,我们就要进行双向的平仓,我们事先设置好滑价,点击就可以平仓。是不是很方便,今天呢,我们就要学习这些插件是如何编写出来的,它和平常的策略编写是有一些不同的,我们在讲解编码的过程中会为大家及时提醒。

5.2.1 跨期差价插件

首先介绍的是画跨期差价插件,这里我们首先设置好外部的参数,期货合约A和B。然后回到代码部分,这里使用原生的chart画图函数,设置图表对象chart,设置好title,x轴,y轴和数据列表,这里想呈现两根线,所以分别设置diff和meandiff。

回到主函数,首先分别获取两个目标合约的最新k线,这里的exchange.GetRecords中填写参数为1,表示要获取的k线周期为1s。接下来,我们要利用K线数据进行差价的计算,两个k线是轮询获取的,因此数组的长度可能会有不一致,为了保存两个差值计算时间的一致对照性,所以我们取两者的最小值,定义为变量rlength;然后定义差价储存列表difflist。

使用for循环,根据获取的k线长度,按索引计算两个合约的diff值,然后向chart的第一个数据系列series[0]添加数据,时间戳也是这样的设置;接下来计算diff的均值,使用difflist收集,使用TA.MA进行计算,这里设置的周期为20,为了更及时的展现变化,最后向chart.series[1]添加最新的diff均值。这样图表的设置就完成了,最后return一下chart结果就可以。有些同学们可能会好奇,这里我们不设置chart更新,和while循环吗?这里需要解释的是,我们这里进行的图像展示,是一个瞬时的差价快照,可以帮助我们迅速的判断当前的差价状态,所以没有设置chart更新和while循环。

python
chart = { "__isStock": True, "title": {"text": '差价分析图'}, "xAxis": {"type": 'datetime'}, "yAxis": { "title": {"text": '差价'}, "opposite": False, }, "series": [ {"name": "diff", "data": []}, {"name": "meandiff", "data": []}, ] } def main(): exchange.SetContractType(Contract_A) recordsA = exchange.GetRecords(1) exchange.SetContractType(Contract_B) recordsB = exchange.GetRecords(1) rlength = min(len(recordsA), len(recordsB)) difflist = [] for i in range(rlength): diff = recordsA[len(recordsA) - rlength + i] - recordsB[len(recordsB) - rlength + i] chart["series"][0]["data"].append([recordsA[recordsA.length - rlength + i].Time, diff]) difflist.append(diff) meandiff = sum(difflist[-20:]) / min(len(difflist), 20) if len(difflist) >= 20 else sum(difflist) / len(difflist) chart["series"][1]["data"].append([recordsA[recordsA.length - rlength + i].Time, meandiff]) return chart

5.2.2 一键对冲开仓插件

这样我们的第一个插件就设置好了,接下来我们来看第二个插件的设置:一键对冲开仓插件。同样的,首先设置策略的参数,在策略参数里,我们设置好交易合约A和B,开仓数量,滑价和正套还是反套的交易选项。这里给大家稍微解释下,当差价大的时候,我们预测价差会回归,从而进行反套的交易,reverse设置为true,进行空A多B的操作;而当差价小的时候,预测价差会变大,进行正套的交易,reverse设置为false,进行多A空B的操作。

所以在主函数里,这里我们首先设置开多的交易操作,使用reverse判断进行正套还是反套的交易,使用if表达式设置获取相应合约,然后获取ticker数据,这里还进行了容错的处理,如果没有获取到ticker数据,直接报错;SetDirection是buy,因为我们想快速的成交,所以设置的价格为当前的卖价加上滑价,这样一个较高的价格可以提升多仓快速成交的成功率;于此相反的,对于空头的操作,和多头的操作刚好相反就可以,设置SetDirection是sell,价格设置是当前较低的买价再减去滑价。这样就完成了开仓的操作。

开仓完成以后,我们想在插件中呈现两个开仓的品种,方向,价格和差价,我们这里设置一个展示的tbl表格,包含需要呈现的开仓信息。接着,在下面的代码中,设置开仓完成以后,休息1s,接下来我们获取仓位的数据。如果判断有仓位,就是开仓成功,使用轮询,然后使用if找到我们需要的品种,获取对应的品种名称,开仓方向和价格,然后计算开仓是的差价,push到tbl中,最后使用return进行呈现。在插件中,我们不想呈现多余的信息,所以SetErrorFilter设置一下,过滤多余的错误信息。这样一键对冲开仓插件就设置好了。

python
import time def main(): tbl = { 'type': 'table', 'title': '开仓信息 ' + str(time.strftime("%Y-%m-%d %H:%M:%S")), 'cols': ['合约A', '合约A价格', '合约A方向', '合约B', '合约B价格', '合约B方向', '价差'], 'rows': [] } contract_type = Contract_B if Reverse else Contract_A SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder") exchange.SetContractType(contract_type) ticker_A = exchange.GetTicker() if not ticker_A: return '无法获取数据' exchange.SetDirection('buy') exchange.Buy(ticker_A['Sell'] + Slip, Amount) exchange.SetContractType(Contract_A if Reverse else Contract_B) ticker_B = exchange.GetTicker() if not ticker_B: return '无法获取数据' exchange.SetDirection('sell') exchange.Sell(ticker_B['Buy'] - Slip, Amount) time.sleep(1) pos = exchange.GetPosition() if pos: for position in pos: if position['ContractType'] == Contract_A: contractA = position['ContractType'] typeA = '多头' if position['Type'] == 0 else '空头' priceA = position['Price'] if position['ContractType'] == Contract_B: contractB = position['ContractType'] typeB = '多头' if position['Type'] == 0 else '空头' priceB = position['Price'] diff = priceA - priceB tbl['rows'].append([contractA, priceA, typeA, contractB, priceB, typeB, diff]) return tbl

5.2.3 一键对冲平仓插件

接下来是最后一个插件的设置一键平仓。首先我们获取持仓的信息,如果没有持仓,直接返回“当前无持仓”信息提醒;在判断有持仓情况下,进行相应的平仓的操作。

python
def main(): SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder") pos_start = _C(exchange.GetPosition) if not pos_start or len(pos_start) == 0: return '已无持仓' for pos in pos_start: if pos["Type"] == 0: direction = 'closebuy_today' elif pos["Type"] == 1: direction = 'closesell_today' elif pos["Type"] == 2: direction = 'closebuy' elif pos["Type"] == 3: direction = 'closesell' else: return '未知持仓类型' exchange.SetContractType(pos["ContractType"]) ticker = exchange.GetTicker() if not ticker: return '无法获取ticker' if direction == 'closesell' or direction == 'closesell_today': exchange.SetDirection(direction) exchange.Buy(ticker["Sell"] + 2, pos["Amount"]) elif direction == 'closebuy' or direction == 'closebuy_today' : exchange.SetDirection(direction) exchange.Sell(ticker["Buy"] - 2, pos["Amount"]) else: return '无效的操作方向' Sleep(2000) pos_end = exchange.GetPosition() if not pos_end or len(pos_end) == 0: return '平仓完成'

这样我们三个小插件就编写完成了,当然这个插件还是比较简陋的,容错性还需要提高,大家在使用的过程中,如果遇到错误,可以留言,我们可以及时改正。不过对于基本的使用还是没有问题的,在交易终端里,我们使用N视界仿真账户,设置好目标合约,首先查看差价,等到入场时机设置reverse对应开仓,到达心理盈利点位或者止损点位,进行平仓,使用起来还是比较丝滑的,比手动进行开仓确实方便多了。看了这个插件的使用,我们应该也有了自己的想法,大家可以根据自己的交易习惯,不妨写成插件方便自己的手动交易。大家如果有好的想法和需求,也可以提出来,我们尽力也会为你实现!

5.3 数据探索模块

优宽量化团队对数据探索模块进行了全面升级。新版可以帮助大家快速获取各种类型的重要数据,更重要的是,它提供了强大的分析工具,使大家能够深入研究基本面数据与不同品种走势之间的关系。这样,大家不仅能够获得所需数据,还能更有效地分析和利用这些数据,从而做出更明智的投资决策。

image

数据探索模块是优宽量化自研开发的DATADATA平台应用模块。作为优宽量化平台精心打造的商品期货研究利器,DATADATA不仅全面汇总了多元化的期货宏观数据和深入的基本面信息,还实时更新期货,期权以及股票行情动态,确保用户第一时间掌握市场动态。通过这一平台,投资者和研究人员可以更加便捷地搭建起针对商品期货品种的系统性研究框架,从而在复杂多变的期货市场中做出更加明智和精准的决策。无论是对于初入市场的新手,还是对于资深交易者,DATADATA都将成为不可或缺的研究和分析工具。在后续的开发工作中,我们也会在策略编写当中提供基本面获取的API接口。这样呢,我们就可以在探索基本面信息和品种走势关系的基础上,在自己的策略当中,应用这些关系构建一个更加全面的交易系统。

image

5.2.1 数据条目

我们来看下怎样进行使用数据探索模块进行基本面数据的获取和分析操作。首先打开优宽量化网页版,进入控制中心,进入数据探索模块。可以看到这里进行了全新的改版,点击数据搜索框,会出现宏观数据,期货期权,Tick数据。并且如果大家有想要分析的数据条目也可以自行上传。

image

宏观数据

宏观数据栏包括期货数据和行情数据两大类。我们打开看一下,一共包含数十个条目,整体汇总了包含宏观数据,比如央行汇率,GDP,CPI,PMI等,还有期货的基本面数据,包括期货合约对应期货代码,期货标准仓单,基差表等。行情数据中包含了股票每日行情,期货每日行情和黄金现货日行情的数据。

期货期权

期货期权数据包括六大交易所包含的期货品种信息,行情信息以及相应的期权行情信息。通过对期货合约的行情数据分析,我们可以更好地把握市场趋势、价格波动情况和交易机会。同时,期权行情信息的分析可以帮助大家评估期权的价格水平、波动性预期以及期权合约的风险收益特征。

Tick数据

优宽量化平台悉心收集了各个品种详尽的Tick数据,作为量化研究人员和交易员们获取洞察的必需工具。Tick数据作为盘口交易数据,具有高频、实时的特点,对于深度分析和实时监控市场至关重要。通过对Tick数据的深度分析,交易者们可以揭示市场的隐藏规律和特点,包括价格走势、交易量分布、成交价与成交量之间的关系等。这种深度分析有助于交易者更准确地判断市场趋势的强弱和转折点,及时调整交易策略并做出相应的交易决策。

大家可以这里的搜索框寻找自己想要的数据,如果找不到自己想要的数据,大家可以留言,我们呢工程师会后续的不断完善数据库,更好的帮助大家方便快捷的获取到更多的数据。

image

5.2.2 操作指南

数据查询

在数据探索模块,数据是采用SQL语句进行查询的。通过灵活的SQL查询,我们可以自由组合各种查询条件,以满足不同分析的查询需求。我们举例示范一下,比如我们选择基差表,可以看到该条目下具体包含的列,有ID,时间戳,商品,基差等一系列数据,方便大家从不同维度研究商品品种的基差状况。点击这里的图表,这是数据预览模块,可以看到保存数据的具体样式,可以看到这里的数据是使用SQL查询的,只返回了10条数据。并且这里包含了sql的示范语句,我们可以使用更多的SQL数据进行数据的查询。

接下来我们来进行自定义数据的获取,我们来看右边上方的小方框,这里呢用来运行SQL语句进行数据查询。比如我们运行一下刚才示例的SQL语句,可以看到返回了10条的数据。如果我们想获得固定日期,或者固定品种的品种信息,只需要改变SQL语句就好。例如这里想查询沪铜的基差信息,输入语句:

sql
select * from futures_data.basis where product_name = '沪铜' ORDER BY date

可以看到立即进行返回了沪铜品种从2011至最新的基差数据。这里需要注意的是,需要把SQL的语句写正确,否则会返回SQL语句解析错误。文字的图表大家可能觉得不太直观,于是贴心的优宽量化也提供了可视化的选项。

可视化分析

通过点击右下角的可视化,这里有一系列的图表进行选择,包括观察面积占比的饼图,矩形树图;时间流变化的折线图,面积图和散点图等,还有一些高端的旭日图,热力图和桑葚图等,针对于不同类型的数据,我们可以选择不同的图表进行更加丰富可视化的呈现。

image

这里我们举一个简单的范例,使用折线图对玻璃品种近200日的基差表进行分析,首先写好SQL语句。

sql
SELECT * FROM futures_data.basis WHERE product_name = '玻璃' ORDER BY date DESC LIMIT 200;

我们点击折线图图标,进入绘图选项的页面。首先选择数据模块,第一个维度选择的是X轴(时间),第二个选择系列,也就是Y轴,这里可以选择不同的列,定义折线的颜色;点击三点符号这里选择折线样式,比如左侧还是右侧,这里当使用两个数据时候,可以定义两个Y轴;还有折线的样式等;第二栏定义轴线,包括具体轴线的设置;第三栏定义具体的显示设置,包括堆叠,图例,还有数据缩放的设置。

image

如果这里我们想画现货价格,期货价格,以及具体的基差,可以这样操作。首先选择现货价格,再添加主力合约价格。最后我们系列里面设置,右侧y轴可以加上差价图,点击添加,然后将y轴定义为右侧。这样我们可以看到最近200日的玻璃期货,现货,和差价的走势图。由此呢,我们可以判断玻璃的升贴水情况,为我们的交易做一些参考。

image

另外呢,在SQL语句中也可以添加外部的参数,这样我们就可以探索不同品种的基差走势图。

image

实盘回放

优宽量化平台为了满足用户的需求,结合流数据分布式表同步回放功能,开发出了盘口实时回放功能。这项功能可以帮助交易者在真实市场环境下模拟交易,并实时观察市场的动态变化。通过不断地实践和观察,交易者可以逐步培养自己的交易节奏,提高对市场的敏感度和把握能力。

首先我们需要进入优宽量化平台官网,点击进入数据探索模块,依次通过选择Tick数据,编写Sql代码选择目标期货品种,然后点击可视化,选择实盘回放功能,这样就可以进入仿真实盘回放模块。不需要任何额外工具的导入,我们可以使用优宽量化平台自带的数据源进行实盘回放的功能,既方便又快捷。

image

我们来观察下实盘回放面板,可以看到它包含三个功能模块,上面是价格分时图,可以观察目标品种的价格实时走势;左下方是逐笔交易历史,由于期货交易所特殊的数据返回机制,并不会返回每笔的交易记录,所以这是根据Tick数据反推出来的交易历史;右下方是盘口的实时数据,动态呈现买一价和卖一价,以及相应的申请数量。

image

最后我们要介绍一下,现在社区板块也支持数据探索模块进行数据引用和图表呈现。点击创建主题,进入编辑页面,选择这里的折线图按钮,就可以进入数据探索的模块。当我们书写好SQL语句获取数据,然后进行可视化设置好图表,点击保存。这样,大家实时探索出来的研究结果就可以在自己的文章中进行呈现。

image

在优宽量化平台上,丰富的数据资源为我们提供了广阔的分析空间。通过利用数据探索模块,我们能够运用SQL语句进行灵活查询,深入挖掘基本面信息与期货品种走势之间的潜在联系。这种深入的数据分析使我们能够更全面地理解市场动态,进而在构建策略模型时确保其更加合理和全面。这样,我们不仅能够更好地把握市场趋势,还能在制定投资策略时更加精准和有效。

5.4 自定义数据源

依托于优宽量化交易平台回测系统的自定义数据源功能,我们可以直接把收集到的外部数据作为回测系统的数据源,这样可以让回测系统应用于任何我们想回测历史数据的市场了,包括外汇,商品现货,股票等等。

5.4.1 数据格式

系统用GET方法请求自定义的URL(可公开可访问的网址)来获取外部数据源进行回测,附加的请求参数如下:

参数意义说明
symbol品种名如: futures.MA888
eid交易所如: Futures_CTP
round价格精度如3 那么返回的数据里的价格都要乘于1000取整
vround数量精度如2 那么返回的数据里的数量都要乘于100取整
periodbar周期(毫秒)比如60000为请求分钟bar
depth深度档数1-20
trades是否需要分笔数据true/false
from开始时间unix时间戳
to结束时间unix时间
custom----
detail----
ttl----

round与vround是为了避免网络传输过程中浮点数的精度丢失设计的两个参数,价格数据和成交量、订单量数据都采用整型传输。

一个拼接后的数据的例子:

python
http://customserver:80/data?symbol=futures.MA888&eid=Futures_CTP&round=3&vround=3&period=900000&from=1564315200&to=1567267200

数据返回的格式必须为以下两种格式其中之一(系统自动识别):

模拟级 Tick

普通的Bar级别回测,以下是数据范例:

python
{ "schema":["time", "open", "high", "low", "close", "vol"], "data":[ [1564315200000, 9531300, 9531300, 9497060, 9497060, 787], [1564316100000, 9495160, 9495160, 9474260, 9489460, 338] ] }

实盘级 Tick

Tick级回测的数据(包含盘口深度信息,深度格式为[价格, 量]的数组。可有多级深度,asks为价格升序,bids为价格倒序),以下是数据范例:

python
{ "schema":["time", "asks", "bids", "trades", "close", "vol"], "data":[ [1564315200000, [[9531300, 10]], [[9531300, 10]], [[1564315200000, 0, 9531300, 10]], 9497060, 787], [1564316100000, [[9531300, 10]], [[9531300, 10]], [[1564316100000, 0, 9531300, 10]], 9497060, 787] ] }

特殊的列属性asks、bids、trades:

字段说明备注
asks/bids[[价格, 数量], ...]例如实盘级 Tick数据范例中的数据:[[9531300, 10]]
trades[[时间, 方向(0:买,1:卖), 价格, 数量], ...]例如实盘级 Tick数据范例中的数据:[[1564315200000, 0, 9531300, 10]]

5.4.2 自定义数据源范例

这里我们通过结合Mysql数据库,将数据库保存的数据进行导出定义为自定义数据源,导入至回测系统进行使用,下面我们来看具体怎样实现。

python
import pymysql import _thread import json import math from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import parse_qs, urlparse def url2Dict(url): query = urlparse(url).query params = parse_qs(query) result = {key: params[key][0] for key in params} return result class Provider(BaseHTTPRequestHandler): def do_GET(self): try: self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() dictParam = url2Dict(self.path) Log("自定义数据源服务接收到请求,self.path:", self.path, "query 参数:", dictParam) databaseName = dictParam["database"] tableName = dictParam["table"] # 连接数据库 Log("连接数据库服务,获取数据,数据库:", databaseName, "表:", tableName) # 创建 MySQL 连接对象 cnx = pymysql.connect( host=????, # 数据库主机地址 user=????, # 数据库用户名 password=????, database=databaseName ) # 创建 MySQL 游标对象 cursor = cnx.cursor() # 获取到表数据 cursor.execute("SELECT * FROM " + tableName) # 获取查询结果 results = cursor.fetchall() # 要求应答的数据 data = { "schema" : ["time", "open", "high", "low", "close", "vol"], "data" : [], "detail": {'PriceTick': 1, 'VolumeMultiple': 10, 'ExchangeID': 'CZCE', 'LongMarginRatio': 0.12, 'ShortMarginRatio': 0.12, "InstrumentID": 'SA888'} } for item in results: # 将数值部分添加到data.data中的列表 data["data"].append([item[1], item[2], item[3], item[4], item[5], item[6]]) Log("数据:", data, "响应回测系统请求。") # 写入数据应答 self.wfile.write(json.dumps(data).encode()) cursor.close() cnx.close() except BaseException as e: Log("Provider do_GET error, e:", e) def createServer(host): try: server = HTTPServer(host, Provider) Log("Starting server, listen at: %s:%s" % host) server.serve_forever() except BaseException as e: Log("createServer error, e:", e) raise Exception("stop") # 创建 MySQL 连接对象 cnx = pymysql.connect( host='????', # 数据库主机地址 user='????', # 数据库用户名 password='????' # 数据库密码 ) # 创建 MySQL 游标对象 cursor = cnx.cursor() # 创建一个新的databases cursor.execute("CREATE DATABASE IF NOT EXISTS my_database") # 在新的databases中创建table来保存期货的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存 cursor.execute("USE my_database") cursor.execute("CREATE TABLE IF NOT EXISTS future_kline ( \ id INT AUTO_INCREMENT PRIMARY KEY, \ timestamp BIGINT NOT NULL, \ open FLOAT NOT NULL, \ high FLOAT NOT NULL, \ low FLOAT NOT NULL, \ close FLOAT NOT NULL, \ volume FLOAT NOT NULL, \ open_interest FLOAT NOT NULL)") # 在新的databases中创建table来保存期权的k线数据,K线数据格式{'Time': 1673884800000, 'Open': 2127.0, 'High': 2148.0, 'Low': 2127.0, 'Close': 2148.0, 'Volume': 2150.0, 'OpenInterest': 3742.0},分割成列进行保存 cursor.execute("USE my_database") cursor.execute("CREATE TABLE IF NOT EXISTS option_kline ( \ id INT AUTO_INCREMENT PRIMARY KEY, \ timestamp BIGINT NOT NULL, \ open FLOAT NOT NULL, \ high FLOAT NOT NULL, \ low FLOAT NOT NULL, \ close FLOAT NOT NULL, \ volume FLOAT NOT NULL, \ open_interest FLOAT NOT NULL)") def main(): preBarTime = 0 Log('测试') try: # _thread.start_new_thread(createServer, (("localhost", 9090), )) # 本机测试 _thread.start_new_thread(createServer, (("0.0.0.0", 9090), )) # VPS服务器上测试 Log("开启自定义数据源服务线程", "#FF0000") except BaseException as e: Log("启动自定义数据源服务失败!") Log("错误信息:", e) raise Exception("stop") while True: # 执行策略逻辑循环 if exchange.IO("status"): # 检测是否与期货公司服务器连接,登录成功 LogStatus(_D(), "已经连接") exchange.SetContractType("SA401") # 设置要操作的合约,这里设置为SA401,也可以做成参数,由策略参数上进行设置 futureR = exchange.GetRecords(PERIOD_M5) exchange.SetContractType("SA401P1500") # 设置要操作的合约,这里设置为SA401P1500,也可以做成参数,由策略参数上进行设置 optionR = exchange.GetRecords(PERIOD_M5) Log(_D(futureR[-1].Time/1000), '期货时间') Log(_D(optionR[-1].Time/1000), '期权时间') if len(futureR) < 2 or len(optionR) < 2: continue newfutureR = futureR[-2] newoptionR = optionR[-2] if newfutureR.Time == newoptionR.Time and preBarTime != newfutureR.Time and preBarTime != newoptionR.Time: Log('更新时间到') Log(_D(newfutureR.Time/1000), '期货') Log(_D(newoptionR.Time/1000), '期权') preBarTime = newfutureR.Time # 如果获取到futureR 和 optionR,添加到新创建的两个表中 cursor.execute("USE my_database") cursor.execute("INSERT INTO future_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \ (newfutureR.Time, newfutureR.Open, newfutureR.High, newfutureR.Low, newfutureR.Close, newfutureR.Volume, newfutureR.OpenInterest)) cnx.commit() cursor.execute("USE my_database") cursor.execute("INSERT INTO option_kline (timestamp, open, high, low, close, volume, open_interest) VALUES (%s, %s, %s, %s, %s, %s, %s)", \ (newoptionR.Time, newoptionR.Open, newoptionR.High, newoptionR.Low, newoptionR.Close, newoptionR.Volume, newoptionR.OpenInterest)) cnx.commit() else: LogStatus(_D(), "未连接") # 如果未连接上期货公司服务器,在机器人状态栏上显示时间,和未连接信息 Sleep(1000*10)

程序开头导入了一些需要使用的模块,包括 pymysql、_thread、json、math、http.server 和 urllib.parse。这里面有几个包,给大家稍微解释一下作用。_thread:这是 Python 的内置模块,用于支持多线程编程。它提供了一些函数,如 start_new_thread(),用于创建新的线程并执行指定的函数。http.server:内置模块,用于创建基于 HTTP 协议的服务器。它提供了一些类,用于处理 HTTP 请求和启动 HTTP 服务器。urllib.parse:这是 Python 的内置模块,用于解析 URL。它提供了一些函数,如 urlparse()parse_qs(),用于解析 URL 字符串,并提取其中的各个部分,如协议、主机、路径、查询参数等。

下面我们定义第一个函数。url2Dict这个函数接收一个 URL 字符串作为参数,并返回一个包含查询参数的字典。具体来说,它使用 urlparse 函数解析 URL,然后使用 parse_qs 函数将查询参数解析为字典。最后,它返回一个包含查询参数的字典。当我们向数据库发送请求url的时候,这个函数帮助我们进行解析。我们来看下url具体包含的参数包括品种名,交易所,价格精度,数量精度,bar周期(毫秒),深度档数,是否需要分笔数据,开始时间和结束时间。当然我们也可以根据数据库类型的区别,定义别的参数,例如在本例中,我们定义了数据库名称和具体的表名称,方便我们在数据库中获取到指定的数据。

接着我们定义一个名为 Provider 的类,它继承自 BaseHTTPRequestHandler 类,可以方便地创建自定义的 HTTP 服务器,而不需要从头开始编写处理 HTTP 请求的逻辑。在 do_GET 方法中,首先发送一个 HTTP 响应头,指定响应状态码为 200,200是HTTP 协议中一个常见的状态码,表示请求成功。然后设置响应内容的 MIME 类型为 application/json。然后解析 URL 中的查询参数,并将其转换为字典形式。然后,打印日志信息,包括 self.path 和 dictParam 的值,用于调试和记录请求的路径和查询参数。之后连接数据库,获取对于参数需求的数据。首先MySQL进行 数据库的连接,这里的参数包括主机地址,用户名,密码,和数据的名称,这里将数据库的名称定义为需求的参数名称databaseName,需求的数据库连接好以后,接下来执行一条 SELECT 查询语句,这里的参数名是我们的第二个参数,tableName,这样我们就可以从指定的数据库和表中获取我们需求的数据。

但是获取到的数据是我们需求的格式吗,我们需要进行一下处理。我们来看下回测系统需求的数据格式,具体需求的字段包括三个,schema是指定data数组里列的属性,data是具体数据的数值,还有detail,是商品期货的品种需要提供的属性,具体包括PriceTick,VolumeMultiple,ExchangeID,LongMarginRatio,ShortMarginRatio和InstrumentID。所以在代码里我们首先创建好需求的格式,其中 schema 键对应一个列表,表示数据的字段名,这是规定好的,包含时间戳,高开低收,和交易量数据;data 键对应一个列表,表示数据的具体内容;第三个detail键对应期货的属性信息,这里我们定义为纯碱的期货信息,PriceTick是1,合约乘数是10,交易所是郑商所,做多和做空保证金比率是12%,还有合约名称。接着我们进行data数据的填写。这样我们返回数据的格式和具体内容就填写完成了。

最后,将构造好的数据以 JSON 格式写入 HTTP 响应体,并发送给客户端。查询完成以后,进行游标和数据库连接的关闭。如果在整个过程中发生了异常,将捕获异常并打印错误日志。总之,这段代码实现了一个简单的自定义数据源服务,通过接收 HTTP GET 请求,连接到 MySQL 数据库,并将查询结果以 JSON 格式返回给请求方。

下面是createServer(host) 函数:这个函数接收一个主机地址作为参数,并创建一个 HTTP 服务器。它使用 HTTPServer 类创建服务器对象,并指定处理请求的类为 Provider。然后,它调用 serve_forever 方法启动服务器。这样我们的Http服务器设置已经完成了。下面我们需要在主函数中启动一个线程来创建 HTTP 服务器。这里使用 _thread.start_new_thread() 方法来创建一个新的线程,用于运行 createServer() 函数。createServer() 是我们定义的函数,用于启动 HTTP 服务器并监听指定的地址和端口。

在这里,通过传入一个包含地址和端口的元组参数 (("0.0.0.0", 9090), ),来指定 HTTP 服务器监听的地址和端口。其中,0.0.0.0 表示监听所有的网络接口,9090 是指定的端口号。在启动线程之前,使用 try...except 语句来捕获可能发生的异常。如果启动线程失败,则会打印错误信息,并抛出一个 Exception 异常,以终止程序的执行。最后,如果线程成功启动,则会打印一条日志信息,表示自定义数据源服务线程已经开启。使用这样多线程的方式来启动 HTTP 服务器,可以让程序在后台持续运行,并且不会阻塞主线程的执行。这样就可以实现同时处理多个客户端请求的功能。以上这些工作呢,就可以外部数据返回到需求的端口中。其实呢,这里http框架如果大家不太熟悉的话,可以了解这里的url参数设置和数据库参数的设置,其余的框架大家照搬就好。

实盘建立好以后,下面我们打开一个测试策略,进行回测。策略代码很简单,首先设置合约,打印合约的信息,然后GetRecords打印k线数据。在回测界面,更多参数这里设置指定数据源,这里的url是这样编写的,包含我们需要的两个参数,database和table。因为我们获取的k线周期是5分钟,所以这里k线的周期和底层k线的周期都设置为5分钟。还有回测时间的设置,需要是我们收集到的数据中包含的一段时间,否则会返回错误。这里的精度设置为0,否则会返回小数。

我们来验证一下,点击开始回测。可以看到回测系统利用获取到的k线进行画图的展示,对比默认的数据源可以发现是一致的。在实盘日志里,可以看到具体的数据提供流程。另外,最重要的就是使用自定义的数据源进行策略回测的功能,这里我们定义买入一手多仓,可以看到回测系统自动为我们进行了收益的统计和指标的展示。这样就可以让服务器上的实盘自己收集K线数据,而我们可以随时获取这些收集的数据直接在回测系统回测了。当然我们的课程属于抛砖引玉,各位大神还可以继续扩展,例如支持多品种、多市场数据收集等等功能,可以真正的搭建一个属于自己的量化数据库。

5.5 本地回测引擎

优宽量化平台为满足用户离线进行量化分析的需要,开发本地回测引擎,使用户能够在本地环境中进行回测,提高回测效率和灵活性,确保数据安全和隐私保护。本地回测引擎的功能和网页的回测系统基本上是一致的,除了不能满足实盘交易的功能,我们可以做一些有意思的工作,比如全品种历史数据的下载,可视化分析和策略的回测。本节课我们将学习从0开始搭建属于你自己的量化回测系统。

我们打开API文档,这里介绍了优宽量化交易平台开源了 JavaScript 语言和 Python 语言的本地回测引擎。我们点击 Python 语言的,看一下对应模块的介绍。打开页面,可以看到这里有详细的安装步骤和简单的例子。我们复制这行代码到终端进行回测引擎的安装。安装成功以后,我们复制这个实例到 Python 的编辑器里面。我们首先看下这个代码,在代码开头是回测参数的配置,这里面设置了策略起始和结束的时间,k线周期,还有交易所的设置,这里就相当于在网页端保存回测设置的结果,然后我们导入优宽量化回测引擎模块,接下来初始化回测引擎。这样我们就可以编写我们的策略了,第一步使用GetAccount打印了账户信息,第二步打印了ticker的信息,最后打印回测结果,并画图展示回测的结果。我们运行一下,看下返回结果。

python
'''backtest start: 2018-02-19 00:00:00 end: 2018-03-22 12:00:00 period: 15m exchanges: [{"eid":"Bitfinex","currency":"BTC_USD","balance":10000,"stocks":0}] ''' from fmz import * task = VCtx(__doc__) # initialize backtest engine from __doc__ print(exchange.GetAccount()) print(exchange.GetTicker()) print(task.Join(True)) # print backtest result task.Show() # or show backtest chart

可以看到,和网页的函数功能是一致的,首先返回了包括余额等的账户的信息,接下来返回的是ticker数据,包括时间戳,高开低收,买价和卖价,交易量和持仓量,接下来backtest result返回的是,策略运行对应的,每日收盘价,账户余额,持有仓位,手续费和净值。最后是回测图表的展示,包括定义的回测时间段内收益的变化等,用来动态展示策略的运行效果。

当然这里的展示还不够全面,我们将使用两个例子来展示优宽量化本地回测引擎的强大。第一个例子,我们用来获取某一期货品种的历史数据。数据的获取,是编写量化策略开始的第一步,相对于其他历史数据的获取方式,有的需要付费的账户,有的需要繁杂的api设置,还有的需要下载数据到本地,然后上传才能进行数据分析,使用优宽量化本地回测引擎。这些烦恼都可以解决。只需要开启回测引擎,就可以获取到和网页端一样的历史数据。

5.5.1 获取数据信息

我们来看下数据获取的具体方法。首先我们进行回测时间的设置,为了省去手工设置,填写参数的烦恼,我们可以到优宽量化的回测页面,选择2016年,然后到最近的时间,2023年11月1号,选择商品期货,点击添加。然后到策略编辑页面,点击保持回测设置,这样我们回测的参数就设置好了。我们复制到 Python 当中。

python
'''backtest start: 2020-01-01 09:00:00 end: 2023-11-02 15:00:00 period: 1d basePeriod: 1h exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] ''' task = VCtx(__doc__) # 初始化引擎 rlist = [] rlisttime = [] prebartime = 0 while True: try: exchange.SetContractType('FG888') r = exchange.GetRecords() if r[-1].Time != prebartime: for i in range(len(r) - 1): if r[i].Time not in rlisttime: rlist.append(r[i]) rlisttime.append(r[i].Time) prebartime = r[-1].Time else: continue except: print('数据读取完成') break

同样的,我们首先启动回测引擎。接下来我们获取k线数据,和网页上的回测系统一样,在策略运行的期间,使用GetRecords会持续不断的获取k线数据,我们想获得的k线数据是不包括重复时间戳的,所以设置三个变量,第一个 rlist 用来保持k线数据,第二个 rlisttime 用来保存k线的时间戳,第三个是 prebartime ,是前一根k线bar的时间戳,只有最新k线更新,我们进行k线的保存。

这里呢,我们不需要定义main函数了,使用while True循环,首先设置合约,当然需要设置一个在2016年已经上市的合约,这里我们设置玻璃的主力合约,然后使用GetRecords获取k线数据。接着判断如果k线的最新一根时间戳不等于prebartime的时候,我们使用for循环,到倒数第二根k线的位置,是已经完成的k线,接着判断如果rlisttime不包含该根k线时间戳的时候,使用append进行添加,同样的rlisttime也添加相应的时间戳,这里还需要重新赋值prebartime为最新的时间戳。这里我们使用try和except的框架包住我们的策略,在try中运行策略,如果运行完毕,在except中打印数据读取完成,直接break。这样,我们需求的数据就获取完成,我们看下获取到的数据,是每日的k线数据,包括高开低收,成交量和持仓量。

5.5.2 回测量化策略

接下来,我们来讲解第二个例子,使用本地的回测引擎,测试在本地运行量化策略。我们在策略广场中找到这个经典的MACD策略。复制到这里。这里我们需要做一些小小的改动。首先重新设置回测引擎。然后在while true里面,加上try和except的结构。最后我们运行这个main函数。稍微等待一下,等到返回策略运行完成,代表回测结束。

python
'''backtest start: 2019-01-01 00:00:00 end: 2021-01-01 00:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}] ''' task = VCtx(__doc__) # 初始化引擎 mp = 0 # 定义一个全局变量,用于控制虚拟持仓 # 程序主函数 def onTick(): _C(exchange.SetContractType, "rb000") # 订阅期货品种 bar = _C(exchange.GetRecords) # 获取K线数组 if len(bar) < 100: # 如果K线数组长度太小,就直接返回跳过 return macd = TA.MACD(bar, 5, 50, 15) # 计算MACD值 dif = macd[0][-2] # 获取DIF的值 dea = macd[1][-2] # 获取DEA的值 depth = exchange.GetDepth() ask = depth['Asks'][0]['Price'] bid = depth['Bids'][0]['Price'] global mp # 全局变量,用于控制虚拟持仓 if mp == 1 and dif < dea: Log('多平信号成立,挂单价格为:', bid) exchange.SetDirection("closebuy") # 设置交易方向和类型 id = exchange.Sell(bid, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and dif > dea: Log('空平信号成立,挂单价格为:', ask) exchange.SetDirection("closesell") # 设置交易方向和类型 id = exchange.Buy(ask, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and dif > dea: Log('多开信号成立,挂单价格为:', ask) exchange.SetDirection("buy") # 设置交易方向和类型 id = exchange.Buy(ask, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and dif < dea: Log('空开信号成立,挂单价格为:', bid) exchange.SetDirection("sell") # 设置交易方向和类型 id = exchange.Sell(bid, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单 def main(): while True: try: onTick() Sleep(1000) except: print('策略运行完成') break if __name__ == "__main__": main() print(task.Join(True)) # print backtest result task.Show() # or show backtest chart

怎样查看策略回测的效果呢,使用我们开头讲过的task.Join和task.Show,运行一下。可以看到策略的实时的资金变化,还有收益的曲线。这个策略确实不太理想哈,赔到最后只剩1600多元了,确实是一个真实的量化效果。我们经常可以看到,使用MACD指标一年翻几十倍的爆炸标题视频,现在呢,我们就可以使用这个策略使用优宽量化本地回测引擎,针对于我们关注的品种,真实的验证传闻策略的真假。

我们在网页界面回测一下,看看是否得出同样的回测效果。点击复制并进行在线回测,点击开始回测,但是这里的初始金额需要和本地的一致,改为10000元,可以看到回测的结果,最后的净值也是1600多元,证明本地回测的结果是可信的。当然这只是一个试验的策略,大家可以在本地继续优化它,直到做出来一个满意的策略。

第 6 章 CTA 之趋势跟踪策略

CTA 是一种多样性的投资方法,一般指商品期货和金融期货策略,它并不拘谨于是主观交易还是量化交易,只要其交易方法相对规则化、系统化都可以称为 CTA 策略。本章将结合不同的策略理论来开发 CTA 策略。

6.1 什么是 CTA 策略

CTA 的英文全称是 Commodity Trading Advisor,直译为商品交易顾问,通常指专业的资金管理人或机构。CTA 最初活跃于商品市场,随着金融市场的发展,其投资领域逐渐拓展到股票、国债、外汇等等。

6.1.1 CTA 策略的分类

CTA 策略的交易周期主要以分钟、小时和日线等数据为主,也有少部分使用 1 分钟以下周期的数据。CTA 策略有多种类型,以策略持仓周期可以分为中长线策略、短线策略、高频策略。以交易方法可以分为趋势策略和反转策略。在量化 CTA 策略实际交易中,这两类策略并不是独立运行,有时候会根据市场状况和各自的优劣进行策略组合,相对于单一类型的策略而言,这种优化组合和分散部署策略的方法,经常能取到更好的效果。注意:CTA 策略总体可以分为趋势策略和反转策略两大类。

6.1.2 趋势策略

用“守株待兔”这个词形容趋势策略再恰当不过了,这种策略的理念是顺势而为。大多时候不需要对未来价格进行预测,而是利用一些技术指标守在趋势发生的必经之路上,买入并持有直到趋势消失时退出。趋势策略的特点是经常亏小钱,但一次就赚个大的。资金曲线呈现脉冲状态,一般回撤较大,所以该策略对资产管理、风险控制,以及交易者的心理素质要求有比较高,通常是叠加多个交易品种,或利用策略多样性来降低风险。

6.1.3 反转策略

反转策略其实就是低买高卖赚取差价,它的交易方法与趋势策略相对应,在价格低时买入,在价格高时卖出。其特点是经常赚小钱,但一旦遇到趋势行情就赔个大的,资金曲线呈现阶梯状。由于市场大部分时间都在一个价格范围内上下波动,反转策略又非常适合这种行情,因此有很多人使用这种策略。反转策略既可以在两个相同品种的价差上套利交易,又可以利用价格网格进行低买高卖。

6.1.4 量化 CTA 策略

当然 CTA 策略绝不仅限于此,根据基本面、产业链调研、操盘经验等主观判断价格走势决定买卖也属于 CTA 策略。不过随着计算机科学的发展,涌现出很多量化 CTA 策略,主要是通过数据建模分析,发掘潜在交易机会。包括:自然语言处理、循环神经网络、随机森林模型等技术。但是总体来看,量化 CTA 中用的比较多的策略是趋势策略,相对于其他策略而言,量化趋势策略的优点是:
1、入门简单,做过交易的人大概都知道什么是技术指标,这些技术指标不需要太多的编程技巧就可以轻易转换为量化趋势策略。
2、无惧牛熊,尤其是在期货这种双向可以交易的市场,趋势策略在价格上涨或下跌的行情中都能获利,特别是在牛市熊市快速转换或趋势明显的时候。
3、由于每个交易者对风险承受力都不一样,随着行情加剧变化,亏损会使反向交易者出现非理性的踩踏式平仓,这一点非常有利于量化趋势策略。

6.2 经典 MACD 交易策略

前面的章节中我们学习了量化交易基础知识、Python编程语言基础语法以及优宽量化平台使用方法。虽然内容很枯燥,但这是你实现交易策略的必备知识。接下来几节课我们就用之前所学到的知识,趁热打铁从最简单的策略开始边学边用,一步一步帮助大家实现交易策略。

6.2.1 MACD 简介

相信做过交易的人对MACD都不陌生,这是一个非常古老的技术指标,它是由查拉尔·阿佩尔(Geral Appel)在上个世纪70年代发明的,全称指数平滑异同移动平均线。顾名思义这个指标是通过均线来对趋势进行判断。

如下图所示MACD主要由两条和中间的红绿柱组成。第一条线是DIF,反映的是一段时间内价格的变化情况。第二条线叫做DEA,它是DIF的均线,所以相对要平缓一些。而红绿柱叫做BAR柱,反映的是DIF与DEA两线之间的距离。注意:MACD是一种中长线的趋势指标,在市场反复震荡时,可能会出现错误信号。

image

6.2.2 MACD 原理

严格来讲MACD是均线的延伸,其意义与均线基本相同,只是它在计算时赋予了权重,时间越近赋予权重越大。同时它在图形上展示时非常直观,观察起来也一目了然。最重要的是其巧妙的利用短期指数移动平均线与长期指数移动平均线之间的聚合与分离状况,来判断市场状态。
具体来说,它是运用快速和慢速移动平均线之差,并加以双重平滑运算得来。这样不仅去除了一部分普通移动平均线频繁发出的假信号,而且还保留了移动平均线判断趋势行情的效果。因此MACD指标比均线更有趋势性和稳定性。具体来说就是:

  • 第1步:先对杂乱的K线均值处理,即EMA12和EMA26。EMA其实是另一种复杂均线,与普通均线不同的是,其价格权重以指数形式逐渐缩小,随着时间的增加,价格的权重越大,更能及时反映近期价格波动情况。
  • 第2步:为了解决信号滞后和频繁的无效信号问题,对两根均线差值处理,即DIF。均线差值可以灵活反映两根均线的相互关系,DIF上升往往意味着短期成本的涨速高于长期成本的涨速,市场短期内资金买入的意愿更强。
  • 第3步:重复第1步,对DIF均值处理,即DEA。
  • 第4步:重复第2步,对DIF、DEA差值处理,即MACD直方图(Histogram),也就是我们常说的红绿柱子。

6.2.3 MACD 计算方法

第 1 步:计算 EMA12 和 EMA26

  • EMA12 = XAverage(Close, 12)
  • EMA26 = XAverage(Close, 26)

第 2 步:计算 DIF

  • DIF = EMA12 - EMA26

第 3 步:计算 DEA

  • DEA = XAverage(DIF, 9)

第 4 步:计算 MACD 直方图(Histogram)

  • Histogram = DIF - DEA

6.2.4 MACD 使用方法

网上关于 MACD 的使用方法层出不穷,有利用 DIF 与 DEA 金叉死叉做趋势的,有结合价格看顶底背离做抄底的,也有辅助其他技术分析工具的等等,这些方法只在特定的时间有效。交易的关键不是用了哪个万能指标,而是制定一个正期望的交易策略,然后重复执行。其实对于大部分交易者来说,做趋势策略要好过做震荡策略,因为趋势策略的容错率更低。

传统的使用方法是看 DIF 与零线的位置关系,或者是 DIF 与 DEA 的交叉状态,来判断价格走势。一般来说 DIF 大于 0 表示上涨,小于 0 表示下跌;或者当 DIF 向上突破 DEA 时,形成买入信号,当 DIF 向下突破 DEA 时,形成卖出信号。但也有一部分人用红绿柱的高低,再结合价格走势判断 MACD 的顶背离和底背离,这是一种典型的反转交易方法。其理论是价格与 MACD 是趋于同向的,当价格上涨,MACD 也跟着上涨。如果价格逐步下跌,但 MACD 并没有跟着下跌,则逐步上涨,与价格走势背道而驰,形成底背离,这将预示着价格可能即将上涨。同理,如果价格逐步上涨,但 MACD 并没有跟着上涨,则逐步下跌,与价格走势形成顶背离,这将预示着价格可能会下跌。

6.2.5 MACD 的有效性

值得注意的是,上面的几种方法,虽然逻辑上能站得住脚,但在实际使用时也有反复打脸的时候。为什么有时候会不灵呢?这里面有个悖论,可以试着想一下,如果 MACD 一直有效,那么大家都会来用,那么大家的买卖点位相似的。比如 DIF 向上突破 DEA 时是买入信号,大家都在买,谁会卖出呢?

所以,最终的结果将会是,MACD 越有效,用的人也就越多,当越来越多人使用它的时候,它就会慢慢失效,直到大部分人都放弃使用它,它又重新变的有效。因为人才是金融市场的最终参与者,这既不像物理定律,也不是数学公式,这里面没有整齐划一的规律,这是人与人买卖博弈的最终结果。

注意:市场是人与人之间的博弈,也是交易策略与交易策略之间的博弈,没有恒久有效的策略,也没有恒久无效的策略。

6.2.6 MACD 策略逻辑

所以,交易界有句俗语:大道至简,重剑无锋。意思就是越简单的东西,越没人信,用的人也就越少,其结果反而越有效,普适性越强。比如:少吃多动至今仍然是减肥的成功秘诀,但真正做的人很少,因为大部分人不相信或者半信半疑没有坚持下来。那么今天我们就用最简单的方法构建一个 MACD 策略。策略逻辑如下:

  • 多头开仓:DIF 大于 DEA
  • 空头开仓:DIF 小于 DEA
  • 多头平仓:DIF 小于 DEA
  • 空头平仓:DIF 大于 DEA

6.2.7 MACD 策略编写

根据上面的策略逻辑,用 Python 实现交易策略。首先注册并登录 优宽量化 网站,依次点击控制中心 > 策略库 > 新建策略 > 点击右上角下拉菜单选择 Python 语言,开始编写策略,注意看下面代码中的注释。

第 1 步:编写策略框架
策略开发就像盖房子一样,先把地基和框架搭建好,再往里面填充东西。策略框架包含 main 函数和 onTick 函数,main 函数是策略的入口函数,程序先从 main 函数开始逐行执行代码。在 main 函数中是 while 无限循环,重复执行 onTick 函数。

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

第 2 步:定义虚拟持仓变量
虚拟持仓指的是理论持仓而非真实持仓,也就是在开平仓时假设订单完全成交。使用虚拟持仓的好处是编写简单,可以降低初学者编程门槛,快速迭代策略更新,一般用于回测环境中。

python
mp = 0 # 定义一个全局变量,用于控制虚拟持仓

虚拟持仓的原理很简单,策略运行之初默认是空仓 mp=0,当开多单后把虚拟持仓重置为 mp=1,当开空单后把虚拟持仓重置为 mp=-1,当平多单或空单后把虚拟持仓重置为 mp=0。这样我们在编写策略逻辑时,只需要判断 mp 的值就可以了。

第 3 步:计算 MACD
在优宽量化平台中内置了很多常用的指标函数,直接调用指标函数传入参数就可以计算结果,不需要再重新计算。MACD 计算过程是:订阅期货合约 >>> 获取 K 线数组 >>> 调用内置 TA.MACD 指标函数即可。

python
_C(exchange.SetContractType, "rb000") # 订阅期货品种 bar = _C(exchange.GetRecords) # 获取 K 线数组 if len(bar) < 100: # 如果 K 线数组长度太小就返回 return macd = TA.MACD(bar, 5, 50, 15) # 计算 MACD 值 dif = macd[0][-2] # 获取 DIF 的值,返回一个数组 dea = macd[1][-2] # 获取 DEA 的值,返回一个数组

注意:在调用内置指标函数之前,需要先判断 K 线数组的长度,因为指标的计算依赖足够的 K 线数据,所以使用 if 语句进行判断,如果 K 线数组长度太小,不足以计算指标,就直接返回。另外由于在计算指标时使用了收盘价数据,在 K 线还没有走完的时候,所计算的结果也会跟着来回变化,直接使用会造成信号闪烁。所以为了解决这个问题,折中的方法是在开平仓条件成立后,在下一根 K 线下单交易。

第 4 步:获取最新价
获取最新价的目的是下单交易,在下单函数 exchange.Buy()exchange.Sell() 中,需要有 2 个参数,第 1 个是下单价格,也就是说在开平仓时必须指定固定的价格,通过获取 K 线数组最后一个元素中的 'Close' 就可以获取最新的价格(卖一价)。

python
last_close = bar_arr[-1]['Close'] # 获取最新价格(卖价)

第 5 步:下单交易
在开平仓条件中,首先判断当前的持仓状态;然后再判断 DIF 与零轴的位置或 DIF 与 DEA 的交叉状态;接着如果条件成立,就设置交易方向和类型,即:开多、开空、平多、平空;最后使用 BuySell 函数下单,下单之后重置虚拟持仓的状态。

python
global mp # 全局变量,用于控制虚拟持仓 if mp == 1 and dif < dea: exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and dif > dea: exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and dif > dea: exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and dif < dea: exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单

6.2.8 完整策略代码

python
# 定义一个全局变量,用于控制虚拟持仓 mp = 0 # 程序主函数 def onTick(): global mp # 使用全局变量 mp # 订阅期货品种 exchange.SetContractType("rb888") # 获取 K 线数组 bar = exchange.GetRecords() # 如果 K 线数组长度太小就返回 if len(bar) < 100: return # 计算 MACD 值 macd = TA.MACD(bar, 5, 50, 15) # 获取 DIF 和 DEA 的值 dif = macd[0][-2] dea = macd[1][-2] # 获取最新价格(卖价) last_close = bar[-1]['Close'] # 根据 dif 和 dea 的关系进行交易操作 if mp == 1 and dif < dea: # 平多单 exchange.SetDirection("closebuy") exchange.Sell(last_close - 1, 1) mp = 0 elif mp == -1 and dif > dea: # 平空单 exchange.SetDirection("closesell") exchange.Buy(last_close, 1) mp = 0 elif mp == 0: if dif > dea: # 开多单 exchange.SetDirection("buy") exchange.Buy(last_close, 1) mp = 1 elif dif < dea: # 开空单 exchange.SetDirection("sell") exchange.Sell(last_close - 1, 1) mp = -1 # 主函数 def main(): while True: onTick() Sleep(1000)

通过本节的学习,相信你已经对 MACD 原理及计算方法有了一定的了解,你可以参照本节中的代码,试着把策略临摹下来进行测试,也可以对策略加以升级改进。

6.3 利用平均趋向指数辅助 MACD 策略

“趋势是你的朋友”这是每一个交易者都耳熟能详的箴言。但做过交易的朋友可能会有体会,趋势总是在毫无预警地开始并突然结束。那么在 CTA 策略中,如何抓住趋势并过滤震荡行情,是许多主观和量化交易者孜孜不倦的追求。在本节课程中,我们将以平均趋向指数(ADX)为滤网,分析在它量化交易中的应用。

6.3.1 什么是平均趋向指数

平均趋向指数是衡量趋势的技术工具,简称 ADX(average directional indicator),它是由韦尔斯·怀尔德在 1978 年提出。

注意:与其他技术分析工具不同的是,ADX 并不能判断多空方向,更不能提示精确的买卖点位,它只是衡量当前趋势的强弱。

ADX 的默认周期参数是 14,它的值是在 0~100 之间,数值越大说明上涨或下跌趋势越强力。通常当 ADX 的值大于 40 时,说明趋势强力,此时使用趋势交易才具有最大的回报潜力;当 ADX 的值小于 20 时,说明趋势疲软,并警告交易者不要使用趋势跟踪交易策略。

6.3.2 ADX 的计算方式

ADX 的计算方式比较复杂,它涉及到了:价格正向移动距离(+DM)、价格负向移动距离(-DM)、真是波动幅度(TR)、正向方向性指数(+DI),负向方向性指数(-DI)等很多中间变量:

计算动向变化:

  • up:今天的最高价 – 昨天的最高价
  • down:昨天的最低价 – 今天的最低价
  • +DM:如果 up 大于 max(down, 0),则+DM 等于 up,否则等于零
  • -DM:如果 down 大于 max(up, 0),则-DM 等于 down,否则等于零

计算真实波幅:

  • TR:max(今天最高价与今天最低价的差值,今天最高价与昨天收盘价差值的绝对值,今天最低价与昨天收盘价差值的绝对值)

计算动向指数:

  • +DI(14):+DM(14)/TR(14)*100
  • -DI(14):-DM(14)/TR(14)*100

计算 ADX:

  • DX:((+DI14) - (-DI14)) / ((+DI14) + (-DI14)) * 100
  • ADX:MA(DX, 14)

虽然 ADX 的计算比较复杂,但其逻辑还是比较清晰的:up 和 down 分别代表了价格正向和负向移动距离;+DI 和-DI 分别代表用波动率修正后上涨和下跌趋势。不管趋势是上涨还是下跌,只要存在明显的趋势行情,那么+DI 和-DI 中总有一个是较大的,因此 DX 的值会随着趋势的强弱指示在 0~100 之间;最后 ADX 则是 DX 的 14 天平均线。

当 +DI 高于 -DI 时,表明价格处于上升趋势。相反,当 -DI 高于 +DI 时,价格处于下降趋势。交易者可以通过检查同一时间点的 ADX 值来确定上升趋势或下降趋势的强度。

6.3.3 策略逻辑

在之前章节中,我们使用 MACD 指标创建了一个简单的策略,虽然该策略在趋势行情中表现还可以,但是在震荡行情常常入不敷出,甚至在长期的震荡行情中资金回撤比较大。为了降低策略在震荡时期的试错成本,因此我们将在本节中将之前的 MACD 策略加入 ADX 滤网,我们来看下效果到底如何?

原策略逻辑:

  • 多头开仓: DIF 大于 DEA
  • 空头开仓: DIF 小于 DEA
  • 多头平仓: DIF 小于 DEA
  • 空头平仓: DIF 大于 DEA

改进后的策略逻辑:

  • 多头开仓: DIF 大于 DEA,并且 ADX 上升
  • 空头开仓: DIF 小于 DEA,并且 ADX 上升
  • 多头平仓: DIF 小于 DEA,或者 ADX 下降
  • 空头平仓: DIF 大于 DEA,或者 ADX 下降

我们在原策略逻辑基础之上,对开仓和平仓分别加入 ADX 滤网,控制行情进入震荡时期的开仓次数。开仓时 ADX 必须是上升的,一旦 ADX 下降就平仓出局。
注意:ADX 的加入,使策略设计成严进宽出模式,控制震荡时期的回撤幅度。

6.3.4 策略编写

根据上面更改的策略逻辑,我们可以直接在原始策略基础上加入 ADX 滤网。虽然 ADX 计算复杂,但可以借助 talib 库,需要几行代码计算 ADX 值。计算 ADX 需要 talib 和 numpy.array,所以在代码开头导入 talib 库和 numpy 库。

python
import talib import numpy as np

使用 talib 计算 ADX,需要 4 个参数:最高价、最低价、收盘价、周期参数。写 get_data 函数,从 K 线数组提取最高价、最低价、收盘价。

python
# 把 K 线数组转换成最高价、最低价、收盘价数组 # 用于转换为 numpy.array 类型数据 def get_data(bars): arr = [[], [], []] for i in bars: arr[0].append(i['High']) arr[1].append(i['Low']) arr[2].append(i['Close']) return arr

使用 numpy 转换列表为 numpy.array,使用 talib 计算 ADX 值。

python
np_arr = np.array(get_data(bar)) # 把列表转换为 numpy.array 类型数据 adx_arr = talib.ADX(np_arr[0], np_arr[1], np_arr[2], 14) # 计算 ADX 的值

策略逻辑中,判断 ADX 大小和上升下降。提取某一天 ADX 值,判断上升下降需要倒数第二根和第三根 K 线 ADX 值。

python
adx1 = adx_arr[-2] # 倒数第二根 K 线的 ADX 值 adx2 = adx_arr[-3] # 倒数第三根 K 线的 ADX 值

最后修改下单逻辑:

python
if mp == 1 and (dif < dea or adx1 < adx2): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and (dif > dea or adx1 < adx2): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and dif > dea and adx1 > adx2: exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and dif < dea and adx1 > adx2: exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单

6.3.5 完整策略代码

python
# 导入库 import talib import numpy as np mp = 0 # 定义一个全局变量,用于控制虚拟持仓 # 把 K 线数组转换成最高价、最低价、收盘价数组 # 用于转换为 numpy.array 类型数据 def get_data(bars): arr = [[], [], []] for i in bars: arr[0].append(i['High']) arr[1].append(i['Low']) arr[2].append(i['Close']) return arr # 程序主函数 def onTick(): _C(exchange.SetContractType, "rb000") # 订阅期货品种 bar = _C(exchange.GetRecords) # 获取 K 线数组 if len(bar) < 100: # 如果 K 线数组长度太小就返回 return macd = TA.MACD(bar, 5, 50, 15) # 计算 MACD 值 dif = macd[0][-2] # 获取 DIF 的值,返回一个数组 dea = macd[1][-2] # 获取 DEA 的值,返回一个数组 np_arr = np.array(get_data(bar)) # 把列表转换为 numpy.array 类型数据 adx_arr = talib.ADX(np_arr[0], np_arr[1], np_arr[2], 14) # 计算 ADX 的值 adx1 = adx_arr[-2] # 倒数第二根 K 线的 ADX 值 adx2 = adx_arr[-3] # 倒数第三根 K 线的 ADX 值 last_close = bar[-1]['Close'] # 获取最新价格(卖价) global mp # 全局变量,用于控制虚拟持仓 if mp == 1 and (dif < dea or adx1 < adx2): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and (dif > dea or adx1 < adx2): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and dif > dea and adx1 > adx2: exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and dif < dea and adx1 > adx2: exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单 def main(): while True: onTick() Sleep(1000)

之前流行过一段话:站在风口猪都会飞,我们做交易也是一样。在大趋势面前,再笨的策略也能分一杯羹,所以我们要做的就是抓住大趋势并在震荡时期控制回撤。ADX 与 MACD 配合使用,可以帮助交易者确认差异,提高交易精度。

注意:MACD 是中长线趋势指标,在市场反复震荡时,可能出现错误信号。增加滤网的好处是,在震荡行情中降低风险,在趋势行情中增加盈利潜力,达到降低风险并最大化利润的目的。一句话:要想赚大钱,就一定不要与趋势为敌!

6.4 自适应动态双均线策略

对于初学者来说,策略开发最好从临摹开始。本节我们将重温经典技术分析工具考夫曼均线,并根据其常用的使用方法来构建策略,深度解析每一个计算步骤,以及如何用 Python 和 talib 库去实现它。

6.4.1 传统均线弊端

我们知道价格变化的速度本身就在变化,传统简单均线受困于固定周期参数,这使得不论市场的走势如何,短期均线灵敏度高,更贴近价格走势,但在市场震荡时期反复转向,造成频繁发出错误开平仓信号;长期均线在趋势判断上更加可靠,但在市场加速上涨或下跌时反应迟钝,造成错过最佳的买卖点。因此虽然传统简单均线可以在一定程度适应行情,但是却很难根据市场变化去进行调整,进而更好的把握趋势。特别在长期震荡行情中,不仅得不到正收益而且付出高额的交易成本,为了解决这个问题,我们引入考夫曼创立的自适应均线。

6.4.2 考夫曼均线原理

image

在《精明交易者》中,作者考夫曼(Kaufman)提出了“自适应移动平均线”,简称 AMA。该均线考虑到了市场价格变化速率,在普通均线的基础上增加了平滑系数,并自适应动态调整均线的灵敏度,可以在慢速趋势和快速趋势之间自我调整。当市场出现盘整、趋势不明显时期,AMA 倾向于慢速移动平均线。当市场波动较大,趋势明显,价格沿一个方向快速移动时,AMA 倾向于快速移动平均线。考夫曼均线本质上是根据一段时间内的价格波动率进行调整,计算出了合适的入场阈值提供了最佳的买卖点位。也就是说,它分为两部分主逻辑,第二部分逻辑在波动率层面做了又一次自适应。从而反应市场真实的趋势,便于快速抓住趋势性上涨和下跌的时机,同时规避市场来回震荡的影响。

6.4.3 考夫曼均线计算

有经验的交易者都习惯于在趋势展开的行情中使用快速均线,在震荡较多的行情中使用慢速均线。但如何把这个方法数量化,让程序来区分这两种行情?这里就需要引入“效率”的概念。如果价格一致朝一个方向运行,每天收盘价的变化贡献于总的运行幅度,那么就被称之为高效率;如果价格涨涨跌跌,很多次收盘价的变化相互抵消,那么就被称为低效率。这类似于物理学中的位移,如果价格在10天内上涨了100个点,我们可称为高效率,如果价格在10天内上涨了10个点,我们可以称为低效率。

第 1 步:计算价格效率
价格效率是建立在市场移动的速度和方向以及市场中噪声量的基础之上的,假设价格效率是在0~1之间,0表示市场没有移动,只有噪声;1表示市场只有移动,没有噪声。如果价格在10天内上涨了100个点,每天移动10个点,其价格效率就是:100 / (10 * 10) = 1;如果价格在10天内上涨了10个点,但每天震荡10个点,其价格效率就是:10 / (10 * 10) = 0.1。其计算公式是:首先计算价格变动值,即当根K线价格与前N根K线的价格差的绝对值;然后计算价格波动值,即N根K线内,所有价格变动绝对值的总和;最后计算效率系数,即价格变动值除以价格波动值。

  • 价格变动值 = abs(价格 - n 日前价格)
  • 价格波动值 = sum(abs(价格 - 上一个交易日价格), n)
  • 效率系数 = 价格变动值 / 价格波动值
    注意:在价格变动值一定条件下,市场波动越大,效率系数越小,此时使用慢速移动平均线更能把握整体趋势走向,因为慢速平均线不易被市场短期波动改变方向;反之,价格变动值一定条件下,市场波动越小,效率系数越大,此时应该使用快速(短期)移动平均线。

第 2 步:计算平滑系数
考夫曼用一系列的移动平均速度来描述平滑系数,其计算方式与EMA类似,根据价格所占权重,重新定义快速和慢速趋势速度系数,比如可以将2天的平均称为快速,30天的平均称为慢速。其中:

  • 快速趋势系数是:2 /(2 + 1) = 2 / 3 = 0.66667
  • 慢速趋势系数是:2 /(30 + 1) = 2 / 31 = 0.06452。它们的差值是:0.60215。
  • 快速趋势系数 = 2 / (n1 + 1)
  • 慢速趋势系数 = 2 / (n2 + 1)
    上面公式中的n1和n2是交易周期数,并且n1小于n2。默认n1为2,n2为30。最后利用效率比率计算平滑系数,也就是:效率系数 * 0.60215 + 0.06452。
  • 平滑系数 = 效率系数 * (快速 - 慢速) + 慢速

可见,当市场波动越大,趋势明显时,平滑系数更加趋向于选择快速趋势系数;反之,在市场震荡盘整,趋势不明显时期,平滑系数更趋向于选择慢速趋势系数。

第 3 步:计算 AMA 值
因为在效率系数太低时,可能会取消交易,所以卡夫曼建议在计算 AMA 值之前,对最后的平滑系数再次乘方。

  • 系数 = 平滑系数 * 平滑系数
  • AMA = 上一个交易日的 AMA + 系数 * (价格 - 上一个交易日的 AMA)

假设昨天的 AMA 值是 40,当前的价格是 47,它们之间有 7 个点的差值。那么在一个高效市场,其 AMA 值提高将近 3.1 个点,这几乎是差值的一半。在一个低效市场,这个差值几乎不会对 AMA 值产生影响。

6.4.4 策略逻辑

根据考夫曼的观点,AMA 相当于平滑指数,如果其方向改变就应该立刻交易。换句话说就是 AMA 上升时应该买进,AMA 下降时应该卖出。不过如果贸然以此做交易信号,可能造成大量的无效信号,因此就需要增加一个合适的滤网,即增加另一根 AMA 均线,以双均线交叉的形式发出买卖信号。

  • 多头开仓:AMA1 和 AMA2 均为向上,并且 AMA1 大于 AMA2。
  • 空头开仓:AMA1 和 AMA2 均为向下,并且 AMA1 小于 AMA2。
  • 多头平仓:AMA1 和 AMA2 均为向下,或者 AMA1 小于 AMA2。
  • 空头平仓:AMA1 和 AMA2 均为向上,或者 AMA1 大于 AMA2。

6.4.5 策略编写

按照以上策略逻辑,开始用代码实现出来。依次打开:控制中心>策略库>新建策略>点击右上角下拉菜单选择 Python 语言,开始编写策略,注意看下面代码中的注释。

第 1 步:抱着不重复造轮子的精神,我们在计算 AMA 的值时,直接使用之前介绍过的 talib 库。因为在使用 talib 计算 AMA 的时候需要用到 numpy.array 数据,所以这里也要导入 numpy 库。

python
# 导入库 import talib import numpy as np

第 2 步:编写策略框架,这个在之前的章节已经学习过,一个是 onTick 函数,另一个是 main 函数,其中在 main 函数中无限循环执行 onTick 函数,如下:

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

第 3 步:定义虚拟持仓变量。

python
mp = 0 # 定义一个全局变量,用于控制虚拟持仓

第 4 步:计算 AMA,因为我们是使用 talib 计算 AMA 的值,所以需要用到收盘价的 numpy.array 数据,那么其流程是:订阅期货数据>>>获取 K 线数组>>>把 K 线数组转换为收盘价数组>>>把收盘价数组转换为 numpy.array 数据>>>使用 talib 计算 AMA 值。

python
# 把 K 线数组转换成收盘价数组,用于计算 AMA 的值 def get_close(r): arr = [] for i in r: arr.append(i['Close']) return arr _C(exchange.SetContractType, "rb000") # 订阅期货品种 bar_arr = _C(exchange.GetRecords) # 获取 K 线数组 if len(bar_arr) < 100: # 如果 K 线数组长度太小就返回 return close_arr = get_close(bar_arr) # 把 K 线数组转换成收盘价数组 np_close_arr = np.array(close_arr) # 把列表转换为 numpy.array ama1 = talib.KAMA(np_close_arr, 10).tolist() # 计算短期 AMA ama2 = talib.KAMA(np_close_arr, 100).tolist() # 计算长期 AMA

请看上面代码所示,第 1~6 行是 get_close 函数,这个函数的作用是把 K 线数组转换成收盘价数组,主要用于计算 AMA。第 8 行~第 15 行是按照流程计算 AMA 值。注意:计算 AMA 需要一个周期参数,如果 K 线长度小于这个周期参数,就不能计算其 AMA 值。所以在第 10 行和第 11 行,我们加了一个判断 K 线数组的长度,也就是说如果 K 线数据不足以计算 AMA 值时直接跳过。

第 5 步:开平仓,首先获取当前最新价格,因为在使用下单接口函数时,必须指定交易价格。K 线数组最后一个数据的收盘价就是最新价格。然后指定交易的方向类型,即:开多、开空、平多、平空。调用 exchange.SetDirection()函数,分别传入:"buy"、"sell"、"closebuy"、"closesell"。最后下单之后重置持仓状态 mp 的值。

python
last_close = close_arr[-1] # 获取最新价格 global mp # 全局变量,用于控制虚拟持仓 if mp == 1 and is_cross(ama2, ama1): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and is_cross(ama1, ama2): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and is_cross(ama1, ama2): exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and is_cross(ama2, ama1): exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单

6.4.6 完整策略代码

python
# 导入库 import talib import numpy as np mp = 0 # 定义一个全局变量,用于控制虚拟持仓 # 把 K 线数组转换成收盘价数组,用于计算 AMA 的值 def get_close(r): arr = [] for i in r: arr.append(i['Close']) return arr # 判断两根 AMA 交叉 def is_cross(arr1, arr2): if arr1[-2] < arr2[-2] and arr1[-1] > arr2[-1]: return True # 程序主函数 def onTick(): _C(exchange.SetContractType, "rb000") # 订阅期货品种 bar_arr = _C(exchange.GetRecords) # 获取 K 线数组 if len(bar_arr) < 100: # 如果 K 线数组长度过小就直接返回 return close_arr = get_close(bar_arr) # 把 K 线数组转换成收盘价数组 np_close_arr = np.array(close_arr) # 把列表转换为 numpy.array ama1 = talib.KAMA(np_close_arr, 10).tolist() # 计算短期 AMA ama2 = talib.KAMA(np_close_arr, 100).tolist() # 计算长期 AMA last_close = close_arr[-1] # 获取最新价格 global mp # 全局变量,用于控制虚拟持仓 if mp == 1 and is_cross(ama2, ama1): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and is_cross(ama1, ama2): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and is_cross(ama1, ama2): exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(last_close, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and is_cross(ama2, ama1): exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(last_close - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单 def main(): while True: onTick() Sleep(1000)

上面的代码构建了自适应双均线,并从细节上逐行阐述其中的原理和算法,最后又以双自适应均线创建一个简单的 CTA 策略。整体来看自适应双均线比普通均线更加稳定又不失灵活性。

注意:AMA 本意上是用来替代普通均线,以更好地拟合市场价格走势,单独一根 AMA 并没有质的提升,所以需要额外配置一个过滤器,这个过滤器的选择基于市场波动状况来选择。

6.5 日内高低点突破策略

之前听过一句话:要想赚大钱必须学会长线持仓,但如果要赚快钱就要学会日内交易。如今的量化交易范围之广令人惊叹,各种交易策略层出不穷,其中最为流行的就是日内交易策略。
日内交易是一种快进快出的交易方式,由于可以控制隔夜风险的特点,得到了很多交易者的推崇和接受。为了帮助大家了解日内交易,丰富策略仓库,本节我们将深入了解商品期货中最为流行的日内策略之一日内高低点突破策略。

6.5.1 什么是日内交易

日内交易的目的是以更小的损失,来获取当天市场微小的价格波动所带来的利润。它是指开仓和平仓在同一天内或同一交易时间段内完成的交易方式,开仓和平仓可以是单次,也可以是多次,只要是开平仓在同一个交易日前结束就行。
理论上日内交易不承担隔夜的跳空风险,相对来说是一种较完美的低风险交易策略,但实际上并非如此,虽然日内交易回避了跳空所带来的风险,同时也错失了跳空所带来的利润。但如果以正确的方式交易,通过配合不同的交易规则,日内交易往往也能产生丰厚的回报。

6.5.2 策略逻辑

我们知道判断上涨趋势最简单的方法是,当前低点比前一个低点更高,当前高点也比前一个高点更高;同理下跌趋势最简单的方法是,当前低点比前一个低点更低,当前高点也比前一个高点更低。但如果仅仅以高低点的比较去判断趋势的涨跌,这未免太过简陋,因为价格可能在一个点上来回跳动几十次甚至上百次,从而导致交易过于频繁。
所以我们需要设定一个价格区间来过滤这些日常杂波,来对简单的高低点突破策略进行完善。我们可以根据历史行情所出现的最高价和最低价,组成一个包含上轨和下轨的通道。根据顺势交易的原则,当价格突破上轨时多头开仓,当价格突破下轨时空头开仓。

  • 多头开仓:当前无持仓,时间是在开盘与收盘前 10 分钟之间,并且价格大于上轨
  • 空头开仓:当前无持仓,时间是在开盘与收盘前 10 分钟之间,并且价格小于下轨
  • 多头平仓:当前持多单,价格小于下轨,或者时间大于 14:50
  • 空头平仓:当前持空单,价格大于上轨,或者时间大于 14:50
    有人统计过,大部分的窄幅止损都是无效的,小空间的止损会频繁打脸,所以我们要做的就是设计一个宽幅止损:
  • 如果多头开仓后,价格不升反跌,我们所要做的不是立即止损,而是等待观望,直到价格跌破下轨才止损出局;
  • 空头开仓后也是如此,当价格不跌反升,继续等待价格是否会自我修正,直到跌破上轨才止损出局。

6.5.3 策略编写

第 1 步:导入 time 库

python
import time

因为日内策略在编写的时候,要判断当前的时间来控制开平仓逻辑,这个策略在设计的时候是:只能在 9 点 30 分至 14 点 50 分之间开仓,14 点 50 分之后全部平仓,其余的时间都过滤掉了。所以就需要引入 time 时间库。

第 2 步:编写策略框架

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

第 3 步:设置全局变量

python
mp = on_line = under_line = 0

在全局变量中,mp 主要用于控制虚拟持仓,on_line 和 under_line 分别记录上轨和下轨。

第 4 步:处理时间

python
def can_time(hour, minute): hour = str(hour) minute = str(minute) if len(minute) == 1: minute = "0" + minute return int(hour + minute) _C(exchange.SetContractType, "AP888") # 订阅期货品种 bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日级别K线数组 if len(bar_arr) < 10: return minute_arr = _C(exchange.GetRecords, PERIOD_M1) # 获取分钟级别K线数组 time_new = minute_arr[-1]['Time'] # 获取当根K线的时间戳 time_local_new = time.localtime(time_new / 1000) # 处理时间戳 hour_new = int(time.strftime("%H", time_local_new)) # 格式化时间戳,并获取小时 minute_new = int(time.strftime("%M", time_local_new)) # 格式化时间戳,并获取分钟 day_new = int(time.strftime("%d", time_local_new)) # 格式化时间戳,并获取日期 time_previous = bar_arr[-2]['Time'] # 获取上根K线的时间戳 previous = time.localtime(time_previous / 1000) # 处理时间戳 day_previous = int(time.strftime("%d", previous)) # 格式化时间戳,并获取日期

处理时间一共用于两个地方:一个是判断当前时间是否在我们规定的交易时间内,如果当前是在这个时间之内,并且已经达到了开仓条件就开仓,如果不是在这个时间之内,并且当前有持仓就平掉所有持仓,达到收盘前平仓的目的。

另一个是判断当前 K 线是不是最新交易日的 K 线,因为我们的策略逻辑是每当新的一天 K 线出现时,就重置上下轨。通过对比两个 K 线的时间戳来重置 on_line 和 under_line 的值,也就是说上下轨通道是在不断变化的。

所以这里获取两个不同时间级别的K线,PERIOD_D1代表日级别的,在同一个交易日内,K线 Time 属性不会改动;PERIOD_M1代表分钟级别的,K线 Time 属性会随分钟级别进行变化。

第 5 步:计算高低点上下轨

python
global mp, on_line, under_line # 引入全局变量 high = bar_arr[-2]['High'] # 获取上根 K 线的最高价 low = bar_arr[-2]['Low'] # 获取上根 K 线的最低价 if day_new != day_previous: # 如果是最新一根 K 线 on_line = high * up # 重置上轨 under_line = low * down # 重置下轨 can_trade = can_time(hour_new, minute_new) if can_trade < 930: # 如果不是在规定交易的时间内 if high > on_line: # 如果上根 K 线最高价大于上轨 on_line = high * up # 重置上轨 if low < under_line: # 如果上根 K 线最低价小于下轨 under_line = low * down # 重置上轨 if on_line - under_line < 10: # 如果上轨与下轨的差小于 10 return

计算高低点上下轨的逻辑其实非常简单:如果当前是第一根 K 线,那么 on_line 和 under_line 的值分别是最高价和最低价,如果当前 K 线是最新交易日的 K 线,就重置 on_line 和 under_line 的值为最高价和最低价;一旦在规定的交易时间内,on_line 和 under_line 的值就固定不变了,除非在这个时间之外并且如果上根 K 线最高价大于 on_line 就重置为最新的最高价;如果上根 K 线最低价小于 under_line 就重置为最新的最低价。

第 6 步:下单交易
在下单交易之前,我们先获取当前最新价格,因为在下单时需要在函数中传入下单价价格。然后使用 if 语句,根据之前设计的交易逻辑,先是判断当前的持仓状态,然后再判断当前时间状态,以及最新价格与上下轨的相互位置关系,最后下单交易并重置虚拟持仓状态。

python
close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价),用于开平仓 # 如果持多单,并且价格小于下轨或者非规定的交易时间 if mp > 0 and (close_new < under_line or can_trade > 1450): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 # 如果持空单,并且价格大于上轨或者非规定的交易时间 if mp < 0 and (close_new > on_line or can_trade > 1450): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and 930 < can_trade < 1450: # 如果当前无持仓且在交易时间内 if close_new > on_line: # 如果价格大于上轨 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 elif close_new < under_line: # 如果价格小于下轨 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单

预测今天下午的天气是很容易的,但是要想预测这个月内的天气却很难。日内交易不需要较长的持仓周期,所承受的市场波动风险较低,尽管这种交易方式不符合每个人的风格,但对于那些风险较为敏感的交易者来说,日内交易还是相当值得深入研究。

6.5.4 完整策略代码

python
# 导入库 import time # 定义全局变量:虚拟持仓、上轨、下轨 mp = on_line = under_line = 0 # 处理时间函数 def can_time(hour, minute): hour = str(hour) minute = str(minute) if len(minute) == 1: minute = "0" + minute return int(hour + minute) def onTick(): _C(exchange.SetContractType, "AP888") # 订阅期货品种 bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日级别K线数组 if len(bar_arr) < 10: return minute_arr = _C(exchange.GetRecords, PERIOD_M1) # 获取分钟级别K线数组 time_new = minute_arr[-1]['Time'] # 获取当根K线的时间戳 time_local_new = time.localtime(time_new / 1000) # 处理时间戳 hour_new = int(time.strftime("%H", time_local_new)) # 格式化时间戳,并获取小时 minute_new = int(time.strftime("%M", time_local_new)) # 格式化时间戳,并获取分钟 day_new = int(time.strftime("%d", time_local_new)) # 格式化时间戳,并获取日期 time_previous = bar_arr[-2]['Time'] # 获取上根K线的时间戳 previous = time.localtime(time_previous / 1000) # 处理时间戳 day_previous = int(time.strftime("%d", previous)) # 格式化时间戳,并获取日期 global mp, on_line, under_line # 引入全局变量 high = bar_arr[-2]['High'] # 获取上根 K 线的最高价 low = bar_arr[-2]['Low'] # 获取上根 K 线的最低价 if day_new != day_previous: # 如果是最新一根 K 线 on_line = high * up # 重置上轨 under_line = low * down # 重置下轨 can_trade = can_time(hour_new, minute_new) if can_trade < 930: # 如果不是在规定交易的时间内 if high > on_line: # 如果上根 K 线最高价大于上轨 on_line = high * up # 重置上轨 if low < under_line: # 如果上根 K 线最低价小于下轨 under_line = low * down # 重置上轨 if on_line - under_line < 10: # 如果上轨与下轨的差小于 10 return close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价),用于开平仓 # 如果持多单,并且价格小于下轨或者非规定的交易时间 if mp > 0 and (close_new < under_line or can_trade > 1450): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 # 如果持空单,并且价格大于上轨或者非规定的交易时间 if mp < 0 and (close_new > on_line or can_trade > 1450): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == 0 and 930 < can_trade < 1450: # 如果当前无持仓且在交易时间内 if close_new > on_line: # 如果价格大于上轨 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 elif close_new < under_line: # 如果价格小于下轨 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单 def main(): while True: onTick() Sleep(1000)

6.6 增强版唐奇安通道策略

唐奇安通道(Donchian Channel)是一种技术分析工具,由理查德·唐奇安(Richard Donchian)在20世纪50年代发明。这种策略基于一个简单的概念:在一个上升趋势中,价格通常会在最近一段时间的最高价附近交易;而在下降趋势中,价格往往会接近最近一段时间的最低价。唐奇安通道通过计算一定时间范围内的最高价和最低价,来展示当前市场的趋势强度和潜在的支撑/阻力区域。

image

6.6.1 唐奇安通道简介

原始的唐奇安通道(Donchianchannel)规则其实很简单,它先设置一条阻力线和一条支撑线,阻力线由过去N天的最高价的最大值形成;支撑线由过去N天的最低价的最小值形成。
唐奇安上阻力线:由过去N天的当日最高价的最大值
唐奇安下支撑线:由过去N天的当日最低价的最小值

注意:图中唐奇安通道阻力线和支撑线,在外观上与布林带比较相像,只不过布林带的波动比较灵敏,而唐奇安通道则是直上直下。唐奇安通道可以衡量市场的波动性,一般来说通道宽度越宽,市场的波动就越大,通道宽度越窄,市场的波动性也就越小。

6.6.2 原始策略逻辑

除了具有衡量市场波动率这个功能外,它的主要作用是帮助交易者确定买入和卖出时机。因为唐奇安通道是根据最高价和最低价计算出来的,通道的宽窄又随着价格的变化自动调整,所以大多数时候价格是在通道之内运行,很少突破其上下轨道的。也就是说,价格并不会随意突破阻力线和支撑线,但如果有效突破,那就预示着大行情可能将会出现。此时交易者可以根据支撑和阻力线,确定买进或卖出的具体时机。比如:当价格向上突破阻力线就买入,当价格跌破支撑线就卖出。

之所以原始策略逻辑在早期的金融市场大行其道,是因为最初的市场和市场参与者不太成熟。现如今散户都已经用上了量化交易,策略的同质性,导致策略低效,也就是说如果一个策略使用的人越多,在市场上的效率就越低。所以我们有必要对原始策略逻辑加以改进,让策略更加与众不同。

6.6.3 改进后的策略逻辑

我们分别从优化开仓方式和止盈止损这两个方面加以改进。首先是开仓方式,做过突破策略的交易者可能会有体会,行情突破阻力线,本来我们是要做多的,结果刚一入场,价格却急转直下,本来看着是一个很好的机会,最后弄了一个措手不及。

大家想一想假突破究竟是怎么来的,怎么总是那么巧合的发生,就好像庄家顶着自己的账号操纵市场一样。其实这是策略同质化的原因,因为前期高低点是固定的,大家都有目共睹,结果大家都等着价格向上突破时买进,该买的都已经买了,此时买力消失,价格自然而然下跌。另外大户也在盯着这个关键点,他也知道散户会在突破时买进,等散户买完不就可以做空割韭菜了么。

所以为了解决这个问题,我们在支撑线和阻力线分别增加一个系数,这样避免与大多数策略参数一致,造成的同质化现象,导致策略低效。另外我们知道,中国的期货市场总是涨的时候涨的缓,跌的时候跌的急,那么可以对支撑线和阻力线设置不同的系数,让策略更合理的适应当前市场环境。
唐奇安上轨:由过去N天的最高价的最大值上涨系数
唐奇安下轨:由过去N天的最低价的最小值
下跌系数
唐奇安中轨:(唐奇安上轨 + 唐奇安下轨) / 2

然后是改进止盈止损的方式,原始的唐奇安通道规则是,价格突破阻力线开多单,把止盈止损放在支撑线这个位置;价格跌破支撑线开空单,把止盈止损放在阻力线这个位置。但是这里面有一个问题,假如市场波动率比较大,唐奇安通道上轨与下轨的距离就会加宽,此时就会增加止损的成本和损失一部分浮盈。

那么折中的办法是,可以根据唐奇安通道的上轨和下轨,再计算出一条中轨,这样把止盈止损放在中轨的位置,无论是持有多单还是空单,只要价格反向突破中轨及时止盈止损,这样不仅可以减少止损时所付出的成本,同时保护你未平仓的利润免受重大不利价格波动的影响。
开多:如果当前无持仓,并且价格突破唐奇安上轨
开空:如果当前无持仓,并且价格跌破唐奇安下轨
平多:如果当前持多单,并且价格跌破唐奇安中轨
平空:如果当前持空单,并且价格突破唐奇安中轨

6.6.4 策略编写

到目前为止,你应该很好地理解了原始唐奇安通道规则,以及我们将要改进它的方法。现在我们就用代码编写这个交易策略吧。

第 1 步:编写策略框架
策略框架其实就是两个函数,其中 main 函数是整个程序的入口函数,也就是说策略开始执行的时候,会先执行 main 函数;另外一个是 onTick 函数,onTick 只是一个函数的名字,当然你也可以自由命名,onTick 函数里面主要编写策略逻辑。整个框架其实就是在 main 函数中重复执行 onTick 函数。

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

第 2 步:定义全局变量
我们这个策略只需要一个控制虚拟持仓的全局变量,所谓的虚拟持仓指的是理论持仓而非真实持仓,无论开仓还是平仓,我们都假设订单已经完全成交。这么做的目的是简化初学者的入门门槛。

python
# 定义全局变量 mp = 0 # 用于控制虚拟持仓

第 3 步:处理 K 线数据
我们在前面已经定义过,上轨是过去 N 天的最高价的最大值,下轨是过去 N 天的最低价的最小值。要想计算这两个值,首先要先获取基础 K 线数据。但是在使用 GetRecords 方法获取完基础 K 线数据之后,先不要慌着计算上轨和下轨,而是先把数据处理一下。

这里有一点需要注意一下,因为我们在计算上轨和下轨的时候需要 N 个 K 线,如果 K 线数量太少就不能计算了,所以要加一个 if 条件,判断当前 K 线是否满足我们所需要的数量,如果不满足就直接返回,等待下一次循环。另外我们还需要从 K 线数组中提取当前最新价格和上根 K 线的收盘价,最新价格主要用于开平仓,上根 K 线收盘价主要用于判断开平仓信号。有的朋友可能会问,为什么不直接使用最新的价格来判断开平仓信号呢?这是因为如果使用最新价格来判断,就可能出现信号反复的问题,同时也为了规避未来函数和偷价这些常见的量化交易问题,所以我们的策略在设计上是:当前 K 线出信号,下根 K 线发单。

python
_C(exchange.SetContractType, "rb000") # 订阅期货品种 bar_arr = _C(exchange.GetRecords) # 获取 K 线数组 if len(bar_arr) < 60: return close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价) close_last = bar_arr[-2]['Close'] # 上根 K 线收盘价 bar_arr.pop() # 删除数组最后一个数据

第 4 步:计算上轨、下轨、中轨
在优宽量化交易平台中,已经内置了 talib 库中的 Highest 函数和 Lowest 函数,所以我们直接调用这两个函数就可以计算上轨和下轨的值。但因为我们是使用上根 K 线收盘价为基准,来判断它与上轨、下轨、中轨的位置关系来开平仓,所以在计算上轨和下轨之前需要先删除 K 线数组中的最后一个元素。

python
bar_arr.pop() # 删除数组最后一个数据 on_line = TA.Highest(bar_arr, 55, 'High') * 0.999 # 计算唐奇安上轨 under_line = TA.Lowest(bar_arr, 55, 'Low') * 1.001 # 计算唐奇安下轨 middle_line = (on_line + under_line) / 2 # 计算唐奇安中轨

第 5 步:下单交易
要想在函数内使用外部的全局变量,需要在使用这个变量之前,先用 global 关键字把变量引入。注意下面代码中的注释,整个代码流程是使用 if 语句,然后根据我们之前定义的策略逻辑来编写。有两个地方需要注意,一个是在下单之前需要先设置下单的类型方向,也就是先调用 SetDirection 函数。另一个是在下单之后,要把虚拟持仓变量 mp 重新赋值。

python
global mp # 引入全局变量 # 如果持多单, 并且价格小于下轨 if mp > 0 and close_last < middle_line: exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值, 即空仓 # 如果持空单, 并且价格大于上轨 if mp < 0 and close_last > middle_line: exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 平空单 mp = 0 # 设置虚拟持仓的值, 即空仓 if mp == 0: # 如果当前无持仓 if close_last > on_line: # 如果价格大于上轨 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 开多单 mp = 1 # 设置虚拟持仓的值, 即有多单 elif close_last < under_line: # 如果价格小于下轨 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值, 即有空单

6.6.5 完整策略代码

python
mp = 0 # 定义全局变量,用于控制虚拟持仓 def onTick(): _C(exchange.SetContractType, "rb000") # 订阅期货品种 bar_arr = _C(exchange.GetRecords) # 获取 K 线数组 if len(bar_arr) < 60: return close_new = bar_arr[-1]['Close'] # 获取最新价格(卖价) close_last = bar_arr[-2]['Close'] # 上根 K 线收盘价 bar_arr.pop() # 删除数组最后一个数据 on_line = TA.Highest(bar_arr, 55, 'High') * 0.999 # 计算唐奇安上轨 under_line = TA.Lowest(bar_arr, 55, 'Low') * 1.001 # 计算唐奇安下轨 middle_line = (on_line + under_line) / 2 # 计算唐奇安中轨 global mp # 引入全局变量 if mp > 0 and close_last < middle_line: # 如果持多单, 并且价格小于下轨 exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值, 即空仓 if mp < 0 and close_last > middle_line: # 如果持空单, 并且价格大于上轨 exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 平空单 mp = 0 # 设置虚拟持仓的值, 即空仓 if mp == 0: # 如果当前无持仓 if close_last > on_line: # 如果价格大于上轨 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(close_new, 1) # 开多单 mp = 1 # 设置虚拟持仓的值, 即有多单 elif close_last < under_line: # 如果价格小于下轨 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(close_new - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值, 即有空单 # 程序入口 def main(): while True: # 进入无线循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

唐奇安通道之所以流传至今,一定有它独特的道理。但随着市场的转变,我们也要与时俱进而不是贸然使用。随着你对交易认知的提升,你会发现改进的方法非常多。这就是交易的魅力所在,每一位交易者都应该是一位探险者,大胆探索小心求证,长此以往就一定能有适合自己的交易方法。最后配合合理的风险管理,方能成为一位成功的交易者。

本节我们以唐奇安通道为例,讲解了通道类策略的基本原理,其实还有类似的通道策略,比如布林带策略,Dual Thrust 策略等,大家可以以此为依据,了解通道策略具体的使用方法。

6.7 菲阿里四价策略

在期货市场,价格呈现一切。几乎所有的技术分析,如均线、布林线、MACD、KDJ等等,这些都是以价格为基础,通过特定的方法计算。包括基本面分析也是如此,通过分析近期和远期价差、期货和现货升贴水、上下游库存等等数据,计算当前价格是否合理,并预估未来的价格。既然如此,为什么不直接研究价格呢?今天我们讲的菲阿里四价策略就是完全根据价格来做出卖决定。

6.7.1 菲阿里简介

菲阿里是一位日本的交易者,主要偏向于商品期货日内主观交易。其大名远扬是在2001年的罗宾斯(ROBBINS-TAICOM)期货冠军大赛中,以1098%的成绩获得冠军,并且在之后的两年里再以709%、1131%的成绩夺冠。从成绩就知道,菲阿里是一个非常优秀的交易者。幸运的是,菲阿里在《1000%的男人》这本书中详尽叙述了他的交易方法,菲阿里四价策略正是后人总结他的交易方法。虽然只是从外在形式加上自己的理解模仿了一部分,并不代表菲阿里全部交易精髓,但至少可以帮助我们在构建策略时拓展思路。

6.7.2 策略逻辑

菲阿里四价策略是一种比较简单的趋势性日内交易策略,四价分别是指:昨天高点、昨天低点、昨日收盘价、今天开盘价。从书中的交易笔记来看,菲阿里不使用任何分析工具,而是大量应用阻溢线的概念,也就是通常我们所说的阻力线和支撑线。

  • 阻力线 = 昨日最高价
  • 支撑线 = 昨日最低价

注意:对于阻力线和支撑线的定义,他使用的是昨日最高价和昨日最低价,可以视为昨天价格的波动范围,这也意味着多头或空头只有足够的力量时,才会有效突破阻力线和支撑线。并且一旦有突破这个波动范围,则说明价格背后的动能较大,后续走势可能会沿最小阻力线运动的概率较大。

  • 多头开仓:价格突破阻力线
  • 空头开仓:价格突破支撑线

如果开盘价处于阻力线和支撑线之间,当价格向上突破阻力线就建立多头,当价格向下突破支撑线就建立空头。如果一切顺利,则一直持仓到收盘。这样做的好处是,符合了充分非必要条件,即突破不一定上涨/下跌,但上涨/下跌一定会突破,也就是始终守在行情发生的必经之路伺机而动,因为较大行情的上涨和下跌一旦出现,势必要突破阻力线和支撑线的。

当然这也是出错率最高的方法,因为很多时候价格只是暂时性的突破了关键位置,如果贸然开仓可能会面临价格随时反向运动的风险。这时就需要设置一些过滤条件,限制假突破造成的来回开平仓问题。另外在交易周期上也尽量避免波动过于混乱的 5 分钟周期以下 K 线。

但是开仓后,盈利了还好,如果遇到亏损,总不能从小亏损一直积累到大亏损,才在收盘时平仓吧,这样显然不合理。所以我们对于平仓有两种处理方式:收盘平仓和止损平仓。如果 K 线上破高点或下破低点后又回到原来的区间内,就要考虑止损了。

  • 多头平仓:收盘前 5 分钟或达到多头止损线
  • 空头平仓:收盘前 5 分钟或达到空头止损线

实际上菲阿里在主观交易中,还有很多交易方法,包括:开盘后先涨后跌,跌破开盘价做空,止损设在之前上涨的最高点;开盘后先跌后涨,突破开盘价做多,止损设在之前下跌的最低点。动手能力强的朋友可以在自己的策略中增改。

到这里你会发现,对于一天的价格走势来说,收盘价相对于开盘价的涨跌,其概率接近于 50%。菲阿里的交易方法在胜率上就利于不败之地,再加上行情顺利的时候一直持仓到收盘,在行情不符合自己的预期时及时止损。形成了截断亏损,让利润奔跑的正向交易方式,这也是长期交易下来积累利润的原因。

6.7.3 策略编写

第 1 步:编写策略框架

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

第 2 步:导入库

python
import time # 用于转换时间格式

因为我们这个是日内策略,到时候需要判断 K 线时间戳,如果有持仓,并且临近收盘时平仓出局。那么我们就直接 import time 就可以了。

第 3 步:获取基础数据

python
_C(exchange.SetContractType, "rb888") # 订阅期货品种 bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日线数组 if len(bar_arr) < 2: # 如果小于 2 根 K 线 return # 返回继续等待数据 yh = bar_arr[-2]['High'] # 昨日最高价 yl = bar_arr[-2]['Low'] # 昨日最低价 today_open = bar_arr[-1]['Open'] # 当日开盘价

菲阿里四价需要用到四个数据:昨日最高价、昨日最低价、昨日收盘价、当日开盘价。因为这些都是日线级别的数据,所以我们在使用 GetRecords 的时候,可以直接传入 PERIOD_D1 参数,表明我们要获取的是日 K,这样无论你的策略加载的是哪个周期的数据,它始终获取的都是日线级别的数据。

另外,细心的朋友可能已经发现,为什么这一次在调用 GetRecords 的时候,代码的写法跟以前不一样?这次我们使用的是优宽量化平台内置的重试函数 _C()。使用这个函数的好处是,该函数会一直调用指定函数到成功返回,这样可以避免直接使用 GetRecords 函数时,没有获取到数据导致报错的情况。

第 4 步:处理时间和获取最新价格

python
bar_arr = _C(exchange.GetRecords) # 获取当前设置周期 K 线数组 current = bar_arr[-1]['Time'] # 获取当前 K 线时间戳 local = time.localtime(current / 1000) # 处理时间戳 hour = int(time.strftime("%H", local)) # 格式化时间戳,并获取小时 minute = int(time.strftime("%M", local)) # 格式化时间戳,并获取分钟 price = bar_arr[-1]['Close'] # 获取最新价格

既然是获取当前的时间,那么肯定是使用获取当前设置周期的 K 线数据更为合适,所以需要重新使用一次 GetRecords,这次我们同样也是使用 _C() 重试函数,在不传入参数的情况下,就是默认获取当前设置周期的 K 线数组。另外,获取最新价格的目的是,计算交易逻辑和下单。

第 5 步:处理时间

python
def trade_time(hour, minute): minute = str(minute) if len(minute) == 1: minute = "0" + minute return int(str(hour) + minute)

注意:之所以创建这个函数,是因为我们在开仓之前,需要判断当前时间,是否在我们规定的交易时间之内,以及有持仓的时候,当前时间是否临近收盘。在第 4 步中,我们已经获取到了当前 K 线小时和分钟,为了方便比较,我们采用小时加分钟的方法,比如:

  • 如果 K 线时间是 9:05,那么 trade_time 返回的结果就是 905
  • 如果 K 线时间是 14:30,那么 trade_time 返回的结果就是 1430

第 6 步:设置虚拟持仓

python
mp = 0

第 7 步:设置止损

python
# 设置多头止损 if today_open / yh > 1.005: # 如果当天开盘价大于昨天最高价 long_stop = yh # 设置多头止损价为昨天最高价 elif today_open / yh < 0.995: # 如果当天开盘价小于昨天最高价 long_stop = today_open # 设置多头止损价为当天开盘价 else: # 如果当天开盘价接近昨天最高价 long_stop = (yh + yl) / 2 # 设置多头止损为昨天中间价 # 设置空头止损 if today_open / yl < 0.995: # 如果当天开盘价小于昨天最低价 short_stop = yl # 设置空头止损价为昨天最低价 elif today_open / yl > 1.005: # 如果当天开盘价大于昨天最低价 short_stop = today_open # 设置空头止损价为当天开盘价 else: # 如果当天开盘价接近昨天最低价 short_stop = (yh + yl) / 2 # 设置空头止损为昨天中间价

在大多数情况下,价格突破阻力线和支撑线,就把止损设置到当前开盘价这个位置。但是这里面有个问题:如果当天开盘价大于阻力线,而价格往下走;或者当天开盘价小于支撑线,而价格

往上走,会造成逻辑错误,导致频繁开平仓。为了解决这个问题,我们需要根据当天开盘与阻力线和支撑线的位置关系,分别设置不同的止损价格。如果当天开盘价大于昨天最高价 0.5%,那么就把多头的止损设置在昨天的最高价;如果当天开盘价在阻力线和支撑线之间,那么多头的止损价格还是当天的开盘价;如果当天开盘价接近于昨天最高价,那么就把多头的止损设置在昨天的中间价。设置空头的止损也是根据这个道理。

第 8 步:下单交易

python
trading = trade_time(hour, minute) if mp > 0: # 如果当前持有多单 # 如果当前价格小于多头止损线,或者超过规定的交易时间 if price < long_stop or trading > 1450: exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(price - 1, 1) # 平多单 mp = 0 # 重置虚拟持仓 if mp < 0: # 如果当前持有空单 # 如果当前价格大于空头止损线,或者超过规定的交易时间 if price > short_stop or trading > 1450: exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(price, 1) # 平空单 mp = 0 # 重置虚拟持仓 # 如果当前无持仓,并且在规定的交易时间内 if mp == 0 and 930 < trading < 1450: if price > yh: # 如果当前价格大于昨天最高价 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(price, 1) # 开多单 mp = 1 # 重置虚拟持仓 elif price < yl: # 如果价格小于昨天最低价 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(price - 1, 1) # 开空单 mp = -1 # 重置虚拟持仓

注意:为了避免逻辑错误,最好是把平仓逻辑写到开仓逻辑的前面。

6.7.4 完整策略代码

python
import time # 导入库, 用于转换时间格式 mp = 0 # 虚拟持仓 def trade_time(hour, minute): minute = str(minute) if len(minute) == 1: minute = "0" + minute return int(str(hour) + minute) def onTick(): _C(exchange.SetContractType, "rb888") # 订阅期货品种 bar_arr = _C(exchange.GetRecords, PERIOD_D1) # 获取日线数组 if len(bar_arr) < 2: # 如果小于 2 根 K 线 return # 返回继续等待数据 yh = bar_arr[-2]['High'] # 昨日最高价 yl = bar_arr[-2]['Low'] # 昨日最低价 today_open = bar_arr[-1]['Open'] # 当日开盘价 cur_bar_arr = _C(exchange.GetRecords, PERIOD_M1) # 获取当前设置周期 K 线数组 current = cur_bar_arr[-1]['Time'] # 获取当前 K 线时间戳 local = time.localtime(current / 1000) # 处理时间戳 hour = int(time.strftime("%H", local)) # 格式化时间戳, 并获取小时 minute = int(time.strftime("%M", local)) # 格式化时间戳, 并获取分钟 price = bar_arr[-1]['Close'] # 获取最新价格 global mp # 设置多头止损 if today_open / yh > 1.005: # 如果当天开盘价大于昨天最高价 long_stop = yh # 设置多头止损价为昨天最高价 elif today_open / yh < 0.995: # 如果当天开盘价小于昨天最高价 long_stop = today_open # 设置多头止损价为当天开盘价 else: # 如果当天开盘价接近昨天最高价 long_stop = (yh + yl) / 2 # 设置多头止损为昨天中间价 # 设置空头止损 if today_open / yl < 0.995: # 如果当天开盘价小于昨天最低价 short_stop = yl # 设置空头止损价为昨天最低价 elif today_open / yl > 1.005: # 如果当天开盘价大于昨天最低价 short_stop = today_open # 设置空头止损价为当天开盘价 else: # 如果当天开盘价接近昨天最低价 short_stop = (yh + yl) / 2 # 设置多头止损为昨天中间价 # 下单交易 trading = trade_time(hour, minute) if mp > 0: # 如果当前持有多单 # 如果当前价格小于多头止损线, 或者超过规定的交易时间 if price < long_stop or (trading > 1450 and trading < 1500): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(price - 1, 1) # 平多单 mp = 0 # 重置虚拟持仓 if mp < 0: # 如果当前持有空单 # 如果当前价格大于空头止损线, 或者超过规定的交易时间 if price > short_stop or (trading > 1450 and trading < 1500): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(price, 1) # 平空单 mp = 0 # 重置虚拟持仓 # 如果当前无持仓, 并且在规定的交易时间内 if mp == 0 and (900 < trading < 1450 or 2130 < trading < 2300): if price > yh: # 如果当前价格大于昨天最高价 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(price + 1, 1) # 开多单 mp = 1 # 重置虚拟持仓 elif price < yl: # 如果价格小于昨天最低价 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(price - 1, 1) # 开空单 mp = -1 # 重置虚拟持仓 def main(): while True: # 无限循环 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

虽然距离比赛结束已经有近 20 年之久了, 但以今天的眼光看那时的交易, 毫无过时感。但需要注意的是, 策略仅仅作为思路拓展, 不能直接用于实盘。对于菲阿里策略来说, 它提供了一个很好的入场参考工具, 我们可以根据自己对市场的认知做更深的开发。

6.8 简易波动 EMV 策略

在前面几节课程中,我们学习了基于指标来构建简单的策略,其中在计算指标时用到了 talib 库,大大简化了策略编写难度。但有时候我们写策略可能会用到 talib 库中没有的计算方法,那么今天我们就通过简易波动 EMV 策略,来学习下这种策略是如何实现的。

与其他技术指标不同,简易波动(Ease of Movement Value)反映的是价格、成交量、人气的变化,它是一种将价格与成交量变化相结合的技术,它通过衡量单位成交量的价格变动,形成一个价格波动指标。当市场人气聚集,交易活跃时提示买入信号;当成交量低迷,市场能量即将耗尽时提示卖出信号。

注意:简易波动 EMV 根据等量图和压缩图的原理设计而成,它的核心理念是:市场价格仅在发生趋势转折或即将转折时,才会消耗大量能量,外在表现就是成交量变大。当价格在上升的过程中,由于推波助澜的作用,不会消耗太多的能量。虽然这个理念与量价同升的观点相悖,但的确有其独特的地方。

6.8.1 EMV 计算公式

第 1 步:计算 mov_mid

$\text{movmid} = \frac{{\text{TH} + \text{TL}}}{2} - \frac{{\text{TY} + \text{YL}}}{2}$

其中 TH 代表当天最高价, TL 代表当天最低价, YH 代表前日最高价, YL 代表前日最低价。那么如果 MID > 0 意味着今天的平均价高于昨天的平均价。

第 2 步:计算 ratio

$\text{ratio} = \frac{{\text{TVOL}/1000}}{{\text{TH} - \text{TL}}}$

其中 TVOL 代表当天交易量, TH 代表当天最高价, TL 代表当天最低价。

第 3 步:计算 emv

$\text{emv} = \frac{{\text{movmid}}}{{\text{ratio}}}$

6.8.2 EMV 用法

EMV 的作者认为,巨量上涨伴随的是能量的快速枯竭,上涨往往不会持续太久;反而温和的成交量,能够保存一定的能量,往往使上涨持续更久。一旦上涨趋势形成,较少的成交量就能推动价格上涨,EMV 的数值就会升高。一旦下跌趋势行情形成,往往伴随的是无量或少量下跌,EMV 的数值就会下降。如果价格处于震荡行情或者价格上涨和下跌都伴随较大成交量时,EMV 的数值也会接近于零。因此你会发现,EMV 在大部分行情中都处于零轴下方,这也是这个指标的一大特色。站在另一个角度看,EMV 重视大趋势且能够产生足够利润的行情。

EMV 的用法相当简单,只要看 EMV 是否穿越零轴即可,当 EMV 在 0 以下时,代表市场弱市;当 EMV 在 0 以上时,代表市场强市。让 EMV 由负数转为正数时应该买进;当 EMV由正数转为负数时应该卖出。其特点是不仅能较好的避免市场中的震荡行情,而且还能在趋势行情启动的时候及时入场。但由于 EMV 反映的是价格在变动时的成交量的变化情况,所以仅对中长期走势有作用。对于短线或交易周期比较小的行情 EMV 的效果很差。

6.8.3 策略实现

第 1 步:编写策略框架

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

优宽量化采用轮询模式,首先需要定义一个 main 函数和一个 onTick 函数,main 函数是策略的入口函数,程序会从 main 函数开始逐行执行代码。在 main 函数中,写入 while 循环,重复执行 onTick 函数,所有的策略核心代码都写在 onTick 函数中。

第 2 步:获取持仓数据

python
# 获取持仓数量 def get_position(): position = 0 # 赋值持仓数量为 0 position_arr = _C(exchange.GetPosition) # 获取持仓数组 if len(position_arr) > 0: # 如果持仓数组长度大于 0 for i in position_arr: # 遍历持仓数组 if i['ContractType'][:2] == 'IH': # 如果持仓品种等于订阅品种 if i['Type'] % 2 == 0: # 如果是多单 position = i['Amount'] # 赋值持仓数量为正数 else: position = -i['Amount'] # 赋值持仓数量为负数 return position # 返回持仓量

因为在这个策略中,只使用了实时的持仓数量,为了方便维护,这里使用 get_position 封装了持仓量,如果当前持有多单就返回正数,如果当前持有空单就返回负数。

第 3 步:获取 K 线数据

python
# 获取数据 exchange.SetContractType('IH000') # 订阅期货品种 bars_arr = exchange.GetRecords() # 获取 K 线数组 if len(bars_arr) < 10: # 如果 K 线数量小于 10 根 return

在获取具体的 K 线数据之前,首先要先订阅具体的合约,使用优宽量化的 SetContractType 函数,并传入合约代码即可,如果想知道该合约的其他信息,也可以使用一个变量来接收这个数据。接着使用 GetRecords 函数就可以获取 K 线数据,因为返回的是一个数组,所以我们使用变量 bars_arr 来接受它。

第 4 步:计算 emv

python
# 计算 emv bar1 = bars_arr[-2] # 获取上一根 K 线数据 bar2 = bars_arr[-3] # 获取前一根 K 线数据 # 计算 mov_mid 的值 mov_mid = (bar1['High'] + bar1['Low'])/2 - (bar2['High'] + bar2['Low'])/2 if bar1['High'] != bar1['Low']: # 如果被除数不为 0 # 计算 ratio 的值 ratio = (bar1['Volume'] / 10000) / (bar1['High'] - bar1['Low']) else: ratio = 0 # 如果 ratio 的值大于 0 if ratio > 0: emv = mov_mid / ratio else: emv = 0

注意:在这里我们并没有使用最新的价格来计算 EMV 的值,而是采用相对滞后的当前 K 线出信号,下根 K 线发单的方法。这么做的目的是让回测更接近于实盘交易。我们知道,尽管现在量化交易软件已经非常先进了,但还是很难做到完全模拟真实的实盘 Tick 环境,特别是面对回测 Bar 级超长数据时,所以就采用这个折中的方法。

第 5 步:下单交易

python
# 下单交易 current_price = bars_arr[-1]['Close'] # 最新价格 position = get_position() # 获取最新持仓量 if position > 0: # 如果持有多单 if emv < 0: # 如果价格小于牙齿 exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(round(current_price - 0.2, 2), 1) # 平多单 if position < 0: # 如果持有空单 if emv > 0: # 如果价格大于牙齿 exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(round(current_price + 0.2, 2), 1) # 平空单 if position == 0: # 如果无持仓 if emv > 0: # 如果价格大于上唇 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(round(current_price + 0.2, 2), 1) # 开多单 if emv < 0: # 如果价格小于下巴 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(round(current_price - 0.2, 2), 1) # 开空单

在下单交易之前,我们需要先确定两个数据,一个是下单的价格,另一个是当前的持仓状态。下单的价格很简单,直接使用当前的收盘价加减品种的最小变动价位即可。由于我们之前已经使用 get_position 函数封装了持仓量,所以这里直接调用即可。最后就是根据 EMV 与零轴的位置关系开平仓了。

6.8.4 完整策略

python
# 获取持仓数量 def get_position(): position = 0 # 赋值持仓数量为 0 position_arr = _C(exchange.GetPosition) # 获取持仓数组 if len(position_arr) > 0: # 如果持仓数组长度大于 0 for i in position_arr: # 遍历持仓数组 if i['ContractType'][:2] == 'IH': # 如果持仓品种等于订阅品种 if i['Type'] % 2 == 0: # 如果是多单 position = i['Amount'] # 赋值持仓数量为正数 else: position = -i['Amount'] # 赋值持仓数量为负数 return position # 返回持仓量 # 策略主函数 def onTick(): exchange.SetContractType('IH000') # 订阅期货品种 bars_arr = exchange.GetRecords() # 获取 K 线数组 if len(bars_arr) < 10: # 如果 K 线数量小于 10 根 return bar1 = bars_arr[-2] # 获取上一根 K 线数据 bar2 = bars_arr[-3] # 获取前一根 K 线数据 mov_mid = (bar1['High'] + bar1['Low']) / 2 - (bar2['High'] + bar2['Low']) / 2 if bar1['High'] != bar1['Low']: # 如果被除数不为 0 ratio = (bar1['Volume'] / 10000) / (bar1['High'] - bar1['Low']) else: ratio = 0 if ratio > 0: # 如果 ratio 的值大于 0 emv = mov_mid / ratio else: emv = 0 current_price = bars_arr[-1]['Close'] # 最新价格 position = get_position() # 获取最新持仓量 if position > 0: # 如果持有多单 if emv < 0: # 如果当前价格小于牙齿 exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(round(current_price - 0.2, 2), 1) # 平多单 if position < 0: # 如果持有空单 if emv > 0: # 如果当前价格大于牙齿 exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(round(current_price + 0.2, 2), 1) # 平空单 if position == 0: # 如果无持仓 if emv > 0: # 如果当前价格大于上唇 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(round(current_price + 0.2, 2), 1) # 开多单 if emv < 0: # 如果当前价格小于下巴 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(round(current_price - 0.2, 2), 1) # 开空单 # 程序入口函数 def main(): while True: # 循环 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

通过本节课程学习,可以看出 EMV 与普通交易者的看法相反,但却不无道理。由于 EMV 引入了成交量数据,因此比其他单纯用价格计算的技术指标,更能有效发现价格背后的东西。每一种策略都有着不同的特点,只有充分了解不同策略之间的优缺点,去其糟粕取其精华才能离成功更进一步。

6.9 经典恒温器策略

趋势行情不会永远持续下去,事实上市场大部分时间都处于震荡行情,所以才会有人希望能得到一种交易策略,既可以用在趋势行情,也可以用在震荡行情。本节我们就用优宽量化交易平台,构建一个趋势和震荡行情通用的经典恒温器策略。

6.9.1 策略简介

提到恒温器可能会有人想到汽车发动机与水箱之间的恒温器。当发动机温度低时,恒温器是关闭状态,此时发动机和水箱的水是不相通的,直到发动机温度升高,达到最佳机油润滑效果;当发动机温度升高到一定阈值时,节温器是开启状态,此时发动机和水箱的水形成循环,并流经风扇开启降温模式,直到达到发动机最佳工作温度。

那么恒温器策略也类似这个原理,并且延用了这个名字。它通过波动指数作为阈值,将市场分为趋势行情和震荡行情,自动对两种不同的行情使用对应的交易逻辑,有效弥补了趋势策略在震荡行情中的不适应。

6.9.2 市场波动指数

如何把市场划分为趋势行情和震荡行情,也就成了这个策略的关键,恒温器策略引入了市场波动指数(Choppy Market Index),简称CMI。它是一个用来判断市场走势类型的技术指标。通过计算当前收盘价与N周期前收盘价的差值与这段时间内价格波动的范围的比值,来判断目前的价格走势是趋势还是震荡。

CMI 的计算公式为:

$CMI = \frac{{\text{{abs}}(\text{{Close}} - \text{{ref}}(\text{{Close}}, n-1)) \times 100}}{{\text{{HHV}}(\text{{High}}, n) - \text{{LLV}}(\text{{Low}}, n)}}$

其中,abs是绝对值,n是周期数。

6.9.3 策略逻辑

一般来说CMI的值在0~100区间,值越大,趋势越强。当CMI的值小于20时,策略认为市场处于震荡模式;当CMI的值大于等于20时,策略认为市场处于趋势模式。

整个策略逻辑,可以简化的写成下面这样:

  • 如果CMI < 20,执行震荡策略;
  • 如果CMI ≥ 20,执行趋势策略;

策略架构就是这么简单,剩下的就是把震荡策略的内容和趋势策略的内容,填充到这个框架里面。

6.9.4 策略编写

依次打开:youquant.com网站 > 登录 > 控制中心 > 策略库 > 新建策略 > 点击右上角下拉菜单选择Python语言,开始编写策略,注意看下面代码中的注释。

第1步:编写策略框架

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠1秒

第2步:定义虚拟持仓变量

python
mp = 0 # 定义一个全局变量,用于控制虚拟持仓

第3步:获取基础数据

python
exchange.SetContractType("rb000") # 订阅期货品种 bar_arr = exchange.GetRecords() # 获取K线数组 if len(bar_arr) < 100: # 如果K线少于100根 return # 直接返回 close0 = bar_arr[-1]['Close'] # 获取最新价格(卖价),用于开平仓 bar_arr.pop() # 删除K线数组最后一个元素,策略采用开平仓条件成立,下根K线交易模式

首先使用优宽量化API中的SetContractType方法订阅期货品种。接着使用GetRecords方法获取K线数组,因为有时候K线数量太少,导致无法计算一些数据,所以我们判断如果K线少于100根,就直接返回等待下一次新数据。然后我们从K线数组中获取最新的卖一价,这个主要用于使用开平仓函数时传入价格参数。最后因为我们的策略采用当前K线开平仓条件成立,在下根K线交易的模式,所以需要删除K线数组最后一个元素。

因为策略回测是根据历史价格来计算各种收益绩效,历史价格是一种固定的数据,不可能完全跟实盘一样,所以这样做有2个好处:第1个可以使回测绩效更接近于实盘;第2个是避免未来函数和偷价这些常见的策略逻辑错误。

第四步:计算市场波动指数CMI

python
# 计算CMI指标用以区分震荡市与趋势市 close1 = bar_arr[-1]['Close'] # 最新收盘价 close30 = bar_arr[-30]['Close'] # 前30根K线的收盘价 hh30 = TA.Highest(bar_arr, 30, 'High') # 最近30根K线最高价 ll30 = TA.Lowest(bar_arr, 30, 'Low') # 最近30根K线最低价 cmi = abs((close1 - close30) / (hh30 - ll30)) * 100 # 计算市场波动指数

根据CMI的计算公式,我们需要4个数据,分别是:最新收盘价、前30根K线的收盘价、最近30根K线的最高价、最近30根K线的最低价。前两个很简单,可以直接从K线数组中获取。

最后两个则需要调用优宽量化内置的talib指标库TA.HighestTA.Lowest,这两个指标函数需要传入三个参数,分别是:K线数据、周期、属性。最后当前收盘价与前30根K线的收盘价的差值与这段时间内价格波动的范围的比值就是市场波动指数CMI。

第五步:定义宜卖市和宜买市

python
# 震荡市中收盘价大于关键价格为宜卖市,否则为宜买市 high1 = bar_arr[-1]['High'] # 最新最高价 low1 = bar_arr[-1]['Low'] # 最新最低价 kod = (close1 + high1 + low1) / 3 # 计算关键价格 if close1 > kod: be = 1 se = 0 else: be = 0 se = 1

在震荡市场中,通常存在一种现象:如果今天价格上涨的话,那么明天的价格下跌的概率更大。而今天价格如果下跌的话,那么明天的价格上涨的概率更大,而这也正是震荡市场的特性。所以这里首先定义一个关键价格(最高价+最低价+收盘价的平均值)。这些数据都可以在K线数据中直接获取。如果当前价格大于关键价格,那么明天应该震荡看空。相反的,如果当前价格小于关键价格,那么明天应该震荡看多。

第六步:计算震荡行情的进出场价格

python
# 计算10根K线ATR指标 atr10 = TA.ATR(bar_arr, 10)[-1] # 定义最高价与最低价3日均线 high2 = bar_arr[-2]['High'] # 上根K线最高价 high3 = bar_arr[-3]['High'] # 前根K线最高价 low2 = bar_arr[-2]['Low'] # 上根K线最低价 low3 = bar_arr[-3]['Low'] # 前根K线最低价 avg3high = (high1 + high2 + high3) / 3 # 最近3根K线最高价的均值 avg3low = (low1 + low2 + low3) / 3 # 最近3根K线最低价的均值 # 计算震荡行情的进场价格 open1 = bar_arr[-1]['Open'] # 最新开盘价 if close1 > kod: # 如果收盘价大于关键价格 lep = open1 + atr10 * 3 sep = open1 - atr10 * 2 else: lep = open1 + atr10 * 2 sep = open1 - atr10 * 3 lep1 = max(lep, avg3high) # 计算震荡市多头进场价格 sep1 = min(sep, avg3low) # 计算震荡市空头进场价格

首先计算10根K线ATR指标,同样也是直接调用优宽量化的内置talib库中的TA.ATR即可。为了防止假突破,导致策略来回止损,因此加入了一个最高价与最低价3日均线滤网来避免这种情形,分别从K线数组中获取最近3根K线的值求其平均就可以了。

有了以上计算步骤,最后就可以计算震荡行情中的进出场价格了,其原理是以开盘价为中心,上下加减最近10根K线的真实波动幅度,形成一个开多和开空的价格通道。为了使策略更加符合市场走势,在做多和做空时分别设置了不同的空间。

注意:在震荡行情中看多,只代表价格上涨的概率更大一些,并不是指价格一定就会上涨。所以把做多的阈值设置的比较低一点,把做空的阈值设置的比较高一点。同理在震荡行情中看空,只代表价格下跌的概率更大一些,并不是指价格一定就会下跌。所以把做空的阈值设置的比较低一点,把做多的阈值设置的比较高一点。

第七步:计算趋势行情的进场价格

python
# 计算趋势行情的进场价格 boll = TA.BOLL(bar_arr, 50, 2) # 调用BOLL指标函数 up_line = boll[0][-1] # 获取上轨 mid_line = boll[1][-1] # 获取中轨 down_line = boll[2][-1] # 获取下柜

在处理趋势行情的进出场价格上,沿用了布林带策略,当价格向上突破布林带上轨时多头开仓,当价格向下突破布林带下轨时空头开仓,平仓方式则是以当前价格与布林中轨的位置关系来判断。

6.9.5 完整策略代码

python
mp = 0 # 定义一个全局变量,用于控制虚拟持仓 # 策略主函数 def onTick(): exchange.SetContractType("rb000") # 订阅期货品种 bar_arr = exchange.GetRecords() # 获取K线数组 if len(bar_arr) < 100: # 如果K线少于100根 return # 直接返回 close0 = bar_arr[-1]['Close'] # 获取最新价格(卖价),用于开平仓 bar_arr.pop() # 删除K线数组最后一个元素 # 计算CMI指标用以区分震荡市与趋势市 close1 = bar_arr[-1]['Close'] # 最新收盘价 close30 = bar_arr[-30]['Close'] # 前30根K线的收盘价 hh30 = TA.Highest(bar_arr, 30, 'High') # 最近30根K线最高价 ll30 = TA.Lowest(bar_arr, 30, 'Low') # 最近30根K线最低价 cmi = abs((close1 - close30) / (hh30 - ll30)) * 100 # 计算市场波动指数 # 震荡市中收盘价大于关键价格为宜卖市,否则为宜买市 high1 = bar_arr[-1]['High'] # 最新最高价 low1 = bar_arr[-1]['Low'] # 最新最低价 kod = (close1 + high1 + low1) / 3 # 计算关键价格 if close1 > kod: be = 1 se = 0 else: be = 0 se = 1 # 计算10根K线ATR指标 atr10 = TA.ATR(bar_arr, 10)[-1] # 定义最高价与最低价3日均线 high2 = bar_arr[-2]['High'] # 上根K线最高价 high3 = bar_arr[-3]['High'] # 前根K线最高价 low2 = bar_arr[-2]['Low'] # 上根K线最低价 low3 = bar_arr[-3]['Low'] # 前根K线最低价 avg3high = (high1 + high2 + high3) / 3 # 最近3根K线最高价的均值 avg3low = (low1 + low2 + low3) / 3 # 最近3根K线最低价的均值 # 计算震荡行情的进场价格 open1 = bar_arr[-1]['Open'] # 最新开盘价 if close1 > kod: # 如果收盘价大于关键价格 lep = open1 + atr10 * 3 sep = open1 - atr10 * 2 else: lep = open1 + atr10 * 2 sep = open1 - atr10 * 3 lep1 = max(lep, avg3high) # 计算震荡市多头进场价格 sep1 = min(sep, avg3low) # 计算震荡市空头进场价格 # 计算趋势行情的进场价格 boll = TA.BOLL(bar_arr, 50, 2) up_line = boll[0][-1] mid_line = boll[1][-1] down_line = boll[2][-1] global mp # 引入全局变量 if cmi < 20: # 如果是震荡行情 if mp == 0 and close1 >= lep1 and se: exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(close0, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and close1 <= sep1 and be: exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(close0 - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单 if mp == 1 and (close1 >= avg3high or be): exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(close0 - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and (close1 <= avg3low or se): exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(close0, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 else: # 如果是趋势行情 if mp == 0 and close1 >= up_line: exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(close0, 1) # 开多单 mp = 1 # 设置虚拟持仓的值,即有多单 if mp == 0 and close1 <= down_line: exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(close0 - 1, 1) # 开空单 mp = -1 # 设置虚拟持仓的值,即有空单 if mp == 1 and close1 <= mid_line: exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(close0 - 1, 1) # 平多单 mp = 0 # 设置虚拟持仓的值,即空仓 if mp == -1 and close1 >= mid_line: exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(close0, 1) # 平空单 mp = 0 # 设置虚拟持仓的值,即空仓 # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠1秒

第 7 章 CTA 之回归策略

趋势跟踪策略的表现形式是追涨杀跌,与之相反就是低买高卖的回归策略,也称均值回归策略。商品期货普遍存在一种规律,震荡行情远多于趋势行情,回归策略通过不断的低买高卖实现利润增长。

7.1 布林带跨期套利策略

索罗斯在 1987 年撰写的《金融炼金术》一书中,曾经提出过一个重要的命题:I believe the market prices are always wrong in the sense that they present a biased view of the future.市场有效假说只是理论上的假设,实际上市场参与者并不总是理性的,并且在每一个时间点上,参与者不可能完全获取和客观解读所有的信息,再者就算是同样的信息,每个人的反馈都不尽相同。也就是说,价格本身就已经包含了市场参与者的错误预期,所以本质上市场价格总错误的。这或许是套利者的利润来源。

7.1.1 策略原理

在一个非有效的期货市场中,不同时期交割合约之间受到市场影响也并不总是同步,其定价也并非完全有效的原因。那么,根据同一种交易标的的不同时期交割合约价格为基础,如果两个价格出现了较大的价差幅度,就可以同时买卖不同时期的期货合约,进行跨期套利。比如:螺纹钢 2010 合约和螺纹钢 2105 合约。

举个例子,假设螺纹钢 2010 和螺纹钢 2105 的价差长期维持在 5 左右。如果某一天价差达到 7,我们预计价差会在未来某段时间回归到 5。那么就可以卖出螺纹钢 2010,同时买入螺纹钢 2105,来做空这个价差。反之亦然。尽管这种价差是存在的,但是人工操作耗时、准确性差以及价格变化的影响,人工套利往往存在诸多不确定性。通过量化模型捕捉套利机会并制定套利交易策略,以及程序化算法自动向交易所下达交易订单,快速准确捕捉机会,高效稳定赚取收益,这就是量化套利的魅力所在。

7.1.2 策略逻辑

  • 做多价差开仓条件:如果当前账户没有持仓,并且价差小于 boll 下轨,就做多价差。即:买开螺纹钢 2010,卖开螺纹钢 2105。
  • 做空价差开仓条件:如果当前账户没有持仓,并且价差大于 boll 上轨,就做空价差。即:卖开螺纹钢 2010,买开螺纹钢 2105。
  • 做多价差平仓条件:如果当前账户持有螺纹钢 2010 多单,并且持有螺纹钢 2105 空单,并且价差大于 boll 中轨,就平多价差。即:卖平螺纹钢 2010,买平螺纹钢 2105。
  • 做空价差平仓条件:如果当前账户持有螺纹钢 2010 空单,并且持有螺纹钢 2105 多单,并且价差小于 boll 中轨,就平空价差。即:买平螺纹钢 2010,卖平螺纹钢 2105。

7.1.3 策略编写

python
'''backtest start: 2024-04-15 09:00:00 end: 2024-04-22 15:00:00 period: 1h basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] args: [["SA","FG405"],["SB","FG409"]] ''' class Hedge: def __init__(self, q, e, initAccount, symbolA, symbolB, maPeriod, atrRatio, opAmount): self.q = q self.initAccount = initAccount self.status = 0 self.symbolA = symbolA self.symbolB = symbolB self.e = e self.isBusy = False self.maPeriod = maPeriod self.atrRatio = atrRatio self.opAmount = opAmount def poll(self): if (self.isBusy or not exchange.IO("status")) or not ext.IsTrading(self.symbolA): Sleep(1000) return exchange.SetContractType(self.symbolA) recordsA = exchange.GetRecords() exchange.SetContractType(self.symbolB) recordsB = exchange.GetRecords() if not recordsA or not recordsB: return if recordsA[-1]["Time"] != recordsB[-1]["Time"]: return minL, rA, rB = min(len(recordsA), len(recordsB)), recordsA.copy(), recordsB.copy() rA.reverse() rB.reverse() arrDiff = [] for i in range(minL): arrDiff.append(rB[i]["Close"] - rA[i]["Close"]) arrDiff.reverse() if len(arrDiff) < self.maPeriod: return boll = TA.BOLL(arrDiff, self.maPeriod, self.atrRatio) ext.PlotLine("上轨", boll[0][-2], recordsA[-2]["Time"]) ext.PlotLine("中轨", boll[1][-2], recordsA[-2]["Time"]) ext.PlotLine("下轨", boll[2][-2], recordsA[-2]["Time"]) ext.PlotLine("收盘价差价", arrDiff[-2], recordsA[-2]["Time"]) LogStatus(f"{_D()}\n 上轨: {boll[0][-1]}\n 中轨: {boll[1][-1]}\n 下轨: {boll[2][-1]}\n 当前收盘差价: {arrDiff[-1]}") action = 0 if self.status == 0: if arrDiff[-1] > boll[0][-1]: Log("开仓 A 买 B 卖", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000") action = 2 elif arrDiff[-1] < boll[2][-1]: Log("开仓 A 卖 B 买", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000") action = 1 elif self.status == 1 and arrDiff[-1] > boll[1][-1]: Log("平仓 A 卖 B 买", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000") action = 1 elif self.status == 2 and arrDiff[-1] < boll[1][-1]: Log("平仓 A 买 B 卖", ", A 最新价格: ", recordsA[-1]["Close"], ", B 最新价格: ", recordsB[-1]["Close"], "#FF0000") action = 2 if action == 0: return self.isBusy = True tasks = [] if action == 1: tasks.append([self.symbolA, "sell" if self.status == 0 else "closebuy"]) tasks.append([self.symbolB, "buy" if self.status == 0 else "closesell"]) elif action == 2: tasks.append([self.symbolA, "buy" if self.status == 0 else "closesell"]) tasks.append([self.symbolB, "sell" if self.status == 0 else "closebuy"]) def callBack(task, ret): self.isBusy = False if task["action"] == "sell": self.status = 2 elif task["action"] == "buy": self.status = 1 else: self.status = 0 account = _C(exchange.GetAccount) LogProfit(account["Balance"] - self.initAccount["Balance"], account) self.q.pushTask(self.e, tasks[1][0], tasks[1][1], self.opAmount, callBack) self.q.pushTask(self.e, tasks[0][0], tasks[0][1], self.opAmount, callBack) def main(): while not exchange.IO("status"): Sleep(1000) initAccount = _C(exchange.GetAccount) q = ext.NewTaskQueue() p = ext.NewPositionManager() if CoverAll: p.CoverAll() t = Hedge(q, exchange, initAccount, SA, SB, MAPeriod, ATRRatio, OpAmount) while True: q.poll() t.poll() Sleep(1000)

策略参数设置如下:

image

如上面的代码,该策略首先订阅了近期合约SA和远期合约SB合约,并分别获取了它们的K线数据,然后计算两个合约的价差,接着以价差为数据计算布林带指标,最后实现当差价超过布林线上轨时正对冲,触碰下轨时反对冲。持仓时触碰布林中线平仓。这样就基本实现了一个跨期的对冲策略,策略逻辑还是比较简洁明朗的,大家可以根据自己中意的品种进行不同的尝试。

7.2 期现套利

“套利”在现实生活中却很常见。比如:便利店老板从批发市场以0.5元买入一瓶矿泉水,然后在店里以1元的价格出售,最后赚取0.5元的差价。这个过程其实就类似套利。金融市场上的套利跟这个道理差不多,只不过套利的形式有多种多样。

7.2.1 什么是套利

在商品期货市场中,理论上5月份交割的苹果合约价格减去10月交割的苹果合约价格,其结果应该接近于0或者稳定在一定的价格区间内。但事实上由于受到天气、市场供需等诸多因素的原因,近期和远期合约价格在一段时间内会分别受到不同程度的影响,价差也会出现较大幅度的波动。

但无论如何,价差通常最终会回归到一定的价格区间内,那么如果价差大于这个区间,就卖5月份合约,同时买10月份合约,做空价差赚取利润;如果价差小于这个区间,就买5月份合约,同时卖10月份合约,做多价差赚取利润。这就是通过买卖同一个品种但不同交割月份的跨期套利。

除了跨期套利外,还有买入出口国大豆同时卖出进口国大豆,或者卖出出口国大豆同时买入进口国大豆的跨市场套利;买入上游原材料铁矿石同时卖出下游成品螺纹钢,或者卖出上游原材料铁矿石同时买入下游成品螺纹钢的跨品种套利等等。

7.2.2 期现套利方法

但是上面这几种套利方法,虽然字面上是“套利”,并不属于纯粹意义上的套利,它们本质上还是属于有风险的投机,只不过这种投机的方式是做多或做空价差。虽然价差在大部分时间内趋于稳定,但也可能出现很长时间不回归的行情。

期现套利的核心原理是,同一个商品在同一个时间点只能有一个价格,期货到了交割时间就变成现货,所以在临近交割时会强制回归。这个与跨期套利完全不同,跨期套利是两个不同交割月的合约,到期时也就是两个不同月份的现货,当然可以是两个价格。

  • 基差 = 期货价格 - 现货价格

期现套利最大的特点是理论上无风险,主要是根据基差状态,计算利润区间。如果基差过大,就可以买入现货,同时做空期货,等待基差重新归零,就可以期货和现货双边平仓,赚取基差的利润。

因为增值税的存在(现货交易通常有 17%~20%的增值税),所以基差一般维持在正常的水平。我们可以计算基差的布林带阈值,当基差超过上线的时候,证明期货价格过高,进行卖出的操作;当回归到布林带中线,进行相应的平仓;与此对立,当基差下穿下线的时候,证明期货期货价格过低,进行买入的操作,同样当回归到布林带中线,进行多头的平仓。

  • 基差超过布林带上线,空头开仓;
  • 基差回归布林带中线,空头平仓;
  • 基差超过布林带下线,多头开仓;
  • 基差回归布林带中线,多头平仓;

7.2.3 策略逻辑

下面我们具体使用代码将具体的程序化交易进行实现。首先注册并登陆优宽量化官网,点击控制中心,点击策略库+新建策略。在左上角下拉菜单中选择Python,并填入策略的名字。

第一步:编写策略框架

python
# 策略主函数 def onTick(): pass # 策略入口 def main(): while True: # 进入循环模式 onTick() # 执行策略主函数 Sleep(1000 * 60 * 60 * 24) # 策略休眠一天

第一步编写策略框架,策略框架是两个函数,main函数是策略的入口,主要功能是交易之前的预处理,程序会先从main函数开始执行,然后进入无限循环模式,重复执行onTick函数,onTick函数是策略的主函数,主要执行核心代码。

第二步:增加图表功能

python
# 全局变量 # 期现图表 cfgA = { "extension": { "layout": 'single', "col": 6, "height": "500px", }, "title": { "text": "期现图表" }, "xAxis": { "type": "datetime" }, "series": [{ "name": "期货价格", "data": [], }, { "name": "现货价格", "data": [], }] } # 基差图表 cfgB = { "extension": { "layout": 'single', "col": 6, "height": "500px", }, "title": { "text": "基差图表" }, "xAxis": { "type": "datetime" }, "series": [{ "name": "基差价格", "data": [], }] } chart = Chart([cfgA, cfgB]) # 创建一个图表对象 # 策略主函数 def onTick(): chart.add(0, []) # 绘制图表 chart.add(1, []) # 绘制图表 chart.add(2, []) # 绘制图表 chart.update([cfgA, cfgB]) # 更新图表 # 策略入口 def main(): LogReset() # 运行前先清空之前的 Log 日志信息 chart.reset() # 运行前先清空之前的图表信息 while True: # 进入循环模式 onTick() # 执行策略主函数 Sleep(1000 * 60 * 60 * 24) # 策略休眠一天

在这个策略中,一共创建了 2 个图表,并左右分布排列。其中左图cfgA是期现图表,包含期货价格和现货价格,右图cfgB是基差图表。然后创建一个chart对象。最后是在onTick函数中实时更新图表中的数据。

第三步:获取数据

我们一共需要获取三种数据:期货价格、现货价格、基差价格。获取期货价格很简单,直接使用SetContractType函数订阅期货品种,再使用GetRecords函数就可以获取 K 线的收盘价。

基差作为外部数据,当引入到策略当中需要一系列的繁杂操作(API获取,数据清理等),为了解决这一痛点,优宽量化开发的DATADATA平台已经内置了商品期货基本面数据,包括期货相关的各种宏观数据。因此我们首先需要在DATADATA平台编写Sql代码,获取基本面数据。

打开DATADATA平台,点击创建图表,写入sql代码,保存数据,然后点击铅笔按钮,设置自动更新,接着点击获取API。

sql
WITH DATA1 AS ( SELECT *, EXTRACT(EPOCH FROM time)::bigint * 1000 AS time_milliseconds FROM futures_data.basis WHERE product_name = '生猪' ORDER BY date ) SELECT time_milliseconds AS time, date, json_build_object('spot_price', spot_price) AS data FROM DATA1;

通过设置“定时刷新”,我们运行策略可以获取最新的数据,基差数据是每日刷新的,因此定制刷新的频率为“每天”,基差时间更新的具体时间点为八点,所以选择时间为"08:00 - 09:00"就可以。

image

接着回到策略编写界面,使用GetData获取数据,里面填写的参数为上面设置完成的 API ,返回的是包含时间戳的字典数据,我们进行一下简单的处理。当检查到DATADATA平台数据更新,就可以获取最新的基差数据进行相应的画图和套利操作。

python
basisList = [] # 基差列表 obj = exchange.GetData("https://www.datadata.cn/api/v1/query/94af6e1e-2efa-4e0c-8c08-f7853b2bf969/data") if obj["Data"] != pre_data: info = json.loads(obj["Data"]) spot = info['spot_price'] pre_data = obj["Data"] basis = futures_price - info['spot_price'] basisList.append(basis) # 基差列表数据的收集

第四步:计算基差上下限
获取到基差数据之后,进行基差数据的布林带上中下限阈值的计算。

python
if len(basisList) < pigPeriod + 2: return basisBoll = TA.BOLL(basisList, pigPeriod, 2) exbasisBoll = TA.BOLL(basisList, pigPeriod, 3)

第五步:进行基差套利操作

前期信号指标计算完毕以后,当检查到实时基差上穿布林带上轨的时候,证明基差过大,进行期货空头开仓的操作;开仓完毕,当检查到回归布林带中轨,进行空头平仓。当检查到实时基差下穿布林带下轨,证明基差过小,进行多头开仓,回归中轨进行平仓。

python
if basis > basisBoll[0][-2] and mp == 0: p.OpenShort('lh888', 1) mp = -1 if mp == -1 and basis < basisBoll[1][-2]: p.Cover('lh888') mp = 0 if basis < basisBoll[2][-2] and mp == 0: p.OpenLong('lh888', 1) mp = 1 if mp == 1 and basis > basisBoll[1][-2]: p.Cover('lh888') mp = 0

7.2.5 策略回测效果

image

根据策略运行结果,可以看到在2024年1月至4月末,策略取得了良好的收益,证明生猪品种具有较平稳的基差回归特性,我们可以以此为依据进行相应的期现套利操作。

7.3 乖离率 BIAS 策略

俗话说分久必合合久必分,在期货市场也有这种现象,没有只涨不跌的品种也没有只跌不涨的品种。但是什么时候分什么时候合,这就要看乖离率了。本节我们将使用乖离率构建一个简单的策略。

7.3.1 乖离率简介

乖离率 BIAS 是由移动平均线衍生出来的一种技术指标,它主要是以百分比的形式,衡量价格在波动中与移动平均线的偏离程度。如果说均线是交易者的平均成本,那么乖离率就是交易者的平均回报率。注意:相对来说乖离率是一个比较冷门的技术指标,但在量化交易中使用乖离率可以增加策略的多样性。

7.3.2 乖离率的原理

乖离率的理论基础是对交易者的心里分析,当价格大于市场平均成本太多时,表示多头交易者获利越丰厚,容易萌生赚钱就走的念头,进而会造成价格下跌。当价格小于市场平均成本太多时,表示空头交易者获利丰厚,容易萌生赚钱就走的念头,进而会造成价格上涨。

  • 当价格向上偏离均线时,乖离率过大,未来价格有很大几率会下跌。
  • 当价格向下偏离均线时,乖离率过小,未来价格有很大几率会上涨。

虽然移动平均线是由价格计算而来,但从外在形式上价格一定会向移动平均线靠拢,或者说价格总是围绕着移动平均线上下波动。如果价格偏离均线太远,不管价格是在均线之上还是之下,最后都可能趋向于均线,而乖离率正是表示价格偏离均线的百分比值。

7.3.3 乖离率计算公式

乖离率 = [(当日收盘价 - N 日平均价) / N 日平均价] * 100%

其中,N 是移动均线参数,由于 N 的周期不同,乖离率的计算结果也不同。一般情况下 N 的取值是:6、12、24、36 等等。在实际使用中,也可以根据不同的品种动态调整。但参数的选择十分重要,如果参数过小,乖离率就会过于敏感,如果参数过大,乖离率就会过于迟钝。乖离率的计算结果有正负之分,正的乖离率越大,代表多头获利越大,价格回调的概率越大。负的乖离率越大,代表空头获利越大,价格反弹的概率越大。

7.3.4 策略逻辑

由于乖离率是另一种均线的表现形式,那么我们也可以根据双均线策略改编一个双乖离率策略。通过短期乖离率与长期乖离率的位置关系,判断当前的市场状态。

  • 多头开仓:如果当前无持仓,并且长期乖离率大于短期乖离率
  • 空头开仓:如果当前无持仓,并且长期乖离率小于短期乖离率
  • 多头平仓:如果当前持多单,并且长期乖离率小于短期乖离率
  • 空头平仓:如果当前持空单,并且长期乖离率大于短期乖离率

7.3.5 策略编写

第 1 步:编写策略框架

python
# 策略主函数 def onTick(): pass # 程序入口 def main(): while True: # 进入无限循环模式 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

优宽量化采用轮询模式,首先需要定义一个 main 函数和一个 onTick 函数,main 函数是策略的入口函数,程序会从 main 函数开始逐行执行代码。在 main 函数中,写入 while 循环,重复执行 onTick 函数,所有的策略核心代码都写在 onTick 函数中。

第 2 步:定义虚拟持仓和外部变量

python
short = 10 long = 50 mp = 0

虚拟持仓的好处是编写简单,快速迭代策略更新,一般用于回测环境中,假设每一笔订单都完全成交,但在实际交易中常用的还是真实持仓。由于虚拟持仓是记录开平仓后的状态,所以需要定义成全局变量。

第 3 步:获取 K 线

python
exchange.SetContractType('rb000') # 订阅期货品种 bars_arr = exchange.GetRecords() # 获取 K 线数组 if len(bars_arr) < long + 1: # 如果 K 线数量过小 return

使用优宽量化的 SetContractType,传入"rb000"就可以订阅螺纹钢指数合约,但在回测和实盘中,是以螺纹钢指数为数据,使用具体的主力合约下单。接着使用 GetRecords 函数就可以获取螺纹钢指数的 K 线数据了。

注意:由于在计算乖离率时需要一定周期,所以为了避免程序出错,使用 if 语句过滤。

第 4 步:计算乖离率

python
close = bars_arr[-2]['Close'] # 获取上一根 K 线收盘价 ma1 = TA.MA(bars_arr, short)[-2] # 计算上一根 K 线短期均线值 bias1 = (close - ma1) / ma1 * 100 # 计算短期乖离率值 ma2 = TA.MA(bars_arr, long)[-2] # 计算上一根 K 线长期均线值 bias2 = (close - ma2) / ma2 * 100 # 计算长期乖离率值

根据乖离率计算公式,首先获取收盘价, 在这个策略中我们使用的是上一根 K 线收盘价,也就是当前 K 线信号成立,下根 K 线发单。接着使用优宽量化内置的 talib 库计算均线。

第 5 步:下单交易

python
global mp # 全局变量 current_price = bars_arr[-1]['Close'] # 最新价格 if mp > 0: # 如果持有多单 if bias2 <= bias1: # 如果长期乖离率小于等于短期乖离率 exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(current_price - 1, 1) # 平多单 mp = 0 # 重置虚拟持仓 elif mp < 0: # 如果持有空单 if bias2 >= bias1: # 如果长期乖离率大于等于短期乖离率 exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(current_price + 1, 1) # 平空单 mp = 0 # 重置虚拟持仓 elif mp == 0: # 如果无持仓 if bias2 > bias1: # 长期乖离率大于短期乖离率 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(current_price + 1, 1) # 开多单 mp = 1 # 重置虚拟持仓 elif bias2 < bias1: # 长期乖离率小于短期乖离率 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(current_price - 1, 1) # 开空 单 mp = -1 # 重置虚拟持仓

由于我们在 while 循环外部定义了一个全局变量 mp,用于接收当前的持仓状态,所以在使用这个变量的时候,需要先用 global 引入这个全局变量。另外还需要获取当前的最新价格用于开平仓。

7.3.7 完整策略

python
# 外部参数和全局变量 short = 10 long = 50 mp = 0 # 策略主函数 def onTick(): # 获取数据 exchange.SetContractType('rb888') # 订阅期货品种 bars_arr = exchange.GetRecords() # 获取 K 线数组 if len(bars_arr) < long + 1: # 如果 K 线数量过小 return # 计算 BIAS close = bars_arr[-2]['Close'] # 获取上一根 K 线收盘价 ma1 = TA.MA(bars_arr, short)[-2] # 计算上一根 K 线短期均线值 bias1 = (close - ma1) / ma1 * 100 # 计算短期乖离率值 ma2 = TA.MA(bars_arr, long)[-2] # 计算上一根 K 线长期均线值 bias2 = (close - ma2) / ma2 * 100 # 计算长期乖离率值 # 下单交易 global mp # 全局变量 current_price = bars_arr[-1]['Close'] # 最新价格 if mp > 0: # 如果持有多单 if bias2 <= bias1: # 如果长期乖离率<=短期乖离率 exchange.SetDirection("closebuy") # 设置交易方向和类型 exchange.Sell(current_price - 1, 1) # 平多单 mp = 0 # 重置虚拟持仓 elif mp < 0: # 如果持有空单 if bias2 >= bias1: # 如果长期乖离率>=短期乖离率 exchange.SetDirection("closesell") # 设置交易方向和类型 exchange.Buy(current_price + 1, 1) # 平空单 mp = 0 # 重置虚拟持仓 elif mp == 0: # 如果无持仓 if bias2 > bias1: # 长期乖离率大于短期乖离率 exchange.SetDirection("buy") # 设置交易方向和类型 exchange.Buy(current_price + 1, 1) # 开多单 mp = 1 # 重置虚拟持仓 elif bias2 < bias1: # 长期乖离率小于短期乖离率 exchange.SetDirection("sell") # 设置交易方向和类型 exchange.Sell(current_price - 1, 1) # 开空单 mp = -1 # 重置虚拟持仓 # 程序入口函数 def main(): while True: # 循环 onTick() # 执行策略主函数 Sleep(1000) # 休眠 1 秒

本节我们学习了乖离率的原理,以及使用乖离率构建了一个简单的交易策略。在实际交易中乖离率是一种简单有效的交易工具,能为交易者提供有效的参考。

7.4 网格策略

网格策略也称“渔网策略”,就行渔夫捕鱼一样,它是以某个价格为基准,在其上下分别设置价格线(撒网待鱼),每当价格触发价格线时,通过加减仓操作尽可能获利。网格策略属于左侧交易,不需要预测价格涨跌方向,不想右侧交易一样追涨杀跌,而是逆势而为,在价格下跌时买入,价格上涨时卖出。

7.4.1 网格设计原理

网格交易本质上是一种空间变时间的玩法,其秉持的原则是“仓位策略比择时策略更重要”。简单的网格是以某个价位为基准点,当价格上涨戓下跌一定的点数或者一定的比例,挂N手数量空单戓多单,每一个格子即是盈利点位,但通常并不设置止损,当价格朝向与持仓有利的方向运动并达到网格点位时获利平仓,并且在该点位挂同样的买单戓卖单。这样这些交易订单就像渔网一样阵列,在行情的波动中来回盈利。在设计网格之前,首先需要根据历史数据确定网格的上限和下限,然后根据品种的波动率情况设置网格的宽度,还要根据自己的资金实力设计网格的数量,网格数量越多,则需要加仓的点位就越多,以及计算好补仓资金份额,防止潜在的风险导致破网。

网格编号网格价格多头持仓空头持仓
050001
1495012
2490023
3485034
4480045
5475056
6470067
7465078
8460089
94550910
10450010

如上面的表格所示:网格的上限为5000元,下限为4500元,起始价格为5000元,网格的间距是50元,一共有10个网格,多头和空头最大的持仓量为10手。

7.4.2 策略实现

好的,让我逐个解释每个函数的逻辑:

  1. cancel_order()

    • 这个函数的目的是取消当前挂单。
    • 首先,它通过 exchange.GetOrders() 获取当前挂单列表。
    • 然后,对于每个订单,它获取订单详细信息,包括订单的价格,并根据订单的 Type 和 Offset 来确定订单的类型。
    • 接着,它使用 exchange.CancelOrder(order.Id) 取消订单。
    • 最后,输出取消订单的信息。
  2. trade(type, price, unit)

    • 这个函数用于执行交易操作。
    • 首先,根据传入的 type 参数设置交易方向。
    • 然后,根据交易方向执行相应的交易操作,使用 exchange.Buy()exchange.Sell()
  3. on_bar()

    • 这个函数是主要的交易逻辑。
    • 首先,它从交易所获取当前持仓和市场深度。
    • 如果没有市场深度数据,它将返回。
    • 接着,它获取盘口数据的 ask 和 bid 价格。
    • 如果当前没有持仓:
      • 如果最高买价高于预设的阈值 top,则执行卖出操作开空头。
      • 否则,执行买入操作开多头,这是底仓的操作。
    • 如果当前有持仓:
      • 如果持仓类型为空头:
        • 如果最低卖价低于预设的阈值 bottom,则执行平空头止盈操作。
        • 如果尚未达到空头盈利的标准,则要根据订单的情况:
          • 如果订单为空,挂卖单和平卖单的操作,价格分别是sell_line 和 buy_line,确保盈利。
          • 如果存在挂单,当判断挂单是空头的时候,取消挂单,然后进行网格减仓(向下移动)的操作;当判断是多头的时候,同样的取消挂单,进行网格加仓(向上移动)的操作。
      • 如果持仓类型为多头:
        • 基本的逻辑思路和空头一致,进行相反理解就可以。
  4. initialize()

    • 这个函数用于初始化策略。
    • 首先,它等待交易所的连接状态可用。
    • 然后,设置合约类型。
    • 最后,获取并记录初始的买入和卖出价格。
  5. main()

    • 这是主函数,负责启动策略。
    • 首先,调用 initialize() 进行初始化。
    • 然后,进入主循环,在每次循环中调用 on_bar() 执行交易逻辑,并通过 Sleep(1000) 控制循环频率。
python
def cancel_order(): orders = exchange.GetOrders() if orders and len(orders) > 0: for order in orders: # 获取订单信息 order_info = exchange.GetOrder(order.Id) # 获取订单类型描述 order_description = "多头开仓" if order_info['Type'] == 0 and order_info['Offset'] == 0 else \ "多头平仓" if order_info['Type'] == 1 and order_info['Offset'] == 1 else \ "空头开仓" if order_info['Type'] == 1 and order_info['Offset'] == 0 else \ "空头平仓" if order_info['Type'] == 0 and order_info['Offset'] == 1 else \ "未知交易类型" # 取消订单 exchange.CancelOrder(order.Id) # 记录撤销订单信息 Log(f"撤销挂单 价格:{order_info['Price']}, 类型:{order_description}") def trade(type, price, unit): exchange.SetDirection(type) if type == 'buy' or type == 'closesell': exchange.Buy(price, unit) elif type == 'sell' or type == 'closebuy': exchange.Sell(price, unit) def on_bar(): global buy_line, sell_line position = exchange.GetPosition() depth = exchange.GetDepth() if not depth: return ask = depth['Asks'][0].Price bid = depth['Bids'][0].Price if len(position) == 0: if bid > top: trade('sell', bid, unit * init_amount) Log(contract_code, '到达开空区域, 买入空头底仓') else: trade('buy', ask, unit * init_amount) Log(contract_code, '到达开多区域, 买入多头底仓') if len(position) != 1: return if position[0]["Type"] == 1: if ask < bottom: Log(contract_code, '空单全部止盈反手') trade('closesell', ask, position[0].Amount) else: orders = exchange.GetOrders() if len(orders) == 0: trade('sell', sell_line, unit) trade('closesell', buy_line, unit) if len(orders) == 1: if orders[0]["Type"] %2 == 1: #止盈成交 Log(contract_code, '网格减仓, 当前份数:', position[0].Amount) cancel_order() buy_line = buy_line - space sell_line = sell_line - space if orders[0]["Type"] %2 == 0: Log(contract_code, '网格加仓, 当前份数:', position[0].Amount) cancel_order() buy_line = buy_line + space sell_line = sell_line + space if position[0]["Type"] == 0: if bid > top: Log(contract_code, '多单全部止盈反手') trade('closebuy', bid, position[0].Amount) else: orders = exchange.GetOrders() if len(orders) == 0: trade('buy', buy_line, unit) trade('closebuy', sell_line, unit) if len(orders) == 1: if orders[0]["Type"] %2 == 0: Log(contract_code, '网格减仓, 当前份数:', position[0].Amount) cancel_order() buy_line = buy_line + space sell_line = sell_line + space if orders[0]["Type"] %2 == 1: Log(contract_code, '网格加仓, 当前份数:', position[0].Amount) cancel_order() buy_line = buy_line - space sell_line = sell_line - space def initialize(): global buy_line, sell_line while not exchange.IO("status"): Sleep(1000) while not exchange.SetContractType(contract_code): Sleep(1000) while True: ticker = exchange.GetTicker() if ticker: break Sleep(1000) buy_line = ticker["Last"] - space sell_line = ticker["Last"] + space Log('初始化网格', '#FF0000') def main(): initialize() while True: on_bar() Sleep(1000)

7.4.3 注意事项

任何策略是有适用范围的,网格策略也不例外,商品期货属于杠杆交易,尤其是网格交易有逆势加仓的特点,风险巨大!因此在使用网格策略交易商品期货时有两个建议:
1、只做多不做空,这样在买入时确定最大成本。因为商品本身自有价值属性,其归零的可能性几乎没有,所以导致商品易涨难跌,长期来看价格上涨空间大,下跌空间小。
2、在期货期权上买入一定张数的虚沽期权进行保护。因为买入虚沽期权是时间价值,同时卖出平值认沽进行时间价值的对冲。那么如果期货价格上涨,期权因为是时间价值对冲,所以不会亏钱,而期货的多单是能赚钱的;如果期货价格下跌,因为有期权虚沽,所以还能赚钱。
3、使用跨期或跨品种价差进行网格交易。假如单个品种的历史走势有很强的规律性,如鸡蛋品种常年在3500~4500之间震荡,但是这仅仅是历史数据,以历史回测的最大单边幅度去设计网格的承受极限都是不可靠的,历史总是会被打破,谁也不能保证其价格会不会走出长期趋势行情。那么安全期间,我们可以利用主力合约与次主力合约的价差或者有相关性的跨品种价差做网格策略,相比于单品种价差的波动是相对稳定的。

第 8 章 实盘级别策略编写和优化

前面章节我们集中于测试策略的讲解和编写,本节内容我们将专注于实盘级别策略。相对于Pine语言和My语言作为专业的金融语言,Python语言我们需要从底层编写架构应对实盘策略运行中可能出现的问题与解决办法。本章内容在首先介绍一些策略优化方法的基础上,后续为大家讲解一些实盘策略的编写思路和注意事项。

8.1 策略参数

相对于固定策略中的一些变量,我们可以设置外部参数的方式从而对策略进行更好的回测和调试。策略界面上设置的策略参数,在策略代码中是以全局变量形式体现的。Python策略的函数中修改全局变量、策略界面参数时需要使用global关键字。

image

8.1.1 界面参数种类

变量描述备注类型默认值说明
number数值类型备注数字型(number)1C++策略为浮点型。
string字符串备注字符串(string)Hello FMZ默认值输入时不需要加引号,输入均作为字符串处理。
combox下拉框备注下拉框(selected)1|2|3combox变量本身是数值,代表下拉框控件选择的栏目的索引,第一个下拉框栏目内容是1,其索引值是0,依次类推。
bool勾选项备注布尔型(true/false)true勾选上,变量bool为true,不勾选,变量bool为false。
secretString加密字符串备注加密串(string)passWord使用和字符串相同,加密字符串会被加密发送,不会明文传输。

image

  • 界面参数,在策略编辑页面代码编辑区下方策略参数区设置。
  • 界面参数在策略代码中是以全局变量形式存在的,也就是说可以在代码中修改界面参数。
  • 界面参数在策略代码中的变量名:即以上表格中的:number、string、combox、bool、secretString。
  • 描述选项:界面参数在策略界面上的名字。
  • 备注选项:界面参数的详细描述,该描述会在鼠标停留在界面参数上的时候相应地显示出。
  • 类型选项:该界面参数的类型。
  • 默认值选项:该界面参数的默认值。

界面参数在策略代码中的变量名不要设置为当前编程语言的保留字(关键字)。

8.1.2 参数依赖

变量描述类型默认值
numberA数值A数字型(number)2
isShowA是否显示numberA参数布尔型(true/false)false

可以设置一个参数,让另一个参数基于该参数的选择实现显示与隐藏。比如我们设置参数numberA,numberA是一个数值类型的界面参数。我们让numberA基于另一个参数:isShowA(布尔类型)的真假决定numberA显示与隐藏。

需要把numberA变量在界面参数上设置为:numberA@isShowA(策略编辑页面中策略参数栏,设置“变量”的控件中填写)。这样不勾选isShowA参数,numberA参数就隐藏了。

变量描述类型默认值
numberA数值A数字型(number)2
combox下拉框下拉框(selected)1|2|3

对于下拉框控件类型的参数,参数依赖部分为判断是否等于下拉框某个选项的索引值。同样以numberA参数为例,在参数设置变量时写为:numberA@combox==2。numberA参数就基于combox参数是否选择为第三个选项3来进行显示或隐藏(索引0对应第一个选项,索引1对应第二个选项,索引2对应第三个选项)。

8.1.3 参数分组

变量描述类型默认值
numberA(?第一组)数值A数字型(number)2
isShowA是否显示numberA参数布尔型(true/false)false
combox(?第二组)下拉框下拉框(selected)1|2|3

在开始分组的参数的描述(策略编辑页面中策略参数栏,设置“描述”的控件)开头加上(?第一组)前缀即可设置从该参数开始向下的参数(直到遇到其它分组结束)归纳于命名的“第一组”这个分组中。

8.1.4 保存参数设置

在回测时如果希望将策略参数保存,可以在策略参数修改后点击「保存回测设置」按钮。运行实盘时需要保存实盘配置的参数数据,可以点击策略实盘页面中「参数设置」选项,再点击「导出参数」按钮,导出的策略参数将以json文件保存。

导出的策略参数配置也可以再次导入实盘,点击「导入参数」按钮即可把保存的策略实盘参数导入到当前实盘,导入后点击「更新参数」按钮保存生效。

8.1.5 参数调参

参数的调优是使用指标函数时非常重要的一步。在实际操作中,不同的交易市场和品种都有自己的特点,因此同样的指标函数,在不同的市场和品种中可能需要不同的参数设置,以达到最佳的效果。这也是为什么在量化交易中,参数的调优显得尤为重要。通过对历史数据进行分析和模拟回测,可以找到最适合当前市场的参数设置,提高交易策略的成功率和盈利水平。

下面这段代码中,我们设置可调参数为布林带周期'period'和布林带宽度'width',在模拟回测界面我们设置参数调优的范围和步伐,接着我们就可以获取最优的参数设置。这里我们也可以设置优化过滤器,更好的设置不同参数的范围,防止无效的调参。

python
def main(): while True: exchange.SetContractType('rb888') r = exchange.GetRecords() if len(r) < period + 1: continue boll = TA.BOLL(r, period, width) # 计算布林带指标 upLine, midLine, downLine = boll[:3] # 获取布林带的上轨、中轨和下轨数组 upPrice, midPrice, downPrice = upLine[-2], midLine[-2], downLine[-2] # 获取上上根K线的上轨、中轨和下轨价格 recclose = r[-1].Close # 获取上根K线的收盘价 posInfo = exchange.GetPosition() # 开多条件:无持仓,并且收盘价大于上轨 if not posInfo and recclose > upPrice: exchange.SetDirection("buy") # 设置下单方向为买入 exchange.Buy(recclose, 1) # 以收盘价开一手多头仓位 # 开空条件:无持仓,并且收盘价小于下轨 if not posInfo and recclose < downPrice: exchange.SetDirection("sell") # 设置下单方向为卖出 exchange.Sell(recclose, 1) # 以收盘价开一手空头仓位 # 平多条件:持多头仓位,并且收盘价小于中轨 if posInfo and (posInfo[0].Type == PD_LONG or posInfo[0].Type == PD_LONG_YD) and recclose < midPrice: exchange.SetDirection("closebuy") # 设置下单方向为平多 exchange.Sell(recclose - 5, 1) # 以收盘价减5作为限价平多仓位 # 平空条件:持空头仓位,并且收盘价大于中轨 if posInfo and (posInfo[0].Type == PD_SHORT or posInfo[0].Type == PD_SHORT_YD) and recclose > midPrice: exchange.SetDirection("closesell") # 设置下单方向为平空 exchange.Buy(recclose + 5, 1) # 以收盘价加5作为限价平空仓位 Sleep(1000 * 60) # 休眠一分钟,等待下一个K线数据

image

根据对布林带周期'period'和布林带宽度'width'参数调试的结果,可以发现当布林带周期为24,布林带宽度为2.25的时候,我们取得了最优的收益。需要注意的是,最优参数可能会陷入过拟合的风险,所以并不是一直有效的。我们需要该参数进行更多样本外的测试验证参数的有效性。

image

8.2 策略交互设置

在策略运行过程中,如果我们想实时调整策略参数,获取状态变量等交互的操作,我们可以交互控件来实现。下面我们来看下交互控件怎样设置。

和策略参数一样,我们可以根据需要进行不同类型交互控件参数的设置。策略交互控件具有五种类型,数字,布尔,字符,下拉框和按钮。我们举例示范下,我们可以设置一个按钮型,命名为action,描述为一个操作;设置一个数值型,actionnumber,来定义指定操作的数量;布尔型ifaction,是或者否的选择;selected,下拉框,进行多项的选择;str,字符型,可以输入字符;在交互预览页面,我们就可以看到交互控件呈现的页面。

image

交互控件设置完成以后,在策略中我们通过全局函数GetCommand(),获取交互命令字符串。GetCommand()函数会获取策略交互界面发来的命令并清空缓存,没有命令就会返回空字符串。返回的命令格式为按钮名称加上参数,如果交互控件没有参数(例如不带输入框的按钮控件),那么命令就是按钮名称。

交互控件是在策略运行时才可以使用的,这就意味着在回测中,交互控件是不支持的。因此,首先我们创建实盘,定义策略标签,选择托管主机和运行策略,定义策略周期,添加交易平台,这样实时运行的实盘就创建成功了。我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。

这里我们先不设置控件需要进行的操作,只是使用Log函数打印出来控件对应的操作,如果需要打印参数的话,使用相应的索引。

python
def main(): cmd = GetCommand() if cmd: Log("cmd:", cmd) arr = cmd.split(":") if arr[0] == "action": Log("操作,该控件不带数量") elif arr[0] == "actionnum": Log("操作,该控件带数量:", arr[1]) elif arr[0] == "ifaction": Log("是否进行操作", arr[1]) elif arr[0] == "selected": Log("下拉框", arr[1]) elif arr[0] == "str": Log("字符型", arr[1])

我们在实盘中运行一下这个控件测试代码,点开策略交互,显示出来我们刚才设置的五个按钮,我们分别点击一下。可以在日志信息中打印出来两个按钮的名称,如果有默认参数,就会附带上。按钮名称和按钮参数中间使用“:”进行分隔。如果我们只想打印参数,可以使用split函数。这里我们先不设置控件需要进行的操作,只是使用Log函数打印出来控件对应的操作,如果有数量的话,再加上数量。可以看到,在实盘里,我们点击action按钮,会出现“买入,该控件不带数量”;点击actionnum,并设置数量2,会打印“操作,该控件带数量:2” 。其他的按钮大家可以自己修改探索一下。关于具体的使用场景,我们下面展开介绍。

image

8.2.1 半自动化交易

我们来看第一个应用场景:半自动化交易。半自动化交易是一种结合量化交易和人工决策的交易方式。在半自动化交易中,交易者使用计算机程序或交易平台上的工具和功能来计算交易信号,但最终的交易操作需要交易者自己做出。交互控件在半自动化交易活动中尤其友好。半自动化交易需要快速决策和执行,交互控件的快速选择交易品种功能非常有用。通过简洁直观的界面设计,交易者可以迅速浏览可交易的品种列表或使用搜索功能快速找到所需的品种。

交互控件还允许交易者预设常用的交易参数,如数量、价格、止损止盈等。预设参数功能极大地简化了操作流程,我们只需在初始设置时输入一次参数,之后就能够随时使用这些预设参数进行快速下单,提升了下单的速度和效率。同时,交互控件还具备错误提示和确认功能,为半自动化交易提供了额外的安全保障。在输入有误或存在潜在风险时,交互控件可以及时提醒并帮助纠正错误。此外,在交易最终执行前,交互控件要求交易者再次确认交易指令,以确保下单操作的准确性和安全性。这有助于半自动化交易避免因输入错误或误操作而造成的交易错误,进一步减少了风险。总体而言,交互控件为半自动化交易提供了便利、效率和安全性。

python
def main(): contractlist = ['i2509', 'rb2509', 'hc2509'] p = ext.NewPositionManager() while True: if exchange.IO("status"): LogStatus(_D()) cmd = GetCommand() if cmd: Log("cmd:", cmd) arr = cmd.split(":") if arr[0] == "selected": contract = contractlist[int(arr[1])] Log("买入期货品种", contract) elif arr[0] == "buy": p.OpenLong(contract, 1) elif arr[0] == "covernum": pos = p.GetPosition(contract, PD_LONG) if pos is None or pos.Amount < int(arr[1]): Log('没有足够的多仓可平') else: Log("平多仓数量", arr[1]) p.Cover(contract, int(arr[1])) Sleep(1000) else: LogStatus(_D(), "未连接CTP!") Sleep(500)

这里我们举一个例子来示范使用交互控件来选择不同的期货合约,并进行开多仓和平多仓操作。这段代码首先创建一个合约列表 contractlist,其中包含了三个期货合约代码:'i2509', 'rb2509', 'hc2509'。使用交易类库,创建一个新的单品种控制对象 p。接着获取用户输入的命令,并将其存储在变量 cmd 中。如果存在命令 (cmd),则执行以下逻辑:记录命令信息到日志,输出命令内容。并将命令内容用冒号分割成一个数组 arr,这样为了获取不同命令的参数。如果命令的第一个元素是 "selected",表示选择了某个期货品种,根据命令的对应元素从合约列表中获取对应的合约代码,打印到日志中。

如果命令的第一个元素是 "buy",表示买入期货品种,使用单品种控制对象 p 执行开多仓的操作,买入数量为1。如果命令的第一个元素是 "covernum",表示平多仓,首先通过 p 获取当前持仓的信息,如果持仓量为空或者持有的多仓数量不足,则输出 "没有足够的多仓可平";如果持有足够的多仓,先打印出来需要平多仓的数量,并使用cover执行平多仓的操作,平仓数量为命令的第二个元素。这里需要注意的是我们获取的arr[1]是一个字符型,需要转换为数值型。关于每一个控件对应的操作我们就设置完成了,接下来我们要在实盘中查看一下控件的运行。

我们在实盘中,首先选择期货品种,日志信息显示选择成功,然后点击buy,点击一次开一手多仓,点击两次,开两手;然后我们使用covernum进行平仓,这里我们首先填写数字3,因为我们目前持有的多仓数目为2,所有会显示没有足够的多仓可平,我们改为数字1,日志信息中呈现我们平了一手多仓。这就是使用交互控件进行交易的一个简单的例子。上面的代码示例演示了如何在策略中使用交互控件来实现期货交易的功能。通过设置不同类型的交互控件,我们可以提供给交易者一个直观、灵活的交互界面。用户可以通过点击按钮、选择下拉框等方式来输入指令,然后策略根据接收到的指令执行相应的交易操作。

请注意,这里只是为了展示控件的使用,还有很多不完善的地方。对于半自动化交易,使用交互控件可以使交易过程更加友好和灵活。通过在代码中集成交互控件,你可以与机器人进行交互,实现对交易的监控和操作,并根据需要进行自定义设置。例如,你可以添加一个交互控件来设置交易的执行时间,让用户选择在何时执行买入或平仓操作。这样,你可以根据市场的波动和行情状况,智能地调整交易时机,以提高交易的成功率和盈利能力。

8.2.2 实时修改参数

在前面的课程中,我们介绍了策略参数的使用,主要针对于策略运行中的不同类型的参数进行控制。策略参数在策略运行中是不能被改动的,如果需要改动,则必须停掉策略修改参数才可以;而有的同学如果希望在交易过程中对策略实时的进行一定程度的控制和修改,交互控件可以帮助你完成这一目标。让我们举一个例子试试看。假如你的日内策略是使用20秒均线作为基准。然而由于美联储加息,你预感今天市场波动比较剧烈,所以你希望改变你的策略,使用10作为参考基准。如果你不设置交互控件,改变参数需要你停掉实盘,修改参数,然后重启实盘;在美联储加息平稳后,重新修改均线参数为20。而如果你使用交互控件,这一个问题变得简单了起来。

首先在策略交互中设置一个交互控件period为数值型,默认值是20。回到代码,默认的period为20,因此策略在起始阶段,会使用默认周期为20,然后判断k线的数量是否满足计算均值,然后打印“当前周期为:20”,输出20周期的均值;然后在代码中书写交互控件控制语句,当点击交互控件修改period周期以后,就会调整均线计算的周期,然后打印出来对应period的值,进行新的period均线的计算。

python
def main(): period = 20 while True: if exchange.IO("status"): LogStatus(_D(), "已连接CTP!") exchange.SetContractType('rb888') bars = exchange.GetRecords() Log('当前周期为:', period) if not bars or len(bars) <= period: continue base = TA.MA(bars, period) Log('均线period: ', base[-1]) cmd = GetCommand() if cmd: Log("cmd:", cmd) arr = cmd.split(":") period = int(arr[1]) Log('修改周期为:', period, '#FF0000') Sleep(1000) else: LogStatus(_D(), "未连接CTP!") Sleep(500)

我们在实盘中看下,为了快速呈现结果,我们使用秒为周期,策略开始,日志输出的是20周期的均线,这里我们修改一下,周期变为10,可以看到日志信息中开始输出10周期的均线。

这就是实时修改参数的一个小例子,确实实现了不需要停掉实盘,重新修改策略的参数。这种实时修改参数的技术可以帮助你根据市场情况、风险偏好等因素进行灵活的调整,从而优化和改进你的交易策略。与停止机器人并重新编译代码相比,它提供了更方便快捷的方式来调整参数,同时也可以避免中断实盘交易。但需要注意的是,在实时修改参数时,要确保对参数的修改是合理和稳妥的。过于频繁或不恰当的参数调整可能会导致策略性能下降或产生意外的结果。因此,在使用这类功能时,建议谨慎评估和测试相关参数的变化对策略表现的影响,并确保在真实环境下具备足够的安全保障和风险控制措施。

8.2.3 实时调试功能

量化策略的实时调试功能可以帮助你在开发和优化策略过程中快速验证和调整策略逻辑,以及观察策略的实际表现。在策略运行过程中,你可以插入调试语句,查询关键变量、指标和决策点的值,这样就可以查看日志并分析策略逻辑的执行路径,以找出潜在的问题和改进点。结合exec()函数,交互控件可以实现实时调试功能。exec()可以执行任意语句(包括赋值、函数定义等),这样就相当于给策略一个后门,你可以随时调用语句修改,或者查询策略的状态。当然这要在确保没有异常的情况下。

这里涉及到了交互控件的字符型控件。首先我们设置这个交互控件。定义为字符型。回到代码主体,我们首先设置价格和数量都为0;在主函数中,首先打印初始的价格和数量;然后获取界面交互控件的消息。在判断有交互信息的时候,输出调试代码并打印出来。

控件代码的运行用到了try和except语句。try和except常常被用于异常处理。try关键字:try关键字标记一个代码块,表示要检测异常的范围。在这个代码块中,你可以放置可能会抛出异常的代码。这里我们执行exec函数。在这里,exec传入的参数是一个字符串形式的代码。except关键字:如果在try中的代码执行过程中发生了异常,控制流将会跳转到except块中。except块用于捕获并处理异常。e参数:在except块中,e是一个表示异常的变量。通过使用e,你可以访问到抛出的异常对象,从而获取异常的相关信息。整体来说,当exec(code)执行时,如果调试代码发生了异常,程序将会跳转到except块,except块中的代码将被执行。通过捕获异常并输出错误信息,你可以对异常进行处理,例如打印错误日志或采取其他适当的措施。

python
price = 0 amount = 0 def main(): Log("初始 price:", price, "初始 amount", amount) while True: cmd = GetCommand() if cmd: code = cmd.split(':', 1)[1] Log("执行调试代码:", code) try: exec(code) except Exception as e: Log("Exception", e) Sleep(1000)

我们在实盘中进行测试,启动实盘,可以看到首先打印出来原始设置的价格和数量,都为0。点击控件进行调试,在这里修改控件语句,设置新的价格和数量信息:

python
price = 100; amount = 1;

现在我们查看下新的价格和数量是多少,日志里是不会呈现的,继续使用控件语句查看我们的修改是否成功:

python
Log(price, amount)

可以看到日志信息打印出来了新的价格和数量。我们也可以设置一个错误语句试试看:

python
log(price, amount)

这里使用了小写的log,是一个错误的语句。所以会抛出异常Exception name 'log' is not defined。这就是一个简单的在线调试功能,但需要谨慎使用,因为存在安全风险。在上面的代码中,通过exec(code)执行了传入的代码,如果代码有异常,则会被except块捕获,并输出异常信息。虽然exec()函数提供了便利的功能,但由于它可以执行任意调试代码,所以可能会被滥用和攻击。你可以实现更多的功能,大家可以挖掘下。

8.2.4 状态栏交互功能

如果你认为交互控件的界面太过于死板,没关系,我们还可以定制个性化多功能交互控件页面,这里可以使用状态栏制定多功能交互面板。通过连接交互控件功能,我们就可以在状态栏进行我们需要的交互操作。

我们举一个比较粗糙的例子,在状态栏进行开仓和平仓的操作。

python
import json def main(): p = ext.NewPositionManager() while True: tbl = { "type": "table", "title": "状态栏交互", "cols": ["操作", "按钮"], "rows": [ ["开仓操作", {"type": "button", "cmd": "open", "name": "开仓", "input": {"name": "开仓数量", "type": "number", "defValue": 1}}], ["平仓操作", {"type": "button", "cmd": "coverAll", "name": "全部平仓"}] ] } log_msg = f"{_D()}\n`{json.dumps(tbl)}`" LogStatus(log_msg) while not exchange.IO('status'): Log('未连接CTP') Sleep(1000) cmd = GetCommand() if cmd: arr = cmd.split(":") if arr[0] == "open": Log('收到开仓指令') p.OpenLong('rb2409', int(arr[1])) elif arr[0] == "coverAll": Log('收到平仓指令') p.CoverAll()

状态栏定制首先我们需要一个用于描述表格的对象tbl,这涉及到了状态栏于展示交互的功能,我们设计的很简单:

  • title: 表格的标题,这里是 "状态栏交互"。
  • cols: 表格的列名数组,这里是 ["操作", "按钮"]。
  • rows: 表格的行数据数组,每一行是一个数组,包含相应的单元格数据。

在这段代码中,tbl 定义了一个简单的表格,包括两列和两行。第一列名为 "操作",第二列名为"按钮"。第一行行名是开仓操作,包含一个按钮,点击按钮时触发的命令是 "open",按钮的名称为"开仓",这里设置了一个input,需要用户手动输入开仓数量。第二行行名是平仓操作,也包含一个按钮,点击按钮时触发的命令是 "coverAll",按钮的名称为 "全部平仓",这个按钮不需要提供其他的输入。接下来我们在LogStatus中,通过这行代码输出表格对象的结构和内容。交互控件的操作设置就是我们上述讲过的内容。

我们在实盘中看下,在状态栏里,可以看到点击open,填写数量,我们就可以开对应数量的多仓,然后点击coverall就可以全部平仓。这里只是一个粗糙的展示,通过自定义的状态栏,我们可以进行更多分页,比如化工类,黑色系和农产品,操作可以有开仓,平仓和止盈等。这些功能的设计我们都可以实现。总之,交互控件为交易提供了更多的灵活性和可操控性,使得交易变得更加智能化和个性化。通过合理地设计和使用交互控件,大家可以实现更灵活的量化交易,提高交易的便捷性和效率。

image

8.3 策略进度恢复

在先前的课程中,我们讲解的策略都是运行在模拟回测系统中,策略直接出来回测结果,不受外部物理条件的干扰,是一种比较理想化的策略运行状态。然而,在实际实盘运行中,当我们部署托管者在自己的电脑上时,突发停电或者断网会造成托管者停止运行;而部署在云服务上的托管者,也有一定短暂宕机故障造成托管者程序停止。因此,怎样处理这种突发的情况,是我们在进行策略设计的时候需要关注的部分。量化交易中的策略进度恢复是指在遇到系统故障、网络中断或其他异常情况导致交易程序中断时,恢复并继续执行交易策略的过程。策略进度恢复是保证交易系统的稳定性和连续性的关键环节。

策略进度恢复的重要性不言而喻。在量化交易中,每一次交易决策都可能产生利润或者亏损,因此无论是策略还是交易状态的丢失都可能导致损失。策略进度恢复能够保证交易系统的连续性和稳定性,避免因系统故障而产生额外的风险和错误交易。同时,对于高频交易或需要实时响应市场变化的策略来说,策略进度恢复更加重要,它能够尽快将系统恢复到正常运行状态,减少交易信号的延迟和错过交易机会的风险。优宽量化作为专业的量化交易平台,非常重视策略进度恢复的设计和实现。而作为专业进阶的量化交易人,这些问题也需要未雨绸缪。相对于Pine语言和麦语言,这种高度封装的交易语言,可以自动实现策略的进度恢复,而JavaScript语言和Python语言作为从底层搭建的量化系统,需要我们手动实现策略的进度恢复。因此,本节课程我们将讲解Python语言的策略进度恢复的设计。

8.3.1 _G(K, V)函数的使用

首先,来介绍我们的策略进度恢复的好帮手,_G函数。作为一个优宽量化平台的内置函数,它的数据结构为KV表,K必须为字符串,它不区分大小写,V可以为任何可以序列化的内容。它可以永久保存在本地文件,所以该函数实现了一个可保存的全局字典功能。它在回测和实盘中都是支持的。在模拟回测系统中,回测结束后,_G函数保存的数据会被清除。而在实盘系统中,每个实盘单独一个数据库,重启或者托管者退出后,_G函数保存的数据是一直存在的。所以它的使用很灵活,可以放入任何我们想要储存的数据,状态变量和持仓状态等。

我们举例示范一下。这里由于不使用接口获取数据的测试,就不需要使用exchange.IO("status")函数判断连接状态,也不用设置合约代码,因为这里仅仅是测试_G函数。这里我们测试_G函数依次保存值为数字,字符串和列表,然后我们可以清空一下,再次读取会返回None值;在实盘运行中当调用_G()函数并且不传任何参数时,_G()函数返回当前实盘的ID。

python
def main(): _G("num", 666) # 存储数字 _G("num", "ok") # 存储字符串 _G("num", [1,2,3]) # 存储列表 Log(_G("num")) # 读取存储值 _G(None) # 清空存储值 Log(_G("num")) # 返回None Log(_G()) # 返回实盘ID }

8.3.2 持仓状态的恢复

在了解完_G函数以后,我们来进行持仓状态恢复的尝试。持仓状态恢复的重要性在于确保交易系统的连续性和准确性。当交易程序中断时,在程序化策略中持仓信息可能会丢失,造成持仓信息和实际的仓位不一致的状况,这可能导致交易系统的错误决策和风险暴露。通过持仓状态恢复,我们可以将中断前的持仓信息重新加载到交易系统中,确保交易系统在恢复后能够基于正确的持仓信息进行进一步的决策和风险管理。此外,持仓状态恢复还对于交易流程的完整性和可追溯性至关重要。通过持仓状态的恢复,我们可以准确地记录交易系统的行为和决策过程,使得后续的回测、风险控制和复盘等工作能够进行。

下面我们我们举例示范一下,布林带策略的持仓状态恢复的设计,我们这里举例示范的交易策略都很简单,重点是让大家可以了解在实盘运作中出现问题,我们解决的思路应该怎样进行。相信各位聪明的小伙伴们也许还有更好的解决方法,如果有兴趣的话,大家可以分享到我们优宽量化平台的文库或者社区板块,供大家一起学习。

本策略当中,我们使用虚拟持仓变量代表持仓状态变量,在策略中断的时候,这个虚拟持仓变量会丢失,这样可能会造成实际仓位和虚拟持仓的不符,造成策略的运行冲突,出现错误。所以呢,我们针对上述的问题,可以提出解决方案。在策略初始阶段,使用_G函数获取持仓状态变量,这个方案可以在策略出现意外中断以后,恢复策略状态,继续策略的运行。

我们可以进行下面具体的设置。首先固定程序,连接交易所,获取k线数据,计算布林带指标。然后根据布林带指标和持有的仓位进行开平仓的操作。这里设置虚拟持仓变量mp,代表记录的持仓状态,在策略运行过程中,mp变量是不断更新的,我们需要及时的进行保存和获取。

在策略第一次运行的时候,我们直接获取持仓状态变量getPos,这个时候,_G函数的键值对还没有定义,因此返回的值会是一个空值,所以我们使用一个逻辑判断,当持仓状态变量存在,直接读取为保存值;否则当为空值的时候,保存getPos是0。这样就完成了保存变量的创建和获取。将它赋值为mp变量,接下来我们进行交易的操作。在每次操作完毕后,重新定义mp变量;最后不要忘了使用_G函数保存新的持仓状态变量。我们在实盘中测试下,可以看到策略开始的时候,getPos变量进行创建,而伴随交易的操作,getPos变量不断更新;接下来我们手动停掉实盘看下,然后重新打开,可以看到getPos的变量没有重置,依旧是实盘停掉之前的状态,策略可以继续运行。

python
def main(): p = ext.NewPositionManager() symbol = 'FG401' if _G('getPos'): Log('已存在持仓变量:','#00FF00') else: _G('getPos', 0) Log('未存在持仓变量,创建持仓变量', '#FF0000') mp = _G('getPos') while True: if exchange.IO("status"): LogStatus(_D(), "已经连接到CTP!") exchange.SetContractType(symbol) records = exchange.GetRecords() if not records or len(records) < 20: Sleep(1000) # 等待获取足够多的K线数据 continue # 获取布林带指标 boll = TA.BOLL(records, 20, 2) upLine = boll[0][-3] # 上轨数值 midLine = boll[1][-3] # 中轨数值 downLine = boll[2][-3] # 下轨数值 recclose = records[-2].Close # 上根K线收盘价 if mp == 0 and recclose > upLine: # 如果无持仓,并且收盘价大于上轨,开多 p.OpenLong(symbol, 1) mp = 1 if mp == 1 and recclose < midLine: # 如果持多,并且收盘价小于中轨,平多 p.Cover(symbol) mp = 0 if mp == 0 and recclose < downLine: # 如果无持仓,并且收盘价小于下轨,开空 p.OpenShort(symbol, 1) mp = -1 if mp == -1 and recclose > midLine: # 如果持空,并且收盘价大于中轨,平空 p.Cover(symbol) mp = 0 _G('getPos', mp) # 更新持仓状态 Log('更新持仓状态:', _G('getPos')) Sleep(5000) else: LogStatus(_D(), "未连接CTP!") Sleep(1000)

以上的就是_G进行持仓状态保存和恢复的样例展示,其实在实盘的代码当中,我们使用的都是GetPosition函数获取实际的仓位信息,然后和保存的持仓变量进行对比。大家可以尝试改写一下上面的代码。这就是我们策略进度恢复中,持仓状态恢复的一个简单的设计。关于其他状态变量,以及实盘数据的保存和恢复,相信大家可以在此基础上进行修改和完善,如果大家哪里遇到问题,大家可以及时反馈,我们会热心解答。

8.4 多品种合约回调策略设计

多品种(多个期货合约)量化策略是指同时在多个不同的期货合约上应用的量化交易策略。这种策略基于对多个期货品种的市场行情数据进行分析和建模,用来制定交易信号和执行交易。

多品种量化策略的优点如下:

  • 分散风险:通过在多个期货品种上分散投资,降低了单一品种风险对整体投资组合的影响。当一个品种的价格波动较大或遭受损失时,其他品种可能表现更好,从而减少整体风险。
  • 增加机会:不同的期货合约在市场中具有不同的特点和波动性。通过同时研究并交易多个期货合约,可以捕捉到更多的交易机会,并且在市场变动时能够快速做出反应。
  • 提高收益稳定性:由于不同期货品种之间的相关性通常较低,因此,在一个品种表现不佳的时候,其他品种可能仍然能够带来正向收益,从而提高整体收益的稳定性。
  • 顺势交易:多品种策略可以根据不同期货合约的趋势和走势进行交易。当一个品种的趋势明显时,可以选择在该品种上建立仓位,从而跟随市场的走势而获取收益。
  • 套利机会:多品种量化策略可以通过不同期货合约之间的套利机会来获得利润。例如,通过同时买入一个合约并卖出另一个合约,从价差中获取利润。

8.4.1 多品种策略架构介绍

多品种策略设计的优点在于使用方便,一个策略程序控制交易多个品种,可以统一信息状态显示。交易多个品种相对分散了风险,增加了交易机会。缺点在于设计比较复杂,各个品种之间不能相互影响,对程序执行效率要求比较高。所以设计难度远大于设计一个单品种策略。优宽量化交易平台上提供了大量策略范例,给我们提供了丰富的参考代码,设计思路。需要注意的是,多品种(多个期货合约)量化策略也面临一些挑战,如数据处理和模型构建复杂性增加、风险控制的难度提高等。因此,在设计和实施多品种量化策略时,需要充分考虑到市场特点、投资者风险承受能力和相关技术工具的支持。

多品种策略架构可以分为轮询设计和事件驱动的设计。轮询设计架构比较直观,它是基于不断循环合约列表,然后在检查到该品种最新的走势满足交易信号的时候,进行相应的交易操作,这样虽然易于设计,但是并不能实现多品种交易的并发执行。因为这是一个串联的模式,一次只能一个合约的信号判断和交易操作;如果在处理A合约的时候,B合约的信号现实触发的话,程序是无法探测并执行交易的。虽然这对于趋势的策略确实影响不大,但是对于高频策略,如果错过相应的信号触发,就不满足策略设计的初衷了。而基于事件驱动的设计,采样并发处理机制,能够显著提高多品种策略的执行效率和信号响应速度。事件驱动的设计模式非常适合处理高频交易策略,因为它可以确保在极短的时间内对多个品种进行快速反应。此外,它还可以有效地管理资源,因为每个合约的数据处理是独立的,不会相互阻塞。这种设计允许策略在保持高效率的同时,也能够灵活地处理各种市场情况。

因此本节课,我们从策略设计层面入手,剖析一个多品种合约回调策略设计,学习一些策略架构设计的经验。对于一个多品种合约的回调策略设计,我们首先需要了解行情的推送模式。在优宽量化平台,对于行情模式,可以使用mode参数进行切换:

  • exchange.IO("mode", 0):立即返回模式,如果当前还没有接收到交易所最新的行情数据推送,就立即返回旧的行情数据,如果有新的数据就返回新的数据。
  • exchange.IO("mode", 1):缓存模式(默认模式),如果当前还没有收到交易所最新的行情数据(同上一次接口获取的数据比较),就等待接收然后再返回,如果调用该函数之前收到了最新的行情数据,就立即返回最新的数据。
  • exchange.IO("mode", 2):强制更新模式,进入等待一直到接收到交易所下一次的最新推送数据后返回。

在同时,可以使用wait参数,设置阻塞:exchange.IO("wait", Timeout),通过这样的设置,在当前交易所有任何品种更新行情信息,或订单成交时才返回信息,可带第二个参数(代表毫秒数)指定超时,超时会返回空值,正常返回EventTick/OrderEvent结构。通过结合exchange.IO("mode", 0)函数使用,这样配合使用就可以使程序在有最新行情时进行响应,执行程序逻辑,这样的目的,是为了在程序中使用exchange.GetTicker()等函数调用时不阻塞)。如果Timeout参数设置为-1,该函数设置成为了立即返回模式,在没有新事件的时候,返回空值,如果Timeout参数设置为0,代表阻塞等待最新事件。需要注意的是在使用exchange.IO("wait")时,必须至少已经订阅了一个当前处于交易状态的合约。另外需要注意的是,该函数只支持商品期货实盘。

EventTick:{Event:"tick", Index:交易所索引, Nano:事件纳秒级时间, Symbol:合约名称, Ticker:行情数据}。
OrderTick:{Event:"order", Index:交易所索引, Nano:事件纳秒级时间, Order:订单信息}。

8.4.2 多品种行情回调示范

我们举例一个多品种行情回调的例子示范下:这里我们设置的多品种合约是黑色系类,包括热卷,螺纹钢,和铁矿石,然后设置wait函数,参数填写为-1,代表立即返回,因此当检查到最新的tick信息的时候,就是e.Event == "tick",该函数会执行on_tick函数,这里我们设置的on_tick函数是在我们初始设置的空图表中,不断的填充对应品种的最新10条的tick数据,然后使用LogStatus进行展示。

我们在实盘中运行下,可以看到由于我们设置了exchange.IO("wait", -1),是立即返回模式,如果有新的信息就立即更新,在没有新事件时返回空值。我们在实盘中可以看到,三个品种的信息不是伴随轮询,一条条逐渐更新的,而是实时更新的,实现了多品种行情的并联展示。这对于交易决策和监控多个品种的交易机会非常有益。

python
import json def on_tick(symbol, ticker, rTbl): rTbl[symbol]["rows"].append([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]) if len(rTbl[symbol]["rows"]) > 10: rTbl[symbol]["rows"].pop(0) def main(): symbols = ["hc2409", "rb2409", "i2409"] rTbl = {symbol: { "type": "table", "title": symbol, "cols": ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume", "OpenInterest", 'Symbol'], "rows": [] } for symbol in symbols} while not exchange.IO("status"): Sleep(1000) for symbol in symbols: _C(exchange.SetContractType, symbol) exchange.IO("mode", 0) while True: e = exchange.IO("wait", -1) if e and e.Event == "tick": on_tick(e.Symbol, e.Ticker, rTbl) LogStatus(json.dumps(list(rTbl.values())))

8.4.3 多品种回调策略设计

下面我们就来举一个实例示范下,多品种合约的回调策略设计。上面的例子我们只使用到了EventTick数据的事件驱动的更新,这可以作为交易信号的判断,而交易操作,还需要另外一种状态变量的辨别,就是实时订单状态的获取,我们可以通过OrderEvent数据结构进行获取。

python
import json global ticker_lists, symbol_pos, rTbl, orderTbl symbols = ["hc2409", "rb2409", "i2410"] cols = { "rTbl": ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume", "OpenInterest"], "orderTbl": ['ContractType', 'Id', "StatusMsg", 'Price', 'Status', 'Type', 'Offset', 'OrderSignal'] } rTbl = {symbol: {"type": "table", "title": symbol, "cols": cols["rTbl"], "rows": []} for symbol in symbols} orderTbl = {symbol: {"type": "table", "title": symbol, "cols": cols["orderTbl"], "rows": []} for symbol in symbols} ticker_lists = {symbol: [] for symbol in symbols} symbol_pos = {symbol: 0 for symbol in symbols} def on_tick(ticksymbol, ticker): ticker_list = ticker_lists[ticksymbol] ticker_list.append(ticker.Last) tick_tbl = rTbl[ticksymbol] tick_tbl["rows"].append([_D(ticker.Time/1000), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest]) if len(ticker_list) < 100: return ma_long = TA.MA(ticker_list, 100)[-1] ma_short = TA.MA(ticker_list, 50)[-1] posInfo = exchange.GetPosition() if ticker.Last > ma_long and symbol_pos[ticksymbol] == 0: symbol_pos[ticksymbol] = 999 q.pushTask(exchange, ticksymbol, "buy", 1, lambda task, ret: Log(task["desc"])) if ticker.Last < ma_long and symbol_pos[ticksymbol] == 0: symbol_pos[ticksymbol] = 999 q.pushTask(exchange, ticksymbol, "sell", 1, lambda task, ret: Log(task["desc"])) if ticker.Last < ma_short and symbol_pos[ticksymbol] == 1: symbol_pos[ticksymbol] = 999 q.pushTask(exchange, ticksymbol, "closebuy", 1, lambda task, ret: Log(task["desc"])) if ticker.Last > ma_short and symbol_pos[ticksymbol] == -1: symbol_pos[ticksymbol] = 999 q.pushTask(exchange, ticksymbol, "closesell", 1, lambda task, ret: Log(task["desc"])) def on_order(order): Log(order) ordersymbol = order.ContractType order_tbl = orderTbl[ordersymbol] if symbol_pos[ordersymbol] == 999 and order.Status == 1 and order.Type == 0 and order.Offset == 0 and order.AvgPrice > 0: # 开多完成 symbol_pos[ordersymbol] = 1 if symbol_pos[ordersymbol] == 999 and order.Status == 1 and order.Type == 1 and order.Offset == 1 and order.AvgPrice > 0: # 平多完成 symbol_pos[ordersymbol] = 0 if symbol_pos[ordersymbol] == 999 and order.Status == 1 and order.Type == 1 and order.Offset == 0 and order.AvgPrice > 0: # 开空完成 symbol_pos[ordersymbol] = -1 if symbol_pos[ordersymbol] == 999 and order.Status == 1 and order.Type == 0 and order.Offset == 1 and order.AvgPrice > 0: # 平空完成 symbol_pos[ordersymbol] = 0 order_tbl["rows"].append([order.ContractType, order.Id, order.Info["StatusMsg"], order.Price, "成交" if order.Status == 1 else "未成交", "买入" if order.Type == 0 else "卖出", "开仓" if order.Offset == 0 else "平仓", symbol_pos[ordersymbol]]) def main(): global q while not exchange.IO("status"): Sleep(1000) for symbol in symbols: _C(exchange.SetContractType, symbol) q = ext.NewTaskQueue() while True: e = exchange.IO("wait", -1) if e: if e.Event == "order": on_order(e.Order) elif e.Event == "tick": on_tick(e.Symbol, e.Ticker) q.poll() rTbl_data = {symbol: {"type": "table", "title": symbol, "cols": cols["rTbl"], "rows": rTbl[symbol]["rows"][-10:-1]} for symbol in symbols} orderTbl_data = {symbol: {"type": "table", "title": symbol, "cols": cols["orderTbl"], "rows": orderTbl[symbol]["rows"][-4:-1]} for symbol in symbols} LogStatus("`" + json.dumps(rTbl_data['hc2409']) + "`\n" + "`" + json.dumps(rTbl_data['rb2409']) + "`\n" + "`" + json.dumps(rTbl_data['i2410']) + "`\n" + "`" + json.dumps(orderTbl['hc2409']) + "`\n" + "`" + json.dumps(orderTbl['rb2409']) + "`\n" + "`" + json.dumps(orderTbl['i2410']) + "`")

在先前的课程中,我们在回测系统中,order结构返回的信息比较少,其实在实际的交易场景中,作为一个交易的完整操作,从交易信号辨别,决定下单,向交易所发送请求,到最后完成下单,OrderEvent会返回一系列的事件:

1.第一阶段,订单提交,"StatusMsg"返回报单已提交,这里的重要属性Status代表(订单状态),Type表示(订单买卖类型),Offset表示(期货开平仓方向),Type和Offset决定操作的方向,而返回Status判断交易是否完成,这个时候Status是0,表示未完成状态;
2.在报单提交和报单完成之间,其实还有一个状态,"StatusMsg"会返回"未成交",这个时候Status仍旧是0,表示未完成;
3.最后交易完成,Status会返回1,表示订单完成状态;
4.交易确认阶段。

注意:仿真交易所和实际交易所返回具体信息会有所区别,大家需要根据订单信息返回结构进行相应程序的修改。

  • Type:表示订单类型,Type为1,即表示该订单是一个卖单,如果是0,就是买单。
  • Offset:持仓方向。开仓(Open是0)或平仓(Close是1)。
  • Status:订单状态。Status为0,表示该订单的状态为“未成交”,如果是1,代表是成交,如果是2,代表被撤销的订单。

这就是一条完整的order返回数据,这里面有几个重要的属性可以帮助我们判断订单的方向和完成的阶段状态:这里的order事件返回也是事件驱动返回的,所以我们可以使用order事件进行持仓状态变量的实时判断;我们实现了信号判断和持仓状态判断的事件驱动,对于交易的操作,我们也可以实现非堵塞的模式,还记得前面我们讲过的交易类库中的pushtask函数吗?这里我们可以使用上。每当信号触发,使用pushtask接受交易任务,放进任务队列;然后继续进行其他品种的信号判断和交易任务的执行。通过这样的方式,我们就可以实现一个多品种合约的事件驱动架构的设计。

我们来看下这个策略的具体架构:首先我们来讲下这个策略的具体交易逻辑:作为一个事件驱动的策略,这个策略使用了ticker双均线作为基准。当持仓量为空,最新的ticker价格超过ticker慢线的时候,我们进行开多仓,跌破ticker慢线的时候,我们开空仓;当具有多仓的时候,最新的价格小于ticker快线,我们进行平多仓;具有空仓,价格大于ticker快线,我们平掉空仓。策略的交易确实很简单,下面我们来看怎样实现。

这里我们设置的多品种合约依旧是热卷,螺纹钢,和铁矿石,首先设置两个字典对象tickerListSymbolPos,用来保存这三个品种的ticker数据和持仓数据。这里的持仓数据,每个品种有三个索引,在下面的讲解中我会为大家进行介绍。接下来我们也要使用交易类库中的多任务对象NewTaskQueue

我们的交易操作是根据tick信息和order信息实时事件驱动的。这里的exchange.IO("wait", -1)设计表示是实时返回的机制,有信息立马进行返回。接下来,我们就要设置on_tickon_order函数了,当交易所返回tick或者order信息的时候,我们需要怎样的操作;

第一个on_tick函数,当不同品种tick数据更新的时候,用来搜集相应的ticker数据,并计算均线,决定交易信号的触发。这里我们使用对应的合约品种的键向tickerList中添加最新的ticker数据,我们设置的快线和慢线周期分别为50和100,然后等收集够足够的数量,我们计算相应的ticker均值。然后比较最新的价格ticker.Last和均值的突破作为交易的信号。交易信号的触发是频繁的,为避免频繁交易,所以我们需要设置交易锁,SymbolPos变量。

当交易信号触发,向交易所发送挂单请求,等待order信息返回的中间,如果最新的ticker信息返回,这个时候由于交易信号的再次触发,交易函数会进行二次下单,这个时候我们可以设置一个类似锁的功能,在对应交易信息触发以后,pushtask进行下单,然后就设置该信号对应的操作已经是完成状态,不需要再进行重复的下单。这里的开多,开空,平多和平空,我们都使用这样的交易锁的设置。使用目标品种的symbol_pos[ticksymbol]交易锁判断相应的状态,当满足交易信号,我们进行加锁的设置symbol_pos[ticksymbol] = 999

持仓状态含义可进行操作
symbol_pos[ticksymbol] = 0无持仓开多/开空
symbol_pos[ticksymbol] == 1持有多仓平多
symbol_pos[ticksymbol] == -1持有空仓平空

一个锁既然会锁上,必然也是需要打开的。下面在on_order函数中,我们进行持仓状态的判断和交易锁的解锁功能。我们前面讲过,一个交易操作的完成,会返回不同类型的操作信息,对于“上期模拟”仿真交易所,交易操作的属性和完成判断,需要order.Statusorder.Typeorder.Offsetorder.AvgPrice这四个属性,还有symbol_pos[ordersymbol] == 999在加锁状态下,一旦信号满足要求,我们修改相应的symbol_pos[ordersymbol]变量,进行下一次交易的操作。这样就可以实现了基于order事件驱动的持仓状态的及时修改和更新。

前提条件TypeOffsetAvgPriceStatus代表状态转换状态可进行操作
symbol_pos[ticksymbol] = 99900>01开多完成symbol_pos[ticksymbol] = 1平多
symbol_pos[ticksymbol] = 99911>01平多完成(持仓为空)symbol_pos[ticksymbol] = 0开多/开空
symbol_pos[ticksymbol] = 99910>01开空完成symbol_pos[ticksymbol] = -1平空
symbol_pos[ticksymbol] = 99901>00平空完成(持仓为空)symbol_pos[ticksymbol] = 0开多/开空

注意:这是上期模拟仿真交易所判断条件,对于真实交易所,可以使用StatusMsg属性,返回“全部成交”代表操作完成。

当设计完成on_tickon_order函数,我们就可以带入我们的主循环,当事件更新时,驱动不同的函数进行运行,完成相应的交易操作;不要忘了这里的q.poll()函数,执行任务队列。这样就是一个区别于轮询架构的事件驱动架构的多品种合约的设计。为了展示不同的状态,我们这里输出了不同合约的ticker表和order事件表,可以伴随策略的更新,观察相应的状态变量的变化。

我们在实盘中看下,可以看到伴随不同的tick或者order事件,不同合约的事件信息状态栏不断进行更新,我们可以具体了解到基于事件的驱动架构是怎样完成的。本策略为了教学讲解,因此个别地方设计的比较冗余,大家可以根据自己的想法对这个策略进行更好的优化。作为一个区别于轮询架构的策略,该策略的细节确实比较多,我们在应用于这类策略的时候,可以在实盘中多次模拟检验,搭建自己的事件驱动架构的多品种策略。

8.5 多周期均线共振系统策略

商品期货的多周期均线共振系统策略是一种综合性的交易策略,它基于不同周期的均线共振现象来制定交易计划。这种策略的核心思想是利用不同周期的均线在同一时间点上形成的共振状态,来判断市场走势的强势或弱势,并以此为依据进行交易。期货交易界有句老话:大级别上涨,小级别不配合,涨不动;小级别上涨,大级别不配合,涨不多。只有多个周期的均线系统形成统一方向的趋势共振时,才可以形成持久的趋势行情,这也是多周期共振的理论认知。

8.5.1 策略讲解

我们可以从图像的角度了解多周期共振,具体包括均线的多头排列和空头排列。这里使用到了不同级别的k线,包括大周期,中周期,小周期,和不同滑动窗口的均线,包括快线,中线,和慢线。当不同级别的不同滑动周期的均线,均呈现金叉的信号向上扩散,则形成均线多头排列,均线多头排列代表上涨行情,可以视为多头开仓的信号;与之相反的,当不同级别的不同滑动周期的均线,均呈现死叉的信号,往下扩散,则形成均线空头排列,均线空头排列代表下跌行情,可以视为空头开仓信号。

当多个周期均线都形成多头排列或者空头排列时,就形成了均线多周期共振。均线多周期共振表示当前多个周期的均线走势是相同的。均线在某种意义上代表着资金的走向,同时均线对价格的支撑或阻力作用也较为明显。由此呢,我们可以制定多周期均值共振的交易策略。具体来说,该策略的应用包括以下步骤:

  • 选择合适的周期参数:首先,我们需要明确自己的交易习惯,选择适合自己的周期参数。长线交易可以选择月线、周线和日线三个周期共振;中线交易可以选择周线、日线和60分钟三个周期共振;短线交易可以选择日线、60分钟和15分钟三个周期共振。
  • 判断共振状态:当不同周期的均线在同一时间点附近出现交叉或聚集时,就形成了均线共振。例如,月线、周线和日线的均线方向一致向上,表明市场处于多头趋势。此时,我们可以制定相应的多头策略。
  • 制定交易策略:根据共振状态和市场走势,制定相应的交易策略。在多头趋势中,我们可以选择买入或持有期货合约;在空头趋势中,我们可以选择卖出或观望期货合约。同时,我们还需要注意不同周期的信号冲突和共振状态的变化,及时调整自己的交易策略。
  • 观察进场信号:在确定了交易策略后,需要密切关注市场走势,等待合适的进场信号。例如,在多头趋势中,我们可以等待价格回调到重要的支撑位附近时,发出买入信号时再进场。
  • 设定止损止盈:进场后,需要设定合理的止损止盈位。止损位可以设置在重要的支撑或压力位附近,止盈位则可以参考目标盈利位或移动平均线等指标来确定。
  • 跟踪持仓:进场后,需要密切关注市场走势和持仓情况。如果市场走势与预期不符,需要及时调整自己的交易策略或平仓离场。

8.5.2 策略设计

本节课呢,我们就来编写一个,更加灵活的多周期均线共振策略。这个策略不仅可以适用于多种的k线周期;另外,我们还可以使用量化的方式定义多头排列和空头排列的程度。下面我们来具体讲解一下。

python
'''backtest start: 2024-01-02 00:00:00 end: 2024-04-11 23:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}] args: [["symbol","rb888"],["bigPeriod",480],["midPeriod",60],["stopProfit",20],["stopLoss",20]] ''' import time from datetime import datetime def main(): trend_big_signal = 0 # 大周期趋势信号 trend_mid_signal = 0 # 中周期趋势信号 trend_small_signal = 0 # 小周期趋势信号 final_signal = 0 # 实时趋势信号 p = ext.NewPositionManager() # 交易类库函数 while True: if exchange.IO('status'): # 订阅合约 info = exchange.SetContractType(symbol) # 大周期趋势判断 big_r = exchange.GetRecords(1 * 60 * bigPeriod) pop_big_r = big_r[:-1] if len(pop_big_r) < bigSlowMA: continue fast_ma_big = TA.MA(pop_big_r, bigFastMA) mid_ma_big = TA.MA(pop_big_r, bigMidMA) slow_ma_big = TA.MA(pop_big_r, bigSlowMA) if (fast_ma_big[-1] - mid_ma_big[-1] > diffBig) and (mid_ma_big[-1] - slow_ma_big[-1] > diffBig): trend_big_signal = 1 elif (fast_ma_big[-1] - mid_ma_big[-1] < -diffBig) and (mid_ma_big[-1] - slow_ma_big[-1] < -diffBig): trend_big_signal = -1 else: trend_big_signal = 0 # 中周期趋势判断 mid_r = exchange.GetRecords(1 * 60 * midPeriod) pop_mid_r = mid_r[:-1] fast_ma_mid = TA.MA(pop_mid_r, midFastMA) mid_ma_mid = TA.MA(pop_mid_r, midMidMA) slow_ma_mid = TA.MA(pop_mid_r, midSlowMA) if (fast_ma_mid[-1] - mid_ma_mid[-1] > diffMid) and (mid_ma_mid[-1] - slow_ma_mid[-1] > diffMid): trend_mid_signal = 1 elif (fast_ma_mid[-1] - mid_ma_mid[-1] < -diffMid) and (mid_ma_mid[-1] - slow_ma_mid[-1] < -diffMid): trend_mid_signal = -1 else: trend_mid_signal = 0 # 小周期趋势判断 small_r = exchange.GetRecords(1 * 60 * smallPeriod) pop_small_r = small_r[:-1] fast_ma_small = TA.MA(pop_small_r, smallFastMA) mid_ma_small = TA.MA(pop_small_r, smallMidMA) slow_ma_small = TA.MA(pop_small_r, midSlowMA) if (fast_ma_small[-1] - mid_ma_small[-1] > diffSmall) and (mid_ma_small[-1] - slow_ma_small[-1] > diffSmall): trend_small_signal = 1 elif (fast_ma_small[-1] - mid_ma_small[-1] < -diffSmall) and (mid_ma_small[-1] - slow_ma_small[-1] < -diffSmall): trend_small_signal = -1 else: trend_small_signal = 0 final_signal = 1 if trend_big_signal == trend_mid_signal == trend_small_signal == 1 else -1 if trend_big_signal == trend_mid_signal == trend_small_signal == -1 else 0 pos_info = exchange.GetPosition() # 下单 if not pos_info: if final_signal == 1: p.OpenLong(symbol, 1) Log('开多仓') if final_signal == -1: p.OpenShort(symbol, 1) Log('开空仓') # 止盈止损平仓 if pos_info: profit_level = info['VolumeMultiple'] * info['PriceTick'] * stopProfit loss_level = -info['VolumeMultiple'] * info['PriceTick'] * stopLoss if (pos_info[0].Type % 2 == 0) and (pos_info[0].Profit > profit_level): Log(pos_info[0].Profit) p.Cover(symbol) Log('平盈利多仓') if (pos_info[0].Type % 2 == 0) and (pos_info[0].Profit < loss_level): Log(pos_info[0].Profit) p.Cover(symbol) Log('平亏损多仓') if (pos_info[0].Type % 2 == 1) and (pos_info[0].Profit > profit_level): Log(pos_info[0].Profit) p.Cover(symbol) Log('平盈利空仓') if (pos_info[0].Type % 2 == 1) and (pos_info[0].Profit < loss_level): Log(pos_info[0].Profit) p.Cover(symbol) Log('平亏损空仓') date_time_str = _D() hour, minute = datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S').hour, datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S').minute if (hour == 14 and minute == 55): Log(pos_info[0].Profit) p.Cover(symbol) Log('日内平仓') else: LogStatus('未连接交易所') Sleep(1000)

在策略开始,我们定义策略的参数,这个策略的原理并不是十分的复杂,关键在于策略参数的制定。这里我们定义四组参数,第一组是期货合约,第二到第四组分别是大周期,中周期和小周期的参数设置,具体包括k线周期,代表使用的是月线,日线还是具体的小时分钟线,快线,中线和慢线周期是各个级别的均线周期,有效间隔用来确定各个均线的距离,因为很多时候,不同周期的均线很可能只有很少的差距,可能会形成不明确的假性信号,所以我们可以把均线间隔作为一个变量,这样呢,只有各个均线间隔之间具有显著的差异,我们确定下来具体的交易信号。最后一组参数是平仓的设置,包括止盈参数和止损参数,当持有的仓位达到固定点数的时候,我们直接平仓。

image

回到代码部分,在策略开头,我们定义几个初始化变量,包括大周期/中周期/小周期趋势信号。这里还定义单品种控制对象,使用交易类库可以更方便的进行交易的操作。

接着设置固定框架,while循环加上交易所连接判断。下面编写策略的主体逻辑,第一步订阅合约,然后就要进行不同周期的不同级别均线的计算了,这里首先获取不同级别的k线,还记得GetRecords()可以填写参数吗,根据这个参数可以获取相应周期的k线,参数是以秒为单位的,所以1*60就是一分钟,然后乘以策略参数bigPeriod,如果定义bigPeriod为60,就可以获取到小时级别的k线。但是最后一根k线是不固定的,所以我们可以舍弃最后一根,定义popbigR变量。然后去除popbigR最后一根k线。我们就可以利用这个完整的k线进行后续的移动均线的计算。

均线的计算是有一点数量要求的,所以如果k线数量不足够,我们跳过本次循环。接着来计算大周期快线,中线,和慢线的移动均值,使用参数bigFastMA,bigMidMA和bigSlowMA。最后来判断大周期的趋势,这里不单单是判断快线,中线和慢线不重叠,我们可以增加约束条件,如果两条均线之间的差值大于某个值的时候,我们定义是有效的信号。这里定义如果快线减中线,中线减慢线都大于阈值,定义大周期趋势为多,赋值为1;相反情况下快线小于中线,中线小于慢线,赋值为-1,是空头趋势;另外的其他情况,定义为0,代表不存在明显的趋势。对于中周期和小周期的趋势判断,也是同样的思路。

三个周期的趋势定义完成,这里我们就来定义共振了。使用if表达式,如果三个周期的趋势都为多,定义最终信号finalSignal也是1,如果三个周期趋势都为空,定义最终信号为-1,其他种情况,定义为0。这样多周期的均线共振就定义完成了。

接下来具体的开仓和平仓的思路大家可以自己发挥,可以使用顺势的信号,如果最终信号统一一致,我们确定入场信号,进行该方向的开仓;也可以增加额外条件,在最终信号判断一致的前提下,如果出现向上或者向下的回踩,我们再确定入场信号。这里呢,为了操作演示,采用第一种的思路,如果出现相应信号,直接入场进行开仓。

对于平仓的逻辑,我们可以使用固定止盈止损或者移动止盈止损,本策略当中我们使用固定止盈止损,当实时仓位的利润达到盈利线或者亏损线,我们直接选择平仓。这里的盈利线和亏损线需要注意下,因为不同合约的单位和跳数都是不同的,例如螺纹钢每次跳动PriceTick是1点,合约单位VolumeMultiple是10吨,所以每个价格变化是10;而纸浆的跳动单位是2,玻璃的合约单位是20,所以这里设置盈利线和亏损线使用止盈或者止损单位乘以合约单位,再乘以价格跳数。

另外一点,对于仓位类型type类型的判断,相对于以往我们手动的定义type等于PD_LONGPD_LONG_YDPD_SHORTPD_SHORT_YD,我们可以简单的表达,PD_LONGPD_LONG_YD的值是0和2,是偶数,另外两种仓位类型是奇数,所以可以除以2,然后取余数的方法,如果余数为0,代表判断是多头持仓,如果余数是1,代表空头持仓,更加简便一点。

这样,我们的策略的交易逻辑就编写完成,我们来使用回测系统测试一下,首先设置策略参数,这里我们大周期定义为一个小时,所以是60分钟,中周期是30分钟,小周期是15分钟,快线,中线和慢线分别定义为5,10,和15。止盈和止损点数设置为2和5。我们来运行一下,可以看到这里呈现了不同周期的空线以及相应的均线,并且在小周期的K线图上,标注了多头和空头信号开启和关闭的提醒,然后我们进行相应的入场和出场的操作。

总之,商品期货的多周期均线共振系统策略是一种综合性的交易策略,它需要我们综合考虑不同周期的市场走势和自己的交易习惯来制定交易计划。同时,我们还需要注意风险管理、进场信号和持仓跟踪等方面的问题,以确保自己的交易能够取得成功。本节课只是一个示范性的内容,大家有在量化交易中碰到的问题,都可以留言,我们在力所能及的范围内,都会耐心解答。

8.6 多品种海龟交易策略

8.6.1 海龟交易系统

在前面我们讲过的策略,为了讲解的方便,使用的大多数都是单品种的策略,并且每次交易的手数都是一手。然而,对于大资金的量化需求,我们需要的是一个多品种的,可以伴随盈利逐步加仓,达到盈利点位或者止损点位进行减仓,并且可以控制风险的量化模型。那么有没有一种策略可以满足我们的要求呢?多品种海龟交易策略不要错过。海龟交易系统是一种经典的趋势追踪交易策略,由美国期货交易员Richard Dennis和William Eckhardt在1983年推出。该系统基于价格的突破和趋势跟随原理,在市场中追踪并参与长期趋势,用来获取较大的收益。

首先,我们介绍下海龟交易系统的主要组成部分和特点:

  • 多个市场:海龟交易系统可以应用于多种交易市场,包括商品期货、股票和外汇等。
  • 入市规则:基于价格的突破是海龟交易系统的核心概念。通过观察市场价格是否突破一定周期内的高点或低点,决定是否入市建立头寸。
  • 逐步建仓:海龟交易系统采用逐步建仓的方式。根据市场的表现和波动情况,逐步扩大头寸规模,但也有严格的风控规则限制仓位大小。
  • 止损规则:海龟交易系统非常重视风险控制,当市场走势反向达到止损位时,及时平仓以限制损失。
  • 退出规则:海龟交易系统有多种退出规则,包括根据价格突破逆向信号、固定的离市周期或固定的利润目标等。这些规则用于判断何时平仓并退出头寸。
  • 波动性管理:海龟交易系统会根据市场的波动性进行头寸规模的调整,通常使用ATR(平均真实波幅)指标来计算波动性,并根据波动性来决定头寸的大小。

接下来我们来具体解释下海龟策略的交易逻辑。海龟交易系统是一个完整的交易系统,它有一个完整的交易系统应该有的所有成分,涵盖了期货交易中的每一个必要决策:

  • 入市----什么时候进行开仓?
  • 入市规模----具体开仓的手数;
  • 加仓----什么时候进行加仓;
  • 止损----什么时候达到止损点位,卖出亏损的品种;
  • 离市----什么时候获利了结,卖出赢利的品种。

首先,我们来看什么时候进行买卖?海龟用两个相关的系统选择品种,这两个系统都以通道突破系统为基础。系统一:以20日突破为基础的偏短线系统;系统二:以55日突破为基础的较简单的长线系统。这两种不同却有关系的突破系统法则,可以称为系统一和系统二。我们可以根据自己的交易理念,自行决定将资金配置在何种系统上。有些交易员选用单一的系统交易所有的资金,或者分别用资金的50%选择系统一,50%选择系统二,当然还有其他的不同的组合选择。海龟策略利用两个突破系统的触发情况确定最高价和最低价,并根据最新价格,与最高价和最低价的关系,选择对应的做多突破,或者做空突破,进而进行相应的做多或者做空操作。

第二点,我们来看买卖多少?买卖期货的数量,用下面的公式计算。这个公式可以帮助我们根据账户资金、风险承受能力和市场波动性来确定每个交易头寸的大小。

  • 交易头寸=帐户金额 *(1-保证金比率)* 风险系数/N值/合约乘数
  • 账户金额:指账户当前可用资金或账户净值。
  • 保证金比率:指期货合约所要求的初始保证金占总价值的比例。它表示每手合约所需的初始保证金占头寸价值的比例。
  • 风险系数:代表个体风险承受的程度,由交易员根据自身的风险偏好设定。
  • N值:是市场波动性的度量指标,例如平均真实波幅(ATR)。它用于衡量价格的波动情况。
  • 合约乘数:是期货合约中每手合约所代表的标的资产数量。

第三点,我们来看什么时候加仓?在建立头寸后,如果最新的价格突破成功,就是价差大于加仓系数(一般是1/2)乘以ATR的间隔,可以选择增加头寸,增加头寸的数量也是使用公式计算出来的。但是通常情况下,对于加仓次数会设置一个最大限制,如果超过,就不再加仓。这样的限制可以控制风险,避免过度加仓。

第四点,什么时候进行止损?在具有仓位的情况下,当判断最新一笔的盈亏大于设定的亏损限制(止损系数(一般是2)乘以N值的时候,表示触发止损,我们进行该品种的止损清仓操作。

第五点,什么时候进行平仓离场?在持有仓位的期间,我们会时刻记录一段周期内的最高价和最低价,定义为上线和下线。对于多头持仓,当前价格小于离场周期内的最低价,进行多头平仓。对于空头持仓,当前价格大于离场周期内的最高价,进行空头平仓。

以上呢,就是海龟策略的交易逻辑。当然,一个完整的量化交易系统,并不是只有交易策略的逻辑,我们还有下面的一系列需求:

  • 交易进度的保存和恢复:在海龟策略中,我们看到了大量的状态变量,比如各个品种的开仓价格,开仓次数,N值等等,而如果遇到实盘突然停止,这些状态变量的丢失,将会造成交易进度的丢失,影响策略的效果,因此我们需要及时的进行交易进度的保存和恢复工作。
  • 策略运行状态的界面展示:作为一个多品种的趋势策略,我们需要及时的了解各个品种的交易进度,包括各个品种的持仓数量,持仓方向,加仓次数,整体收益等等,因此我们需要设计完善的运行状态的表格和图像的展示。
  • 信息的及时推送:作为一个量化策略,省去的就是人工盯盘的烦恼。在每次交易开仓,加仓和平仓的时候,我们可以设置手机APP或者邮箱的自动提示,帮助我们及时关注策略的运行状况。

除了上述工作之外呢,我们还需要根据策略的运行情况,及时的进行不同品种和不同参数的设置,帮助优化策略表现,提升收益水平。

怎么样,在了解完海龟策略的基本概念和需求以后,有没有想尝试用代码搭建完成模型呢?来,让我们从打地基开始,着手开始吧。首先,提示一下,本节课代码内容确实比较多,因此逐句进行讲解可能会耽误大家太多的时间,因此,海龟策略的讲解重点将放在策略整体的设计框架,但是我们也会讲解到每个函数的功能以及具体的代码逻辑。其实大部分的知识都是我们所讲过的。策略的设计架构在量化交易系统中起着重要的作用。作为初学者,我们常常过于关注策略的盈利能力,而忽视了设计架构的重要性。一个良好的架构,在升级功能,调试测试,和扩展优化都是非常方便的,并且不容易出现潜藏BUG。在同时,一个好的设计架构可以让策略交易逻辑和策略下单处理逻辑等其它与策略不相关的功能代码,进行很好的分离。这些代码耦合很低,所以非常容易修改,当然前提是要在通读过策略,完全理解策略架构之后。除去海龟策略,还有很多的多品种交易策略,比如均线策略,R-Breaker策略等等,其实我们完全可以把原版策略中,和交易策略相关的内容,分离出来删除掉,只留下一个多品种策略框架,就可以根据自己的需求进行其他策略的开发。

首先,我们来设置策略的参数,在了解完这些参数以后,我们可以对策略的整体脉络有一个程序化的认识。

变量描述类型默认值
Instruments合约列表字符串(string)MA888,pp888,v888,rb888,jm888
LoopInterval轮询周期(秒)数字型(number)3
RiskRatio% Risk Per N (0 - 100)数字型(number)1
ATRLengthATR计算周期数字型(number)20
EnterPeriodA系统一入市周期数字型(number)20
LeavePeriodA系统一离市周期数字型(number)10
EnterPeriodB系统二入市周期数字型(number)55
LeavePeriodB系统二离市周期数字型(number)20
UseEnterFilter使用入市过滤布尔型(true/false)false
IncSpace加仓间隔(N的倍数)数字型(number)0.5
StopLossRatio止损系数(N的倍数)数字型(number)2
MaxLots单品种加仓次数数字型(number)4
Push推送交易信息布尔型(true/false)true
KeepRatio预留保证金比例数字型(number)20
RMode进度恢复模式下拉框(selected)自动
VMStatus@RMode==1手动恢复字符串字符串(string){}

对于海龟交易策略的各个参数,我们是这样设置的:

  • Instruments(合约列表):指定要交易的合约列表,以逗号分隔不同合约的代码。
  • LoopInterval(轮询周期):指定策略运行轮询休息周期。
  • RiskRatio(% Risk Per N):表示每个交易单元风险的百分比。N代表ATR(平均真实波动幅度)的值。用于确定每个交易单元的头寸规模。
  • ATRLength(ATR计算周期):用于计算ATR指标的时间周期长度。ATR是一种衡量市场波动性的指标,用于确定止损和加仓的位置。
  • EnterPeriodA(系统一入市周期):在系统一中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。
  • LeavePeriodA(系统一离市周期):在系统一中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。
  • EnterPeriodB(系统二入市周期):在系统二中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。
  • LeavePeriodB(系统二离市周期):在系统二中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。
  • UseEnterFilter(入市过滤):是否采用入市过滤。
  • IncSpace(加仓间隔):加仓的价格间隔,以N倍的ATR为单位。在每次加仓时,头寸规模将乘以IncSpace。
  • StopLossRatio(止损系数):止损价格相对于入场价格的距离。以N倍的ATR为单位。当市场价格跌破止损价格时,触发止损。
  • MaxLots(单品种加仓次数):允许的最大加仓次数。超过该次数后将不再进行加仓。
  • WXPush(推送交易信息):确定是否推送交易信息到微信或其他渠道。
  • KeepRatio(预留保证金比例):在计算可用保证金时的预留比例。用于确保保证金余额不会过低,以应对不利行情。
  • Mode和VMStatus@RMode,是选择进度恢复模式,手动还是自动,如果是自动的话,可以使用_G进行读取;如果是手动,可以填写字符串,然后使用JavaScriptON.parse函数进行读取。

这些参数可以根据具体的交易策略和市场情况进行调整,以达到更好的交易结果。

8.6.2 多品种海龟交易策略框架

我们来继续海龟策略的编写。进入我们的代码,这段代码确实比较复杂,具体的变量和函数有很多,我们先来看一下策略的框架。整体来看,海龟策略的代码分为两个板块,第一个是交易逻辑类对象 Manager。作为一个交易逻辑对象类,这个对象主要就是用来构造海龟交易逻辑对象的。整个的「海龟交易法则」用代码表达的部分都封装在这个部分。

第二个部分是main主函数。在主函数中,主要包括了while循环之前的程序初始化部分的设置工作,在这里会调用交易逻辑对象 Manager,使用轮询的方式,构造每个要交易的合约对应的海龟交易逻辑对象。下面是while循环,该循环为策略的主要循环,这一部分主要是遍历所有的海龟交易逻辑对象,调用每个海龟交易逻辑对象的处理函数进行相应的交易操作,最后一部分进行了策略运行时的界面显示的设计。可以看到,这里把海龟交易逻辑相关的操作都完全独立了出来,让整个策略层次比较分明。

python
class Manager: # 定义常量... # 定义类函数... def main(): # 主函数实现...

8.6.3 Manager交易对象

首先,我们来看第一部分的代码。相对于以往我们在主函数中编写策略逻辑,这段代码实现了一个名为 Manager 类,其中包含各种海龟交易逻辑所需的属性和方法。通过调用Manager,可以创建一个新的合约交易对象。

这里我们稍作补充,使用类在策略设计中的好处:

  • 更符合面向对象的编程思想:类是面向对象编程的核心概念,通过类可以更好地模拟现实世界中的对象,使得代码结构更加清晰和易于理解。

  • 封装性和抽象性:类能够将数据和方法封装在一起,实现了数据和行为的关联性,提高了代码的封装性和抽象性。

  • 继承和多态:类支持继承和多态,可以通过继承现有的类来创建新的类,并且可以重写父类的方法以实现不同的行为,提高了代码的复用性和灵活性。

  • 代码组织和管理:类使得代码组织更加结构化,能够更轻松地管理和维护代码,提高了代码的可维护性和可扩展性。

  • 更好的代码复用和扩展性:类的属性和方法可以在不同的对象之间共享和复用,通过创建新的子类或者扩展现有类,可以轻松地实现新的功能,提高了代码的复用性和扩展性。

综上所述,使用类能够更好地体现面向对象编程的特性,使得代码更加模块化、可维护性更高、扩展性更强。

这个 Manager 类是一个用于管理交易活动的类。让我们来解释一下它的功能和工作原理:

python
import re import json import time _bot = ext.NewPositionManager() class Manager: ACT_IDIE = 0 ACT_LONG = 1 ACT_SHORT = 2 ACT_COVER = 3 ERR_SUCCESS = 0 ERR_SET_SYMBOL = 1 ERR_GET_ORDERS = 2 ERR_GET_POS = 3 ERR_TRADE = 4 ERR_GET_DEPTH = 5 ERR_NOT_TRADING = 6 errMsg = ["成功","切换合约失败","获取订单失败","获取持仓失败","交易下单失败","获取深度失败","不在交易时间"] def __init__(self,needRestore,symbol,keepBalance,riskRatio,atrLen,enterPeriodA,leavePeriodA,enterPeriodB,leavePeriodB,useFilter,multiplierN,multiplierS,maxLots): symbolDetail = _C(exchange.SetContractType,symbol) if symbolDetail["VolumeMultiple"] == 0 or symbolDetail["MaxLimitOrderVolume"] == 0 or symbolDetail["MinLimitOrderVolume"] == 0 or symbolDetail["LongMarginRatio"] == 0 or symbolDetail["ShortMarginRatio"] == 0 : Log(symbolDetail) raise Exception("合约信息异常") else: Log("合约:",symbolDetail["InstrumentName"],"一手",symbolDetail["VolumeMultiple"],"份,最大下单量:",symbolDetail["MaxLimitOrderVolume"],"保证金比率:",symbolDetail["LongMarginRatio"],symbolDetail["ShortMarginRatio"],"交割日期:",symbolDetail["StartDelivDate"]) self.symbol = symbol self.keepBalance = keepBalance self.riskRatio = riskRatio self.atrLen = atrLen self.enterPeriodA = enterPeriodA self.leavePeriodA = leavePeriodA self.enterPeriodB = enterPeriodB self.leavePeriodB = leavePeriodB self.useFilter = useFilter self.multiplierN = multiplierN self.multiplierS = multiplierS self.maxLots = maxLots self.symbolDetail = symbolDetail self.lastPrice = 0 self.task = { "action" : Manager.ACT_IDIE , "amount" : 0 , "dealAmount" : 0 , "avgPrice" : 0 , "preCost" : 0 , "preAmount" : 0 , "init" :False , "retry" : 0 , "desc" : "空闲" , "onFinish" : None } self.status = { "symbol" : symbol , "recordsLen" : 0 , "vm" : [] , "open" : 0 , "cover" : 0 , "st" : 0, "marketPosition" : 0 , "lastPrice" : 0, "holdPrice" : 0 , "holdAmount" : 0 , "holdProfit" : 0, "N" : 0, "upLine" : 0, "downLine" : 0, "symbolDetail" : symbolDetail, "lastErr" : "", "lastErrTime" : "", "stopPrice" : "", "leavePrice" : "", "isTrading" : False } vm = None if RMode == 0 : vm = _G(self.symbol) else: vm = json.loads(VMStatus)[self.symbol] if vm : Log("准备恢复进度,当前合约状态为",vm) self.reset(vm[0],vm[1],vm[2],vm[3],vm[4]) else: if needRestore : Log("没有找到" + self.symbol +"的进度恢复信息") self.reset() def setLastError(self,err = None): if err is None : self.status["lastErr"] = "" self.status["lastErrTime"] = "" return t = _D() self.status["lastErr"] = err self.status["lastErrTime"] = t def reset(self,marketPosition = None,openPrice = None,N = None,leavePeriod = None,preBreakoutFailure = None): if marketPosition is not None: self.marketPosition = marketPosition self.openPrice = openPrice self.N = N self.leavePeriod = leavePeriod self.preBreakoutFailure = preBreakoutFailure pos = _bot.GetPosition(self.symbol,PD_LONG if marketPosition > 0 else PD_SHORT) if pos is not None: self.holdPrice = pos["Price"] self.holdAmount = pos["Amount"] Log(self.symbol,"仓位:",pos) else: raise Exception("恢复" + self.symbol + "的持仓状态出错,没有找到仓位信息") Log("恢复",self.symbol,"加仓次数:",self.marketPosition,"持仓均价:",self.holdPrice,"持仓数量:",self.holdAmount,"最后一次加仓价:",self.openPrice,"N值:",self.N,"离市周期:",self.leavePeriod,"上次突破:","失败" if self.preBreakoutFailure else "成功") self.status["open"] = 1 self.status["vm"] = [self.marketPosition,self.openPrice,self.N,self.leavePeriod,self.preBreakoutFailure] else: self.marketPosition = 0 self.holdPrice = 0 self.openPrice = 0 self.holdAmount = 0 self.holdProfit = 0 self.preBreakoutFailure = True self.N = 0 self.leavePeriod = self.leavePeriodA self.holdProfit = 0 self.lastErr = "" self.lastErrTime = "" def Status(self): self.status["N"] = self.N self.status["marketPosition"] = self.marketPosition self.status["holdProfit"] = self.holdProfit self.status["holdAmount"] = self.holdAmount self.status["lastPrice"] = self.lastPrice if self.lastPrice > 0 and self.holdAmount > 0 and self.marketPosition != 0 : self.status["holdProfit"] = _N((self.lastPrice - self.holdPrice)*self.holdAmount * self.symbolDetail["VolumeMultiple"], 4) *(1 if self.marketPosition > 0 else -1) else: self.status["holdProfit"] = 0 return self.status def setTask(self,action,amount = None,onFinish = None): self.task["init"] = False self.task["retry"] = 0 self.task["action"] = action self.task["preAmount"] = 0 self.task["preCost"] = 0 self.task["amount"] = 0 if amount is None else amount self.task["onFinish"] = onFinish if action == Manager.ACT_IDIE : self.task["desc"] = "空闲" self.task["onFinish"] = None else: if action != Manager.ACT_COVER: self.task["desc"] = ("加多仓" if action == Manager.ACT_LONG else "加空仓") + "(" + str(amount) + ")" else: self.task["desc"] = "平仓" Log("接收到任务",self.symbol,self.task["desc"]) self.Poll(True) def processTask(self): insDetail = exchange.SetContractType(self.symbol) if not insDetail: return Manager.ERR_SET_SYMBOL SlideTick = 1 ret = False if self.task["action"] == Manager.ACT_COVER: hasPosition = False while True: if not ext.IsTrading(self.symbol): return Manager.ERR_NOT_TRADING hasPosition = False positions = exchange.GetPosition() if positions is None: return Manager.ERR_GET_POS depth = exchange.GetDepth() if depth is None: return Manager.ERR_GET_DEPTH orderID = None for i in range(len(positions)): if positions[i]["ContractType"] != self.symbol: continue amount = min(insDetail["MaxLimitOrderVolume"], positions[i]["Amount"]) if positions[i]["Type"] == PD_LONG or positions[i]["Type"] == PD_LONG_YD : exchange.SetDirection("closebuy_today" if positions[i]["Type"] == PD_LONG else "closebuy") orderID = exchange.Sell(_N(depth["Bids"][0]["Price"] - (insDetail["PriceTick"] * SlideTick), 2),min(amount, depth["Bids"][0]["Amount"]), self.symbol,"平今" if positions[i]["Type"] == PD_LONG else "平昨", "Bid",depth["Bids"][0]) hasPosition = True elif positions[i]["Type"] == PD_SHORT or positions[i]["Type"] == PD_SHORT_YD : exchange.SetDirection("closesell_today" if positions[i]["Type"] == PD_SHORT else "closesell") orderID = exchange.Buy(_N(depth["Asks"][0]["Price"] + (insDetail["PriceTick"] * SlideTick), 2),min(amount, depth["Asks"][0]["Amount"]), self.symbol,"平今" if positions[i]["Type"] == PD_SHORT else "平昨","Ask",depth["Asks"][0]) hasPosition = True if hasPosition: if not orderID: return Manager.ERR_TRADE Sleep(1000) while True: orders = exchange.GetOrders() if orders is None: return Manager.ERR_GET_ORDERS if len(orders) == 0: break for i in range(len(orders)): exchange.CancelOrder(orders[i]["Id"]) Sleep(500) if not hasPosition: break ret = True elif self.task["action"] == Manager.ACT_LONG or self.task["action"] == Manager.ACT_SHORT: while True: if not ext.IsTrading(self.symbol): return Manager.ERR_NOT_TRADING Sleep(1000) while True: orders = exchange.GetOrders() if orders is None: return Manager.ERR_GET_ORDERS if len(orders) == 0: break for i in range(len(orders)): exchange.CancelOrder(orders[i]["Id"]) Sleep(500) positions = exchange.GetPosition() if positions is None: return Manager.ERR_GET_POS pos = None for i in range(len(positions)): if positions[i]["ContractType"] == self.symbol and (((positions[i]["Type"] == PD_LONG or positions[i]["Type"] == PD_LONG_YD) and self.task["action"] == Manager.ACT_LONG) or ((positions[i]["Type"] == PD_SHORT) or positions[i]["Type"] == PD_SHORT_YD) and self.task["action"] == Manager.ACT_SHORT): if not pos: pos = positions[i] pos["Cost"] = positions[i]["Price"] * positions[i]["Amount"] else: pos["Amount"] += positions[i]["Amount"] pos["Profit"] += positions[i]["Profit"] pos["Cost"] += positions[i]["Price"] * positions[i]["Amount"] if not self.task["init"]: self.task["init"] = True if pos: self.task["preAmount"] = pos["Amount"] self.task["preCost"] = pos["Cost"] else: self.task["preAmount"] = 0 self.task["preCost"] = 0 remain = self.task["amount"] if pos: self.task["dealAmount"] = pos["Amount"] - self.task["preAmount"] remain = self.task["amount"] - self.task["dealAmount"] if remain <= 0 or self.task["retry"] >= MaxTaskRetry: ret = { "price" : (pos["Cost"] - self.task["preCost"]) / (pos["Amount"] - self.task["preAmount"]), "amount" : (pos["Amount"] - self.task["preAmount"]), "position" : pos } break elif self.task["retry"] >= MaxTaskRetry: ret = None break depth = exchange.GetDepth() if depth is None: return Manager.ERR_GET_DEPTH orderID = None if self.task["action"] == Manager.ACT_LONG: exchange.SetDirection("buy") orderID = exchange.Buy(_N(depth["Asks"][0]["Price"] + (insDetail["PriceTick"] * SlideTick), 2), min(remain,depth["Asks"][0]["Amount"]),self.symbol,"Ask",depth["Asks"][0]) else: exchange.SetDirection("sell") orderID = exchange.Sell(_N(depth["Bids"][0]["Price"] - (insDetail["PriceTick"] * SlideTick), 2), min(remain,depth["Bids"][0]["Amount"]),self.symbol,"Bid",depth["Bids"][0]) if orderID is None: self.task["retry"] += 1 return Manager.ERR_TRADE if self.task["onFinish"]: self.task["onFinish"](ret) self.setTask(Manager.ACT_IDIE) return Manager.ERR_SUCCESS def Poll(self,subroutine = False): self.status["isTrading"] = ext.IsTrading(self.symbol) if not self.status["isTrading"]: return if self.task["action"] != Manager.ACT_IDIE: retCode = self.processTask() if self.task["action"] != Manager.ACT_IDIE: self.setLastError("任务没有处理成功:" + Manager.errMsg[retCode] + ", " + self.task["desc"] + ", 重试:" + str(self.task["retry"])) else: self.setLastError() return if subroutine: return suffix = "@" if Push else "" _C(exchange.SetContractType,self.symbol) records = exchange.GetRecords() if records is None: self.setLastError("获取K线数据失败") return self.status["recordsLen"] = len(records) if len(records) < self.atrLen: self.setLastError("K线数据长度小于ATR:" + str(self.atrLen)) return opCode = 0 lastPrice = records[-1]["Close"] self.lastPrice = lastPrice if self.marketPosition == 0 : self.status["stopPrice"] = "--" self.status["leavePrice"] = "--" self.status["upLine"] = 0 self.status["downLine"] = 0 for i in range(2): if i == 0 and self.useFilter and not self.preBreakoutFailure: continue enterPeriod = self.enterPeriodA if i == 0 else self.enterPeriodB if len(records) < (enterPeriod+1): continue highest = TA.Highest(records, enterPeriod, "High") lowest = TA.Lowest(records, enterPeriod, "Low") self.status["upLine"] = highest if self.status["upLine"] == 0 else min(self.status["upLine"], highest) self.status["downLine"] = lowest if self.status["downLine"] == 0 else max(self.status["upLine"], lowest) if lastPrice > highest: opCode = 1 elif lastPrice < lowest: opCode = 2 self.leavePeriod = self.leavePeriodA if (enterPeriod == self.enterPeriodA) else self.leavePeriodB else: spread = (self.openPrice - lastPrice) if self.marketPosition > 0 else (lastPrice - self.openPrice) self.status["stopPrice"] = _N(self.openPrice + (self.N * StopLossRatio * (-1 if self.marketPosition > 0 else 1))) if spread > (self.N * StopLossRatio): opCode = 3 self.preBreakoutFailure = True Log(self.symbolDetail["InstrumentName"], "止损平仓", suffix) self.status["st"] += 1 elif -spread > (IncSpace * self.N): opCode = 1 if self.marketPosition > 0 else 2 elif len(records) > self.leavePeriod : self.status["leavePrice"] = TA.Lowest(records,self.leavePeriod,"Low") if self.marketPosition > 0 else TA.Highest(records,self.leavePeriod,"High") if (self.marketPosition > 0 and lastPrice < self.status["leavePrice"]) or (self.marketPosition < 0 and lastPrice > self.status["leavePrice"]): self.preBreakoutFailure = True Log(self.symbolDetail["InstrumentName"] , "正常平仓", suffix) opCode = 3 self.status["cover"] += 1 if opCode == 0: return if opCode == 3: def coverCallBack(ret): self.reset() _G(self.symbol,None) self.setTask(Manager.ACT_COVER,0,coverCallBack) return if abs(self.marketPosition) >= self.maxLots: self.setLastError("禁止开仓,超过最大开仓次数" + str(self.maxLots)) return atrs = TA.ATR(records,self.atrLen) N = _N(atrs[len(atrs) - 1],4) account = _bot.GetAccount() currMargin = json.loads(exchange.GetRawJSON())["CurrMargin"] unit = int((account["Balance"] + currMargin - self.keepBalance) * (self.riskRatio / 100) / N / self.symbolDetail["VolumeMultiple"]) canOpen = int((account["Balance"] - self.keepBalance) / (self.symbolDetail["LongMarginRatio"] if opCode == 1 else self.symbolDetail["ShortMarginRatio"]) / (lastPrice*1.2) / self.symbolDetail["VolumeMultiple"]) unit = min(unit,canOpen) if unit < self.symbolDetail["MinLimitOrderVolume"]: self.setLastError(str(unit) + "手,无法开仓") return def setTaskCallBack(ret): if not ret : self.setLastError("下单失败") return Log(self.symbolDetail["InstrumentName"], "开仓" if self.marketPosition == 0 else "加仓", "离市周期", self.leavePeriod, suffix) self.N = N self.openPrice = ret["price"] self.holdPrice = ret["position"]["Price"] self.holdAmount = ret["position"]["Amount"] if self.marketPosition == 0 : self.status["open"] += 1 self.marketPosition += (1 if opCode == 1 else -1) self.status["vm"] = [self.marketPosition, self.openPrice, self.N, self.leavePeriod, self.preBreakoutFailure] _G(self.symbol,self.status["vm"]) self.setTask(Manager.ACT_LONG if opCode == 1 else Manager.ACT_SHORT, unit, setTaskCallBack)
  1. 常量

    • ACT_IDLE, ACT_LONG, ACT_SHORT, ACT_COVER:这些常量定义了管理器可以执行的不同操作或状态。例如,ACT_IDLE表示空闲状态,ACT_LONGACT_SHORT表示开多和开空仓位,而ACT_COVER表示平仓或关闭仓位。
    • ERR_SUCCESS, ERR_SET_SYMBOL, ERR_GET_ORDERS等:这些常量是类在操作过程中可能遇到的错误代码。errMsg列表提供了与这些错误代码相对应的可读的错误消息。
  2. 初始化方法 (__init__)

    • 初始化时,它接受一系列参数,包括是否需要恢复、交易合约、风险管理相关参数等。
    • 它检查交易合约的详细信息,如成交量倍数、最大/最小限价订单量、保证金比率等,并在初始化时记录这些信息。
    • 还初始化了内部变量和字典,用于跟踪任务和状态。
  3. 辅助方法

    • setLastError:设置管理器遇到的最后一个错误。
    • reset:重置管理器的状态,可选择根据提供的相关参数将其恢复到先前的状态。
    • Status:获取管理器的当前状态,包括持仓情况、利润等。
    • setTask:为管理器设置新任务,例如开仓,加仓或平仓。
    • processTask:处理为管理器设置的当前任务,这涉及与交易所的具体的交易操作。
    • Poll:轮询市场条件,并根据海龟交易策略决定行动。
  4. 主要逻辑

    • 类中的 Poll 方法是核心逻辑,它根据市场条件执行不同的操作。包括具体交易直播的计算,以及开仓信号的判断。
    • 通过分析市场数据和预定义的交易策略,它可以决定是否开仓、加仓、平仓,或者维持空闲状态。
    • 交易操作涉及到与外部交易所的交互,包括下单、获取持仓、获取市场深度等操作。
  5. 错误处理

    • 类中有一些错误代码和错误消息的定义,用于处理可能出现的异常情况。
    • 在发生错误时,管理器会记录错误信息,并根据需要采取相应的措施,如重新尝试或报警。

综上所述,这个 Manager 类是一个用于管理交易活动的抽象类,它封装了交易策略的执行逻辑和与交易所的交互过程。不过这里重要的是作为一个实盘级别策略,其中一些策略设计,比如关于错误信息显示,状态变量保存和恢复,具体交易信号的获取和交易操作执行的函数设计,都是值得我们学习和探索的地方,大家可以应用到自己的实盘策略编写技巧当中。

8.6.4 海龟交易逻辑主函数

python
def main(): while not exchange.IO("status"): Sleep(3000) LogStatus("正在等待与交易服务器连接") positions = _C(exchange.GetPosition) if len(positions) > 0 : Log("检测到当前持有仓位,系统开始恢复进度....") Log("持仓信息:",positions) Log("风险参数:",RiskRatio, "N值周期:",ATRlength, "系统1:入市周期",EnterPeriodA, "离市周期",LeavePeriodA, "系统2:入市周期",EnterPeriodB, "离市周期",LeavePeriodB,"加仓系数:",IncSpace,"止损系数:",StopLossRatio,"单品种最多开仓",MaxLots,"次") initAccount = _bot.GetAccount() initMargin = json.loads(exchange.GetRawJSON())["CurrMargin"] keepBalance = _N((initAccount["Balance"] + initMargin) * (keepRatio / 100), 3) Log("资产信息:",initAccount, "保留资金:",keepBalance) tts = [] symbolFilter = {} arr = Instruments.split(",") for i in range(len(arr)): symbol = re.sub(r'/\s+$/g', "", re.sub(r'/^\s+/g', "", arr[i])) if symbol in symbolFilter.keys(): raise Exception(symbol + "已经存在,请检查合约参数") symbolFilter[symbol] = True hasPosition = False for j in range(len(positions)): if positions[j]["ContractType"] == symbol : hasPosition = True break obj = Manager(hasPosition,symbol,keepBalance,RiskRatio,ATRlength,EnterPeriodA,LeavePeriodA,EnterPeriodB,LeavePeriodB,UseEnterFilter,IncSpace,StopLossRatio,MaxLots) tts.append(obj) while True: while not exchange.IO("status"): Sleep(1000) LogStatus("正在等待与交易服务器连接") tblStatus = { "type" : "table", "title" : "持仓信息", "cols" : ["合约名称","持仓方向","持仓均价","持仓数量","持仓盈亏","加仓次数","开仓次数","止损次数","成功次数","当前价格","N"], "rows" : [] } tblMarket = { "type" : "table", "title" : "运行状态", "cols" : ["合约名称","合约乘数","保证金比率","交易时间","柱线长度","上线","下线","止损价","离市价","异常描述","发生时间"], "rows" : [] } totalHold = 0 vmStatus = {} holdSymbol = 0 for i in range(len(tts)): tts[i].Poll() d = tts[i].Status() if d["holdAmount"] > 0 : vmStatus[d["symbol"]] = d["vm"] holdSymbol += 1 tblStatus["rows"].append([d["symbolDetail"]["InstrumentName"], "--" if d["holdAmount"] == 0 else ("多" if d["marketPosition"] > 0 else "空"), d["holdPrice"], d["holdAmount"], d["holdProfit"], d["marketPosition"], d["open"], d["st"], d["cover"], d["lastPrice"], d["N"]]) tblMarket["rows"].append([d["symbolDetail"]["InstrumentName"], d["symbolDetail"]["VolumeMultiple"], str(d["symbolDetail"]["LongMarginRatio"]) + "/"+ str(d["symbolDetail"]["ShortMarginRatio"]), "是#0000ff" if d["isTrading"] else "否#0000ff", d["recordsLen"],d["upLine"], d["downLine"], d["stopPrice"],d["leavePrice"],d["lastErr"],d["lastErrTime"]]) totalHold += abs(d["holdAmount"]) lastStatus = "`" + json.dumps([tblStatus, tblMarket]) + "`\n" + "当前时间:"+_D() + ",持有品种个数:" + str(holdSymbol) if totalHold > 0 : lastStatus += "\n手动恢复字符串:" + json.dumps(vmStatus) LogStatus(lastStatus) Sleep(LoopInterval * 1000)

这段代码是一个主函数 main(),主要是用于初始化交易环境、创建交易管理对象,并在主循环中监控市场情况和持仓状态,以及进行相应的交易操作,具体它执行以下操作:

  1. 等待交易服务器连接

    • 在进入主循环之前,首先等待与交易服务器的连接建立。如果连接未建立,它会每隔3秒检查一次,直到连接建立为止。
  2. 获取初始持仓信息

    • 通过调用交易所的 GetPosition 函数获取当前的持仓信息,并记录到 positions 变量中。
    • 如果检测到有持仓信息,会记录下来并显示在日志中。
  3. 初始化账户和保留资金

    • 获取账户初始信息,包括余额和当前已用保证金。
    • 根据预设的保留比例计算需要保留的资金。
  4. 创建交易管理对象

    • 针对每个指定的交易合约,创建一个 Manager 对象,用于管理该合约的交易活动。
    • 检查是否已经持有该合约的仓位,并根据情况初始化 Manager 对象。
  5. 主循环

    • 进入一个无限循环,不断检查交易服务器的连接状态和市场情况。
    • 对每个交易合约执行 Poll 操作,更新交易状态和市场情况。
    • 将持仓信息和市场状态以表格的形式展示,并记录在 tblStatustblMarket 中。
    • 更新日志状态,显示最新的持仓信息和市场状态,以及持有的交易品种数量。
    • 如果有持仓,则记录手动恢复字符串,用于手动恢复持仓状态。
    • 根据设定的循环间隔(LoopInterval)等待一段时间后,重新开始下一轮循环。

通过这些完善的设计,希望能够有效地帮助大家理解多品种海龟交易策略。海龟交易逻辑并不复杂,但是怎样将这种逻辑使用程序的语言一步步搭建起来,确实考验我们的能力和耐心,我们要做好各个品种,各个交易环节,程序的容错以及结果呈现等一系列的工作,希望通过本策略学习,可以帮助大家了解一下一个真正的实盘级别的大模型应该怎样搭建。当然,这对比于一个工业级的实盘大模型确实比较稚嫩,这里重要的是,理解并掌握这里的程序设计,学会一个复杂模型的搭建方式,希望大家都有所领会。

第9章 用科学的方法进行回测

前面的章节详细介绍了如何开发一个交易策略,包括 CTA 趋势策略和震荡策略。一个新开发出来的交易策略,需要全方位检测才能应用于实战,同样一个优秀的策略也是在试错中不断改进得以产生。因此对于平台的回测机制,回测优化手段,我们需要全面认识,以便更好的进行回测。

9.1 回测数据级别

量化交易回测的目的是还原交易过程,进而验证策略的逻辑和可行性,所以回测的准确性尤为重要。CTA 策略有很多种风格,从频率上来讲:有高频策略、中低频策略。从另一个角度来讲:有日内策略,也有隔夜策略。通常不同类型策略对于回测时选用的数据也是不同的。

9.1.1 回测需要哪些数据

如何做到精准回测是很多量化交易者关心的问题,那么首先要弄清楚回测中都有哪些数据,因为数据的质量很大程度上已经决定了回测的质量。常见的数据有开盘价、最高价、最低价、收盘价、成交量等等,这些统称为 K 线数据。

另外还有一种原始的 Tick 快照,如果把交易所内的数据想象成一条河流,其中包含每个订单的详细数据,那么 Tick 快照就是这个数据流中的某个切片,频率是每秒 2 次,是当时某一时刻市场交易情况的再现。

事实上 K 线数据就是基于 Tick 数据合成的,按照时间周期 1 分钟的 K 线数据是由 1 分钟内的 Tick 数据组成,5 分钟的 K 线数据是由 5 分钟内的 Tick 数据组成,以此类推......形成了各种分钟图、小时图、日线图等等。这就意味着一分钟的 K 线数据可能包含 120 个 Tick 数据。因此,回测的历史数据可以分为:K 线数据和 Tick 数据,并且在同一个周期内 Tick 的数据量要比 K 线数据量大很多,理论上 Tick 数据比 K 线数据回测更加准确。

9.1.2 基于 Bar 数据的回测

市面上量化交易软件几乎都支持 K 线数据的回测,由于数据量少,大大简化了回测引擎的工作量,所以这种回测通常都非常快,十年左右的数据几秒之内就能回测完,甚至叠加几十个期货品种回测也不会超过一分钟。但是 K 线数据回测有很多问题:

极端价格
做过交易的人都知道,在涨停中很难买入,在跌停中很难卖出,但是在回测中是可以成交的,一些做量化交易的新手,如果不在策略中对涨跌停价进行过滤,回测的结果会与实盘不一致。

价格真空
从跌停瞬间到涨停或者价格跳空上涨,在大周期 K 线图上看是一根大阳线,但是这根 K 线中间的实质挂单很少,如果是即时价成交的策略,在 Bar 数据上回测,是可以成交的。举个例子,当前 K 线一直在 5000 价格附近徘徊,临近收盘瞬间涨至 5100,并且中间几乎没有挂单和成交。如果策略信号在这根 K 线上是 5050 开仓,那么在 K 线数据回测中是可以成交的。

过去和未来的数据
理论上 K 线的形成可能是:开盘价>>>最低价>>>最高价>>>收盘价。但实际上它有可能先创新高,再创新低,再收盘;也有可能先创新低,再创新高,再收盘;甚至也可能一波三折先创新低,再创新高,再创新低,再拉高收盘;表面上看是一根有上影线和下影线的 K 线,中间的过程有很多种可能。

假如有一根 K 线是这样的:开盘价 4950、最低价 4900、最高价 5100、收盘价 5050,一根普通阳线。策略是:如果最新价超过前期高点 5000 就买入,买入后设置 1%的止损,也就是价格跌破 4950 就止损。

模拟回测:开盘 4950>>>价格超过前期高点 5000>>>信号成立买入开仓>>>收盘时赚了 1%;但真实的情况可能是这样的:开盘 4950>>>价格超过前期高点 5000>>>信号成立买入开仓>>>不久价格开始下跌>>>继续下跌至 4949>>>止损信号成立卖出平仓亏损 1%>>>价格上涨 5100>>>价格下跌至 5050 收盘。同样的策略,在 K 线数据回测和真实的交易中,会有两种截然不同的结果。

9.1.3 基于 Tick 数据的回测

如果能用 Tick 数据来做回测和分析,无疑具有很大的优势,但目前市面上很少能真正做到,有些量化交易软件只是使用了 Tick 价格,并没有使用 Tick 挂单量,可能造成见价成交的现象。比如当前的 Tick 数据是:卖价 5001、买价 5000,如果挂的买单是 5000,结果肯定是买不到的,但事实并非如此。

在真实的交易环境中,订单是在交易所的 Tick 数据流中完成撮合的,交易所的撮合规则是:价格优先、时间优先。如果此时盘口订单只要不是太厚,那么所挂的 5000 买单,是有可能被动成交的。

9.1.4 盘口数据回测引擎原理

真正的 Tick 数据回测不仅根据 Tick 数据的价格优先来撮合订单,还根据价格相同时间优先,通过计算盘口挂单量,来判断当前挂单是否达到被动成交的条件实现见量成交,以此做到真正的模拟实盘环境。以下图为例:

image

首先第 1 个 Tick 买价是 100,挂单量是 30 手;此时产生了买入信号,以 100 的价格买入 20 手等待被动成交;第 2 个 Tick 产生了,买价是 100,挂单量是 50 手,这里面有我们 20 手的挂单;第 3 个 Tick 产生了,买价是 100,挂单量是 30 手,这证明已经有 20 手买单被成交了或者撤单了,我们离成交又近了一步;第 4 个 Tick 产生了,买价是 100,挂单量是 10 手,是一个大卖家,一下子把我们买单全部成交。

通过上面的例子,我们可以发现,在 Tick 数据中,价格未变的前提下,可以通过盘口挂单量的变化,来推算自己的挂单有没有被动成交。利用的就是价格相同,时间优先的方法。这种回测引擎几乎最大程度的仿生了真实的实盘交易环境,杜绝了见价成交和虚假成交,让每一个盘口数据真实回放,使回测与实盘最大可能的一致。

9.1.5 如何选择最佳回测方式

通常中低频策略交易次数不多,滑点成本对策略的影响较小,对数据精度要求也不是太高,所以一般情况下使用 K 线数据回测,只需要在回测的时候加上几跳的滑点就可以,真正需要注意的是过度拟合的问题。

有些日内交易或者涉及到日内开平仓交易的策略,如果有必要,也可以在回测的配置参数页面上调整数据粒度,比如在 1 小时周期上回测,可以调整为更精细的 15 分钟数据力度。必要时也可以使用 Tick 级别的数据,来提高回测的精准度。

注意:如果是隔夜的中低频策略尽量以商品指数为数据,如果是日内的中低频策略尽量以商品主力连续为数据。

高频交易因为策略交易的次数足够多,单品种一天就能交易几十甚至上百次,所以只要撮合引擎是合理的,那么在大数定律的作用下,回测的结果基本靠谱,一般不存在过度拟合的问题。但是由于高频交易的次数很多,因此就需要对回测数据有非常高的要求。

因为在高频交易回测中,交易频率越高,持仓的时间周期就越短,单笔的平均利润就越低,此时如果回测引擎设计的不合理,或者撮合方式与真实的交易环境不一样,那么就会出现差之毫厘,谬以千里的现象,所以对于高频交易来说,盘口级别的回测引擎是不二之选。

9.2 回测绩效报告详解

量化交易与主观交易最重要的区别之一是量化交易通过历史数据复盘,得出一系列绩效报告,交易者可以从报告中发现策略的缺点来优化和改进策略。当然优化和改进策略的前提是需要对策略有一个正确的评价,那么就需要对回测的绩效报告有一定的了解。只有正确解读回测绩效报告,才能知道策略需要改进的方向。

9.2.1 年化收益率

年化收益率表示投资期限为一年的理论收益率,日收益率、月收益率、季度收益率都可以换算成年收益率。如果一个策略回测的日收益率是 0.01%,那么年化收益率是 3.65%。其计算公式为:(收益 / 本金) / 投资天数 * 250 ×100%

注意:策略回测的年化收益率不是从开始开仓的时候算起,而是从数据开始日期算起。实际上年化收益率代表了策略的盈利效率。另外期货市场其有效的投资时间是一年的交易日,扣除节假日约等于 250 天。

9.2.2 年化波动率

波动率是衡量策略风险的指标之一,它是描述策略资金曲线的涨跌幅程度,是对策略稳健性的衡量,也反映了策略风险水平。其计算方式为:(最高价 - 最低价) / 最低价所得到的比率,年化波动率就是每日波动标准差的年化。

波动率越高,其资金曲线的波动越激烈,策略的稳健性就越低。波动率越低,其资金曲线的波动越平缓,策略的稳健性就越高。

9.2.3 最大回撤比率

除了波动率外,更能直观反映风险的绩效指标就是最大回撤率。它是统计资金曲线任意周期内最高点到最低点时的回撤幅度的最大值。它是描述策略可能出现的最糟糕情况。最大回撤是一个重要的风险指标,对于量化交易而言,该指标甚至比波动率还重要。

9.2.4 夏普比率

很多人喜欢用收益率衡量一个策略,这个无可厚非,因为从投资交易的角度讲,只要赚钱的策略都是好策略。但是请看下图:

image

左边的策略收益是 100%,右边的策略收益也是 100%,而左边的策略最大回撤是 50%,右边的策略几乎没有回撤,毫无疑问右边的策略要明显好于左边的策略。所以仅仅用收益率评价一个策略是不科学的。

回撤意味着风险,也意味着波动,正确的方式是将收益率和风险都考虑在内,也就是说不但要考虑收益率,更要考虑每承担每一单位风险所产生的超额收益。夏普比率就是一个对收益和风险综合考虑的指标。其公式为:(策略收益率-无风险利率)/策略收益率的标准差

举个例子,假如十年期国债收益率是 3%,而策略回测的收益率是 15%,那么超额收益就是 15%-3%=12%,12%除以 6%的策略收益标准差等于 2。这就意味着交易者每承担 10%的风险,能得来 20%的超额收益。

每个策略回测都可以计算夏普比率,如果值为正数,则表示策略收益大于策略波动风险;如果值为负数,则表示策略波动风险大于策略收益。也就是说在设计策略时要考虑风险,尽量用最小的风险换取最大的回报。

9.3 如何避免回测陷阱

量化交易回测虽然可以快速验证策略在历史数据中是否有效,但很多时候回测并不代表未来能盈利,回测看起来非常好的策略,往往实盘表现不佳。因此需要在策略设计过程中规避回测的陷阱,才能让策略回测反映真实的结果。

9.3.1 未来函数

未来函数就是利用了未来的价格,交易策略如果包含未来函数,在实盘运行时会造成信号闪烁的问题。比如有一个策略逻辑是这样的:当收盘价大于开盘价就买入,当收盘价小于开盘价就卖出。这在回测时是没有问题的,因为收盘价是已经完成,固定不变的数据。

注意:在实盘交易中,收盘价只有在收盘的时候才能固定下来,所以程序会把当前的最新价格当作收盘价,这种利用未来价格的策略,会导致买卖信号频繁出现和消失。如果一个策略的买卖点不是固定的,回测的数据也是没有意义的。

如何避免使用未来函数?最简单的办法是使用滞后的价格,可以把这个策略条件修改为:当上根 K 线收盘价大于开盘价就买入,当上根 K 线收盘价小于开盘价就卖出。因为在无论是在回测中还是在实盘中,上根 K 线始终是已经完成的,这样就可以保证回测与实盘保持一致。

9.3.2 偷价

相反偷价是利用了已经过去的价格,偷价并不会造成信号频繁出现和消失,但是会造成信号无效。比如有一个策略逻辑是:当收盘价大于开盘价就在开盘时买入,当收盘价小于开盘价就在开盘时卖出。

显然这个策略条件在实盘时是不能成交的,当收盘价出现时,开盘价早就过去了。但是在回测中,程序是会以开盘价买入卖出的,这相当于在原本的资金曲线上叠加了一条斜率为正的直线会造成一种非常夸张的回测资金曲线。

为避免这种情况发生,编写完策略首先要检查策略逻辑,如果策略回测的收益曲线非常平滑,回撤极小,就要警惕了。尤其是策略逻辑存在隐蔽性偷价行为,务必在实盘之前先用仿真交易测试一段时间。

9.3.3 成本冲击

实盘交易中为了保证订单能及时成交,通常需要用对手价或者市价下单,商品期货买一价和卖一价至少相差一个点差,如果是交易不活跃的期货合约就需要更多的点差成本。或者当自己的订单量超过市场现有的订单量时,就会造成自己的订单消耗了市场流动性,触动价格朝向不利于自己的方向移动,使交易成本进一步上升。

不仅如此,手续费、极端行情、软硬件系统、服务器响应、网络延迟都会增加实盘的交易成本。尤其是交易频率比较高的策略对受市场冲击成本更大,为了让回测更接近实盘环境,折中的办法是在回测时加上固定 2 跳左右的滑点。

9.3.4 幸存者偏差

幸存者偏差是一种逻辑上谬误,意思是没有意识数据筛选的过程,忽略被筛选掉的数据,只通过筛选后的数据得出与实际偏离的结论。

image

左边的图是一个非常好的交易策略,资金曲线稳稳向上,没有最大回撤。再看看右边这张图,这个资金曲线只是 100 次随机交易回测中表现最好的一个。通过这个例子可以知道,回测也有运气的成分,有时候的回测结果可能这个策略刚好适应了历史数据,再换几个参数或者回测品种就不一定有这么好的结果了。

9.3.5 过拟合

过拟合是统计学中的术语,它是指过于精确地匹配数据特征,以至于无法在其他数据中良好地拟合,量化交易中的过拟合是一种回测时表现很好,实盘中表现较差的现象。

image

上图分别是模型欠拟合、适当拟合、过拟合的素描,实际上策略回测应该像第 2 张图那样在数据中匹配普遍规律,而不是像第 3 张图那样试图匹配所有规律,这样才能在新的数据中更好地适应,否则将会导致策略泛化能力下降。

由于商品期货历史数据有限,所以过拟合问题就更加严重,尤其是对于中低频策略来说,几乎不可能完全避免过拟合,但可以利用下面几种方法来减少拟合:

  1. 减少核心参数
  2. 简化处理逻辑
  3. 增加数据样本
  4. 样本内外测试

如果策略核心参数过多或策略逻辑非常复杂就很容易过拟合历史数据,尤其是当数据样本过少时,不足以策略获得整个全局特征,如果在样本过少的情况下企图验证策略是否有效,无异于坐井观天,可能会把回测数据自身的特性当成所有潜在样本的共性,这样一来策略再面对实盘时就不适应了,当样本数据足够时,就不会被局部特征所迷惑。

9.4 递进和交叉回测

巴菲特曾经说过:“投资市场里,后视镜永远比挡风玻璃让你看得更清楚。”他的投资哲学是,除非投资标的的“过去”和“未来预期”一样稳定可靠,否则绝不投资。量化交易回测就像是后视镜,如何通过回测判断策略预期,就得使用科学的回测方法。

9.4.1 样本内和样本外回测

期货交易往往当时很难理解,但事后分析起来很简单,那是因为复盘总是站在上帝视角。量化交易回测也同样存在这个问题,回测是站在数据的终点往数据的起点看,那么企图在有限的数据中发现规律,无异于坐井观天。

为了解决这个问题,通常是把数据分成样本内数据和样本外数据。样本内数据相当于上课学习的课本知识,样本外数据相当于课后作业和期末考试。而量化交易中是在样本内数据上进行策略参数调优,在样本外数据上验证策略是否有效。

数据的划分并没有严格的要求,但至少有几个原则:在数据有限的情况下,通常样本内与样本外的比例是:6:4 或者 7:3;如果数据足够,则这比例可以更宽限一些,可以分为:5:5 甚至 2:8;尤其是逻辑简单的日内短线策略,可以减少样本内数据,更多的分配给样本外数据。

9.4.2 样本递进回测

但是仅凭一次测试就判定策略的好坏显然是不太合理的,因为回测的结果也有运气的成分,有可能保留了不好的策略,也有可能把好的策略筛选掉。那么样本递进回测将是一种更好的回测方式。

样本递进回测是将数据分为多个阶段,每个阶段又分为样本内数据和样本外数据,通过对样本内数据优化得到策略参数,再应用到样本外进行检验,并且不断以递进方式移动样本段,最后将所有样本外的测试报告组合成一个整体的回测绩效报告。

注意:在实际应用中,可以通过改变测试数据的长度,进行多次测试,用来判断策略在应对非平稳数据的稳定性。

9.4.3 样本交叉回测

除了递进回测外,还有一种交叉回测方式。这也是一种动态的回测方式,将数据分为多个阶段,每个阶段又分为样本内数据和样本外数据,通过对样本内数据优化得到策略参数,再应用到样本外进行检验,只不过交叉回测的样本外数据是贯穿整个样本,最后将所有样本外的测试报告组合成一个整体的回测绩效报告。

交叉回测最大的优点就是充分的利用有限的数据,每个样本内数据同样也是样本外数据。但交叉回测应用时也存在明显的缺点:

  1. 当价格数据非平稳时,模型的测试结果往往不可靠。例如,用 2008 年的数据做样本内,用 2005 年的数据做样本外。很有可能 2008 年的市场环境与 2005 年相比发生了很大变化,所以策略测试的结果不可信。

  2. 与第一条类似,在交叉回测中,如果用最新的数据作为样本内优化策略,而用较老的数据回测策略,这本身就不符合逻辑。

优秀的交易策略应该能够在未来具有获利性,很多时候直接在全部的历史数据上选择最优参数是非常危险的,如果先利用样本内数据进行参数优化,再利用样本外数据进行样本外测试,除了能客观检测交易策略外,更能效率节省时间。

量化交易策略本质上就是一个从大量的貌似随机的数据中找寻局部非随机数据的过程,如果不借助统计学的知识,很容易数据陷阱中。如果发现样本外数据表现不好,又觉得丢掉策略太可惜或者不愿意承认这个策略不行,而对着样本外数据继续做优化,直到样本外数据上也表现得一样好,那最后受伤的一定是自己的真金白银。

但即便拥有庞大数据的历史,但面对浩瀚无尽且不可预测的未来,历史就显得极度匮乏。所以基于历史自下而上倒推出来的交易系统,终究会随着时间而沉没。因为历史不能穷尽未来。因此一个完整的正期望交易系统必须由其内在原理或逻辑所支撑。

第10章 风险管理与投资组合

量化交易最关键的常识就是风险,但对于大多数交易者来说,风险是一个令人不愉快的话题。虽然严格控制风险意味着与暴利绝缘,但对于优秀的量化交易者来说资金和风险管理绝对有必要。

10.1 认识期货中的风险

大部分初学者热衷于谈交易技术,这个无可厚非。因为技术是基础的东西,初入市场的人最直观可以学到的。技术就像你在撸代码中使用的编程语言、或者库、或者框架,合理使用技术会初步积累你在市场中的优势。但资金管理也同样重要。

10.1.1 系统性风险

在量化交易中,系统性风险是指与整个市场相关的风险,无法通过个体交易行为控制或规避的风险因素。这包括宏观经济因素、全球金融市场的波动、政治事件等。例如,金融危机或全球经济衰退会导致市场整体下跌,从而影响到所有交易者的投资组合。对于量化交易者来说,面对系统性风险,最常见的风险管理方法是通过多样化投资组合,利用相关性较低的资产进行对冲,以降低整体风险暴露。

10.1.2 人为主观风险

人为主观风险在量化交易中同样存在,尽管交易决策主要依赖于算法和模型,但交易者的主观情绪和行为仍然可能影响交易结果。例如,情绪化的交易决策、过度自信导致的过度交易以及缺乏纪律性的行为可能会使交易者偏离原定的策略,从而增加亏损的风险。为了应对人为主观风险,量化交易者通常会通过建立严格的交易规则和纪律,以及采用自动化执行交易的方式来减少情绪干扰,确保交易的客观性和一致性。

10.1.3 策略风险

在量化交易中,策略风险指的是与交易策略本身相关的风险,包括模型不准确、参数选择不当、市场条件变化等因素。即使是经过充分测试和优化的交易策略,也可能在实际交易中遇到亏损。为了降低策略风险,量化交易者通常会采取多种策略多样化投资的方式,以及建立有效的风险管理措施。这可能包括对策略进行定期检查和优化,灵活地调整参数和模型,以适应不断变化的市场条件,从而降低策略风险并提高投资组合的稳定性。

10.1.4 资金管理的意义

就如上面所述,期货市场的风险规模大,涉及面广,具有放大性、复杂性与可预防性等特征。期货的风险成因主要有价格频繁波动、保证金交易的杠杆效应、非理性投机及市场机制不健全等等。

因此,我们需要正确对待资金管理。资金管理的主要功能是控制净值回撤并提高交易稳定性,而不是单纯追求盈利。通过合理的总仓位控制亏损,可以确保交易策略能够持续执行而不至于资金耗尽。需要明确的是,资金管理并不能将一个本身是负期望的交易策略从亏损状态扭转为盈利状态。然而,它可以避免一个本身是正期望的交易策略由于不合理的资金管理而从盈利状态转变为亏损状态。因此,虽然资金管理并非万能,但它是交易系统中不可或缺的一部分。

10.1.5 资金管理的方法

第 1 种:固定百分比资金管理

固定百分比资金管理,是一种非常流行的方法,同时也比较稳健,它的优点是在回撤期,延缓本金的下降速度,而在行情来的时候,又可以加快本金的上涨速度。并且它的方法也非常简单,所以建议初学者采用这种资金管理方式, 它的计算公式如下:

$f^* = \frac{{\text{剩余本金} \times N\%}}{{\text{止损距离(点)} \times \text{每一点代表的金额}}}$

其中, N 代表策略设定最大能承受的亏损额度的百分比, 止损距离就是开仓点减去止损点。

第 2 种:赢冲输缩
赢冲输缩是一种资金管理策略,旨在通过调整交易时的头寸大小来控制风险,并延长投资者在市场中的生存时间。在期货市场中,这种策略可以通过以下方式进行应用:

赢冲(Winning Streak): 当投资者获利时,根据赢冲策略,他们会增加投资的头寸大小。例如,假设投资者开始时拥有100元,每次交易只投入总资金的10%。如果第一次交易赢了,他们会将获利后的资金中的 10% 增加于下一次交易,以此类推。这样做的目的是在盈利时增加投资规模,以获得更大的收益。

输缩(Losing Shrink): 相反,当投资者遭受亏损时,根据输缩策略,他们会减少投资的头寸大小。假设上一次交易亏损了,投资者在下一次交易中只会使用剩余资金中的 10% 作为投资,而不是总资金的 10% 。这样做的目的是在亏损时减少投资规模,以降低亏损带来的损失。

在期货市场中,赢冲输缩策略的应用可以帮助投资者更有效地管理风险。通过在盈利时增加头寸大小,在亏损时减少头寸大小,投资者可以更灵活地调整自己的交易规模,从而保护资金并延长在市场中的生存时间。这种策略的优势在于在盈利时能够加大收益的同时,通过在亏损时减少头寸大小来控制风险,从而保持资金的稳定性。

第 3 种:分散式交易

华尔街有句名言:不要把所有的鸡蛋放在同一个篮子里面。多元化资产配置,是唯一的免费午餐。单品种的稳定性和抗风险性极差。并且单个品种的市场容量是有限的,大资金出入会对市场造成很大的冲击,出入场都很难成交在理想价位。品种分散,还可以参与150多个全球市场, 从商品,黄金,到货币和股票指数等。除了品种分散,还可以多策略、多参数、多周期组合。从而达到削峰填谷的目的。

第 4 种:凯利公式

凯利公式是一个特定赌局中,使得拥有正期望值之重复行为长期增长率最大化的公式。公式如下:

math
f* = (bp - p) / b = (p(b+1) - 1) / b

其中:

  • f*为现有资金应进行下次投注的比例;
  • b 为投注可得的赔率( 不含本金) ;
  • p 为获胜率;
  • q 为落败率, 即 1 - p;

注意,这个公式的适用范围是反复多次下注的场景。这是一切赌戏和投资最基本的道理,也就是『没有把握,决不下注』-不下注,就不会输。暴力背后往往背负着风险,

在期货市场中,凯利公式同样可以用于确定每个交易的头寸大小,以最大化长期利润。以下是应用凯利公式在期货市场中的一般步骤:

  1. 确定期望收益率和赔率: 首先,需要对交易进行分析,确定每个交易的期望收益率和赔率。期望收益率可以通过技术分析、基本面分析或其他方法来估计,而赔率则取决于交易的结构和条件。例如,如果你期望一笔交易的收益率是10%,而止损和获利点的比例为2:1,那么赔率就是2。

  2. 计算获胜概率: 接下来,需要估计每个交易的获胜概率。这可能需要基于历史数据或者技术分析来进行。对于某些交易策略,可能需要经验或模拟交易来确定获胜概率。

  3. 应用凯利公式: 一旦确定了期望收益率、赔率和获胜概率,就可以使用凯利公式来计算每个交易的头寸大小。

  4. 确定头寸量: 最后,将计算得到的 f* 与可用资金相乘,得出每个交易的头寸量。这将确定在每个交易中应该投入的资金比例,以最大化长期利润。

需要注意的是,凯利公式提供的是一种理论上的最优投资策略,但在实际应用中,仍然需要考虑到市场条件、个人风险偏好以及其他因素。因此,投资者可能会根据自己的情况对凯利公式的计算结果进行调整。

资金管理的重要性无论怎么强调都不为过,当然不同的交易者有不同的资金管理方式,如同没有策略圣杯一样,没有一种资金管理是适合所有投资方式的,适合的才是最好的。

10.2 等价鞅资金管理

鞅是概率论中的一种随机过程,也就是说这个随机过程未来的期望值与当前时间点的值相同。在量化交易中鞅代表了收益期望为 0,如果某个策略的交易过程是鞅,那么在理想情况下其未来的净值跟当前的净值一样。

10.2.1 什么是马丁格尔

马丁格尔是一种等价鞅式的资金管理方法,英文直译为:martegal,最初是指控制马车的马具。后来马丁格尔代表一种赌博策略。最早可以追溯到十八世纪,历经几百年经久不衰,直到现在还有很多马丁格尔或者类似的策略。

马丁格尔最初应用在轮盘赌博中,逐渐延伸到金融交易中,直到现在在股票、期货、外汇,还能看到马丁格尔的影子。之所以经久不衰,是因为从理论上讲,这是一种永不亏钱的策略。

10.2.2 正向马丁格尔

这种永不亏钱的秘诀在于,在每次赔钱后,将赌注加倍,而在任何一次赢钱之后,将赌注回归到初始单位。无论在赢钱之前输了多少次,只要概率让下注者赢一次,不但能赢回之前所有的损失,还外加一次赌注的收益。马丁格尔在金融市场中,创造了很多盈利奇迹和滑铁卢的亏损。

以抛硬币为例,出现正反面的概率约等于50%,连续出现正面或反面的次数,都是以 50%的概率开始递减,也就是说在任何一次抛硬币中,出现正面的概率是 50%,连续 2 次出现正面的概率是 25%,连续 3 次出现正面的概率是 12.5%等等以此类推。

如果最开始赌注是 1 元,连续输钱的赌注以 2 的倍数增加,也就是:1、2、4、8、16、32、64、128、256、512 等等,直到赢钱为止,一个回合才结束,所以每个回合都能赢得 1 元。虽然在纸面上,马丁格尔可以做到永不亏钱,但是随着一连串的损失发生,赌注的规模会呈几何倍的速度增加。

注意:为了避免资金雄厚的赌徒运用这个策略,几乎所有的赌场对每一次赌局有最高的下注限制。

10.2.3 反向马丁格尔

与正向马丁格尔相反,反向马丁格尔则是每次赢的时候,将赌注加倍,直到出现赔钱的时候将赌注回归到初始单位。这是马丁格尔策略的延伸,理论上这种策略更适合用在趋势行情中,因为顺势而为的操作有很高的成功率。成功率的提高伴随的是逐步加仓获取的超额收益。

10.2.4 马丁格尔在期货市场上的应用

虽然在期货市场并没有最高下单量的限制,但与赌场不同的是,期货的涨跌并不是完全随机赌大小,真实的金融交易市场要比赌场更加复杂。如果将马丁格尔策略用在期货交易中,一旦市场按照反方向趋势行情运行,后面随着行情的发展,头寸翻倍增加会越来越大,风险也随之加大。那么对于想要使用马丁格尔策略用于期货市场的交易者来说,至少需要解决 3 个问题:

  • 起始仓位
  • 加仓倍数
  • 加仓距离

注意:起始仓位需要根据你的资金量而定,也就是在交易之前计算好资金能承受的最大连续亏损次数。如果起始仓位过高,会导致每次翻倍加仓后投入过大的资金量。另外加仓倍数太高也会导致同样的问题,马丁格尔默认是双倍加仓,如果设置成 3 倍加仓,破产的速度会更快,但如果设置成 1.5 倍加仓,就会出现另一种结果。最后需要考虑的是加仓的距离,比如在 5000 点开多单,价格下跌 15 点加仓,和价格下跌 30 点加仓,也是不一样的。这点完全取决于交易者的风险承受能力和交易习惯偏好。

10.3 构建投资组合和风险控制

有人曾经做过一个研究,长期来看,整个投资收益中来自择时部分的还不足 5%,剩下的约 90% 的投资收益都是来自于成功的投资组合,这个人就是“全球资产配置之父”加里·布林森。

10.3.1 投资分散与均衡

一个好的投资组合有助于家族财富的传承,在财富的更替洗牌中,那些历经百年依然屹立不倒的家族,绝非偶然。比如洛克菲勒家族、摩根家族、罗斯柴尔德家族等等。基本都是靠某个行业发家,而后代则是靠均衡的资产配置,使这些财富长期保持增长。

在量化交易中,一个好的投资组合可以在跌宕起伏的金融市场中起到“削峰填谷”的作用。均衡的资产配置就跟合理的搭配膳食一样。营养学界流传着这样一种说法:没有不好的食物,只有不合理的膳食。

但是在投资领域“资产配置”这个词一直饱受争议。有人说,不能把所有的鸡蛋放在同一个篮子里;也有人说,与其鸡蛋放在多个篮子里,不如放在一个篮子里,然后看好这个篮子。特别是中国过去十几年快速发展,导致资产价格升值过快,在房价暴涨的背景下,只需要买房,就能轻易的跑赢通货膨胀,均衡的资产配置显的可有可无。但现在不一样了,国内经济增速明显下降,资产价格持续回落,加上全球货币宽松,资产变化轮动加快。特别是在高速通胀环境下,单一的资产配置的时代已经过去。

10.3.2 投资组合分类

商品期货只是量化交易投资组合中的一部分,其中还包括:基金、债券、股票等等,从收益的角度看:基金<债券<股票<期货。从风险的角度看也是同样的顺序:基金<债券<股票<期货。这些都可以用于投资组合。

除了多品种组合外,也可以将不同的策略组合到一起,比如:CTA 趋势策略、CTA 震荡策略、跨期套利策略等等。在实际应用中,不同周期的数据也可以组合到一块,比如:15 分钟、1 小时、2 小时、日线数据组合到一块。

10.3.3 构建投资组合

通过将上述这些不完全相关的资产加入到组合中,就可以有效降低系统性风险,同时收益最大化;也可以在总体回报率不变的情况,大概率降低亏损的概率,波动(风险)最小化。

  • 鸡蛋不要放在同一个篮子里(分散)
  • 篮子也不要放在同一个地方(分散)
  • 不要一次性把鸡蛋都放进去(定投)
  • 也不要在篮子里只是放鸡蛋(多元)

并且最大程度分散风险,稳健增值。即使在不利的市场环境中,也能具备足够的防御性。另外,投资是反人性的,科学的投资组合,可以稍微顺从一些人性,提高投资的容错率。

在投资组合中,一定要先解决保障性配置,也就是保险。它是整个资产配置的前提和基础保障。另外配置低风险、低收益的货币基金,兼顾日常消费和周转资金的流动性。其次配置一些用来跑赢 GDP 的投资品种,确保整个投资组合有好的收益基础。低风险的政府债券、企业债券、债券基金、黄金、房地产等是较好的选择。

如果想要跑赢“印钞机”的速度(M2),就需要配置一些高风险、高收益的资产,比如:个股、ETF、股票型基金、混合型基金;或者风险更高的美股、港股、商品期货、外汇现货等。最后,用很少的一部分钱,去博胜率小赔率大的机会。比如:项目股权、数字货币。

10.3.4 收益与风险

投资组合没有固定的模式和比例,一切取决于自己的风险偏好、收益预期、投资期限,将资金在不同资产类别之间合理分配。从收益风险比的角度可以划分为:保守型、稳健型、激进型。

注意:如果你对风险的承受能力较弱,就需要多配置些第 1 条和第 2 条;如果你对收益要求比较高,就需要多配置第 2 条和第 3 条。

但这并非一成不变,而是因人而异,并根据资本市场行情动态调整。通常股市与债券的相关系数大约为 0.6,如果股市比较低迷,那么债券市场相对比较火爆。比如,2008 年股市低迷的时候,债券市场风生水起。

客观的讲,摆在投资者面前的工具,并不只有投资组合,还有品种选择和市场择时,但对于大多数人而言,真正有用的也就只有投资组合,而且往往还能兵不血刃,出奇制胜。总之,投资是一种极大的不确定性行为,任何风险会在任何时候,都可能一触即发,所以投资组合绝对不能缺席,因为在崩盘的时候会让你稍微好过一些,而且在最应该买入的时候,也能产生足够的现金。

第11章 交易技巧及交易理念

华罗庚在谈到读书方法的时候,提到“读书是由薄到厚再由厚到薄的过程”。交易也类似,刚开始什么都不懂自然就很“薄”,等到入门发现需要学习内容太多就感到很“厚”,随着日积月累,理解了交易的核心,升华到交易理念时就变的很“薄”。

11.1 止盈止损

交易界有句古老的谚语:“会买是徒弟,会卖是师傅”。顾名思义怎么卖比怎么买更难,因为在买入的时候只需要判断行情是否开始就可以了;但是一旦买进之后,不但需要判断行情是否转向,还需要时刻控制风险。相信很多交易者都经历过过山车的行情,明明上车了最后还是以小赚甚至亏损的结果出局。或者本来可以以小亏出局,结果从小亏损积累成大亏损。所以从这点看,卖出比买入更为重要。简单的说,卖出无非是两种情况:止盈和止损。如果运气很好,买入后价格开始上涨,这时就要考虑止盈的问题,否则可能账面上赚到了钱,没有在合适的位置获利了结,最后平亏出局。如果运气不好,买入后不久价格就开始下跌,这时就要考虑止损了,或者在开仓买入之前就应该考虑好止损的位置,否则小亏损会积累成大亏损。

从统计学的角度看,大部分亏损的单子,在之后的行情里都会回到成本价附近。但是如果遇到一次小概率大幅反向走势,可能会损失之前所有的利润或本金。因此对我们散户来说,可以大赚,可以小赚,也可以小亏,但是永远不能大亏。所以有这么一句话:止损让我们活着,止盈让我们活得更好。

11.1.1 止损的成本

你有没有过这样的经历?当价格朝开仓的反方向移动时,你可能认为是小幅度回调。继续移动时,你可能认为调整这么多了,价格也该反转了吧。但是如果行情继续如此反复下去,一般人能够忍受这样快速且长时间的回撤。这时“止损”也许是多数人心中所想并去执行的。

止损是有成本的,然而,当你止损之后,接着就出现了一段非常暴力又迅速的行情。回头一看。止损止到了低谷。结果明明是赚钱的单子,却以止损提前离场,还白白损失了前几次的试错成本。

面对这种情况,不仅资金缩水,更会有一种被左右愚弄的痛苦。特别连续出现几次这样的情况,对自信心的打击是难以承受的。以至于怀疑自己,为了止损却错失行情而感到不值,久而久之就不再止损。

曾经有家机构做过统计:在止损后,80%多的情况下,价格会重返原点附近。换句话说80%的止损是错误的,不止损的话有80%概率不仅不会亏损,甚至还会小赚。如果把周期拉的足够大,大多数股票、债券、商品、外汇,价格都在一个大的范围内上下波动,毕竟趋势行情的占比还不足三分之一。

那么,在行情中间,无论多,还是空,大部分时候,我们的单子即使被套,只要不止损,总能抗回来。甚至还能获得一些利润。嗯......表面上是这么个理,既然如此,为什么要傻乎乎止损呢。

11.1.2 止损的意义

假如没有止损,却真的碰到反向的大行情。那资金遭到重创,回本岂不更加困难了?我们可以尝试制作一个图表:

亏损率10%20%30%40%50%60%70%80%90%100%
盈利率11%25%43%67%100%150%233%400%900%本金赔光

如上图,如果在必要的时候没有割肉止损,而是心存侥幸抱着,期待价格能回归。结果可能需要很长的时间才能完成“回本儿”。看完以上,你是不是明白了交易一定要:坚决止损。你下次回归到成本的难度,取决于你上次是否保护了好你的本金。而赚多少取决于市场,亏多少几乎全部取决于自己。

虽然80%的止损都是错误的,但是为了避免80%的错误,不去执行止损,则在剩余的20%的行情中,巧合遇到单边大行情或大调整、大反弹,又是反向,就容易死亡。所以,为了避免20%的小概率的爆仓或大幅亏损的可能,我们却要去选择80%的大概率的止损的错误,这是没有办法的事情。因此,在大部分时候,我们止损,并不是我们方向错了,而是出于控制损失的需要。

在市场里,鳄鱼法则就是:当你发现自己的交易背离了市场的方向,必须立即止损,不得有任何延误,不得存有任何侥幸。鳄鱼吃人听起来太残酷,但市场其实就是一个残酷的地方,每天都有人被它吞没或黯然消失。

11.1.3 止损的本质

止损只是必要条件,而不是充分条件。它只是整个交易系统中的一个分支。前提是有一个正期望的交易系统,否则止损只是让你死的慢些。交易中八成的止损都是因为杂乱无序的波段造成的。

人生和投机一样,大部分都是自己打败了自己。合理的止损需要严格的条件判断,否则频繁的止损只会让人一点点步入失败的泥沼。可以这样说,止损本质上是对市场的敬畏、对不确定性的承认、对市场的尊重。会不会买只是我们能赚多少的因素之一,而会不会止损却是我们能亏多少的全部因素。未来是不确定的,错误的持仓,未必就是错误的方向。止损虽不能决定市场,但是却能界定你面对怎样的市场。

如果把交易比作生活,市场中所有的价格都是合理的,就跟生活一样,存在即为合理。就如同我们无法在生活中决定什么应该存在一样,无法决定市场的价格应该是什么。但我们依然可以决定我们能在生活中做什么,在市场中做什么。在生活中的底线,就如同在市场中的止损一样。

11.1.4 固定比例止盈止损

固定比例止盈止损策略,是一种基于盈利和亏损的比例,设定止盈点和止损点的交易策略。它的思想很简单,该策略基于设定的固定比例来确定止盈和止损点。当价格达到预设的盈利目标时,进行止盈;当价格下跌达到预设的损失限制时,进行止损。为了方便演示,本节课我们使用的都是多头止盈和止损的策略设计,对于空头的止盈止损策略,大家只需要改变少许参数就可以。

这个策略的思路是基于固定百分位止盈止损设计的。对于多头开仓,我们使用的都是当最新价格超越20均线时,进行入场。大家也可以设置别的入场信号,比如macd,双均线等等。具体代码实现是这个样子的:

python
def main(): buy_price = None take_profit_price = None stop_loss_price = None mp = 0 c = KLineChart({"overlay": True}) p = ext.NewPositionManager() while True: if exchange.IO("status"): LogStatus(_D(), "已经连接CTP!") exchange.SetContractType(symbol) records = exchange.GetRecords() if not records or len(records) < 20: Sleep(1000) # 等待获取足够多的K线数据 continue ma20 = TA.MA(records, 20) if mp == 0 and records[-1].Close > ma20[-1]: # 大于20均线,下多单 p.OpenLong(symbol, 1) pos = p.GetPosition(symbol, PD_LONG) if pos: buy_price = pos['Price'] mp = pos['Amount'] take_profit_ratio = 0.005 # 止盈比例 stop_loss_ratio = 0.005 # 止损比例 # 根据开仓价计算止盈和止损价格 take_profit_price = buy_price * (1 + take_profit_ratio) stop_loss_price = buy_price * (1 - stop_loss_ratio) if mp == 1 and records[-1].Close > take_profit_price: Log('止盈时间到') p.Cover(symbol) buy_price = None take_profit_price = None stop_loss_price = None mp = 0 if mp == 1 and records[-1].Close < stop_loss_price: Log('止损时间到') p.Cover(symbol) buy_price = None take_profit_price = None stop_loss_price = None mp = 0 for bar in records: c.begin(bar) c.plot(ma20[-1], "均线20") c.plot(buy_price, "买入线") c.plot(take_profit_price, "止盈线") c.plot(stop_loss_price, "止损线") c.close() else: LogStatus(_D(), "未连接CTP!") Sleep(1000*60)

首先设置一些变量,包括持仓变量mp,实时买入价格buy_price,止盈价格takeProfitPrice,止损价格stopLossPrice,为了提升交易的成功率,我们这里使用了交易类库,同样这里为了方便画图,使用KLineChart函数。按照固定的流程,首先检查ctp协议是否连接,接着设定合约,然后获取最新的K线数据和Ticker数据。等收集够足够的k线数量后,使用移动平均线(MA)计算20均线(ma20)。

如果当前持仓为空(mp == 0),并且当前Ticker的买入价格大于20均线的最新值,就使用OpenLong执行下多单操作。下单的价格和实时的仓位信息的获取,可以通过GetPosition函数,通过PriceAmount属性,获取实时的买入价格和持仓数量。固定的止盈止损比例是我们自己设定的,这里我们设定止盈比例takeProfitRatio和止损比例stopLossRatio,都是0.005,这两个比例大家可以根据自己的交易理念进行修改。接下来根据买入价计算出止盈价格takeProfitPrice和止损价格stopLossPrice。这就是我们设定的止盈点位和止损点位。

接着进入我们的止盈和止损设计了。对于止盈操作,如果持仓不为空(mp == 1)且K线数据的最新收盘价大于止盈价格,则执行止盈操作。使用交易类库的cover函数进行平仓。由于这一单已经完结,因此可以重新设置买入价,止盈价和止损价都为None,并将mp标记为0表示无持仓。对于止损操作,如果持仓不为空(mp == 1)且K线数据的收盘价小于止损价格,则执行止损操作。止损平仓操作完成,同样重新设置mp,买入价,止盈价和止损价。最后,使用KLineChart函数,将20均线、买入价、止盈价和止损价绘制在图表上。循环进行上述操作,实现实时的交易决策和止盈止损操作。

在回测结果中可以看到,我们设置周期为分钟,当达到开仓条件,会执行开仓的操作,并且设置相对应的止盈和止损线,当最新的价格达到固定比例的止盈或者止损价格的时候,会进行对应的止盈或者止损的平仓操作。总体来说,这个策略通过固定百分位止盈止损设计,根据价格与均线的关系进行交易决策,并且根据买入价设定止盈和止损价格,以限制风险和保护收益。除去固定比例之外,我们也可以使用固定的点位变化进行固定点数止盈止损操作。当盈利或者亏损达到一定点数之后,进行迅速的平仓,实现止盈止损的目的。

11.1.5 移动止盈止损

固定的比例或者点位进行止盈止损有时候太过于死板。当我们主动止盈时,可能会遇到一大波行情,只赚到其中一小部分就错过了后续更大的利润。虽然这样的交易并不亏损,但心理上会有一种错失良机的遗憾感。为了解决这个问题,移动止盈止损策略采用了浮动止损的方法,就是在获得不同级别的浮动盈利之后,开启下一级主动止损模式。这样一方面可以持续享受较高的浮动盈利,同时也不断提高止损点位,用来实现更多盈利的目标。这个策略实际上只有止损,没有止盈,随着盈利比例的移动,止损点也随之更新。这样可以更好地抓住市场的趋势,同时尽量的减少亏损。

这个策略是一个移动止盈止损的交易策略。下面是这个策略的思路:

  • 初始化一些变量,包括持仓状态(mp)、开仓价格(buy_price)、初始盈利比例(takeProfitRatio)和止盈止损比例(stopLossRatio);
  • 固定程序,连接交易所,获取K线和行情数据。检查K线数据数量;
  • 计算20日均线(ma20);
  • 空仓状态(mp == 0)使用20均线下多单/空单,记录开仓价格并将持仓状态置为持仓(mp = ±1);
  • 根据初始盈利比例和止盈止损比例计算盈利价格(takeProfitPrice)和止损价格(stopLossPrice),对于多头来说,是开仓价格乘以(1+盈利比例);对于空头来说,是开仓价格乘以(1-盈利比例),相应的止盈止损价格是盈利的价格乘以(1±止盈止损比例);
  • 如果当前处于持仓状态(mp == 1)且最新的收盘价大于(多头)/小于(空头)止盈价格,就要更新盈利比例,并更新盈利价格和止盈止损价格。

这里的关键是盈利比例和止盈止损比例的更新。首先通过条件判断确认当前处于持仓状态且当前收盘价上穿/下穿上一个阶段的盈利价格。如果满足条件,那么表示达到了新的盈利点,打印“达到新的赢利点,更新比例和价格”。接着,将止盈比例(takeProfitRatio)增加0.005。根据新的止盈比例,更新止盈价格(takeProfitPrice),对应的止盈止损价格也会相应更新。

根据新的止盈价格和止损比例,更新止损价格(stopLossPrice)。所以当价格呈现一定的上涨/下跌趋势,止盈止损的价格也会一直对应提升/下降。一旦出现当前处于持仓状态且最新的收盘价小于(多头)/大于(空头)止损价格的状况时,就会平仓并退出交易,重置开仓价格和相关变量。最后使用KLineChart进行绘图的操作。

该策略的核心思想是根据价格与均线的关系判断买入和卖出时机,并根据盈利情况逐步调整比例和止盈止损比例。在回测结果中可以看到,当价格达到新的盈利价格时,会增加盈利比例并更新盈利价格和止盈止损价格,从而保护已实现的盈利。而当价格达到止盈止损价格时,就会平仓并退出交易,可以限制损失。图表中我们也可以看到,相对于固定的止盈和止损点位,移动的止盈和止损线是会实时更新的。

通过使用移动止盈止损策略,我们可以更加灵活地应对市场变化,逐步提高止盈点位以获取更多的收益。这种策略能够在保护利润的同时,也给予市场更多的空间,以便捕捉更大的行情。当然,在实际操作中,需要基于市场情况和自身风险承受能力进行合理的设置和调整。总之,通过采用浮动止损的移动止盈止损策略,我们可以在控制风险的同时,最大限度地提高利润。这种策略的灵活性和适应性,使得我们能够更好地应对市场的变化。

python
def main(): mp = 0 # 持仓状态,0表示空仓,1表示持仓 buy_price = None # 开仓价格 takeProfitPrice = None stopLossPrice = None takeProfitRatio = 0.005 # 初始盈利比例 stopLossRatio = 0.01 # 初始止盈止损比例 p = ext.NewPositionManager() c = KLineChart({"overlay": True}) while True: if exchange.IO("status"): LogStatus(_D(), "已经连接到CTP!") exchange.SetContractType(symbol) records = exchange.GetRecords() if not records or len(records) < 5: Sleep(1000) # 等待获取足够多的K线数据 continue ma20 = TA.MA(records, 5) # 计算20均线 if mp == 0 and records[-2].Close > ma20[-2]: # 大于20均线,下多单 p.OpenLong(symbol, 1) if mp == 0 and records[-2].Close < ma20[-2]: # 小于20均线,下空单 p.OpenShort(symbol, 1) pos = exchange.GetPosition() if pos and pos[0].Type % 2 == 0: buy_price = pos[0].Price mp = pos[0].Amount takeProfitPrice = buy_price * (1 + takeProfitRatio) # 计算盈利价格 stopLossPrice = takeProfitPrice * (1 - stopLossRatio) # 计算止盈止损价格 if mp == 1 and records[-1].Close > takeProfitPrice: Log("达到新的赢利点,更新比例和价格") takeProfitRatio += 0.005 # 增加盈利比例 takeProfitRatio = _N(takeProfitRatio) # 格式化浮点数 takeProfitPrice = buy_price * (1 + takeProfitRatio) # 更新盈利价格 stopLossPrice = takeProfitPrice * (1 - stopLossRatio) # 更新止盈止损价格 Log("更新盈利比例:", takeProfitRatio) Log("更新止盈止损比例:", stopLossRatio) if mp == 1 and records[-1].Close < stopLossPrice: Log("达到止盈止损点,平仓并退出交易") p.Cover(symbol) buy_price = None # 重置 takeProfitPrice = None stopLossPrice = None takeProfitRatio = 0.005 stopLossRatio = 0.01 mp = 0 if pos and pos[0].Type == 1: buy_price = pos[0].Price mp = pos[0].Amount * -1 takeProfitPrice = buy_price * (1 - takeProfitRatio) # 计算盈利价格 stopLossPrice = takeProfitPrice * (1 + stopLossRatio) # 计算止盈止损价格 if mp == -1 and records[-1].Close < takeProfitPrice: Log("达到新的赢利点,更新比例和价格") takeProfitRatio += 0.005 # 增加盈利比例 takeProfitRatio = _N(takeProfitRatio) # 格式化浮点数 takeProfitPrice = buy_price * (1 - takeProfitRatio) # 更新盈利价格 stopLossPrice = takeProfitPrice * (1 + stopLossRatio) # 更新止盈止损价格 Log("更新盈利比例:", takeProfitRatio) Log("更新止盈止损比例:", stopLossRatio) if mp == -1 and records[-1].Close > stopLossPrice: Log("达到止盈止损点,平仓并退出交易") p.Cover(symbol) buy_price = None # 重置 takeProfitPrice = None stopLossPrice = None takeProfitRatio = 0.005 stopLossRatio = 0.01 mp = 0 for bar in records: c.begin(bar) c.plot(ma20[-1], "均线20") c.plot(buy_price, "买入线") c.plot(takeProfitPrice, "止盈线") c.plot(stopLossPrice, "止损线") c.close() Sleep(1000) else: LogStatus(_D(), "未连接CTP!") Sleep(1000)

11.1.6 动态止盈止损

动态止盈止损策略利用技术指标或其他市场信号来确定止盈和止损点。通过分析市场趋势指标,决定何时进行止盈和止损操作。因为指标实时计算的,所以这种策略能够根据市场情况作出灵活的决策。移动止盈止损和动态止盈止损虽然有些相似,但在概念和实施方式上存在一些区别。移动止盈止损是通过逐步调整止盈和止损点位的数值来实现的。比如,当获得一定的浮动盈利后,将止盈点位从初始设定的水平A移动到更高的水平B,以获取更多的利润。而动态止盈止损是根据市场行情和交易情况进行实时调整的。对于止盈点位,可以根据技术指标或者其他因素来设定触发条件,当满足条件时进行止盈操作。对于止损点位,可以根据风险管理原则和市场情况来灵活设定。

动态止盈止损的策略有很多种,以下是其中几种常见的策略:

  • 移动平均线策略:这个大家都很熟悉,该策略利用移动平均线来确定止盈和止损点位。
  • 波动带宽策略:这个前面我们也讲解过,代表策略是布林带等波动带宽指标,来确定止盈和止损点位。
  • 技术指标策略:该策略使用技术指标作为判断依据来设定止盈和止损点位。常用的技术指标包括相对强弱指标(RSI)、移动平均收敛/扩展指标(MACD)等。根据技术指标的信号来调整止盈和止损点位,例如当RSI超过一定阈值时设定止盈点位,低于一定阈值设置止损点位。
  • 支撑阻力位策略:该策略基于价格的支撑位和阻力位来设定止盈和止损点位。

我们来举例一个波动性止盈止损策略。当市场波动性较高时,设定较宽松的止盈和止损点;而当市场波动性较低时,设定较紧密的止盈和止损点。这样可以根据市场行情的不同情况来动态调整止盈止损的幅度。

波动性止盈止损的原理是这样的:

  • 波动性测量:首先,需要度量市场的波动性。常用的指标包括标准差、平均真实范围(ATR)等。这些指标可以帮助确定价格的波动范围。

  • 止盈/止损点位:接着根据波动性指标,可以设定一个合理的止盈/止损点位。止盈/止损点位应该超过当前价格的波动范围,以确保能够捕捉到足够的利润和限制损失。例如,可以将点位设定在标准差或ATR的倍数之外。

随着市场的波动性变化,波动性止盈止损策略的止盈和止损点位是动态调整的。例如,如果市场波动增加,可以适当扩大止盈和止损点位;如果市场波动减小,可以适当缩小止盈和止损点位。

python
import talib def main(): mp = 0 buy_price = None takeProfitPrice = None stopLossPrice = None c = KLineChart() p = ext.NewPositionManager() while True: if exchange.IO("status"): LogStatus(_D(), "已经连接CTP !") exchange.SetContractType(symbol) records = exchange.GetRecords() if not records or len(records) < 20: Sleep(1000) # 等待获取足够多的K线数据 continue ma20 = TA.MA(records, 20) if mp == 0 and records[-1].Close > ma20[-1]: # 大于20均线,下多单 p.OpenLong(symbol, 1) if mp == 0 and records[-1].Close < ma20[-1]: # 小于20均线,下空单 p.OpenShort(symbol, 1) pos = exchange.GetPosition() if pos: buy_price = pos[0].Price #使用条件表达式(三元操作符)来决定乘以1还是-1 mp = (pos[0].Amount * 1) if (pos[0].Type % 2 == 0) else (pos[0].Amount * -1) stdValue = talib.STDDEV(records.Close, 14)[-1] # 计算标准差 # 根据开仓价计算止盈和止损价格 upperPrice = buy_price + 2 * stdValue # 两倍标准差区间 bottomPrice = buy_price - 2 * stdValue if mp == 1 and records[-1].Close > upperPrice: Log('多头止盈时间到') p.Cover(symbol) buy_price = None # 重置开仓价格 takeProfitPrice = None stopLossPrice = None mp = 0 if mp == 1 and records[-1].Close < bottomPrice: Log('多头止损时间到') p.Cover(symbol) buy_price = None # 重置开仓价格 takeProfitPrice = None stopLossPrice = None mp = 0 if mp == -1 and records[-1].Close < bottomPrice: Log('空头止盈时间到') p.Cover(symbol) buy_price = None # 重置开仓价格 takeProfitPrice = None stopLossPrice = None mp = 0 if mp == -1 and records[-1].Close > upperPrice: Log('空头止损时间到') p.Cover(symbol) buy_price = None # 重置开仓价格 takeProfitPrice = None stopLossPrice = None mp = 0 for bar in records: c.begin(bar) c.plot(ma20[-1], "均线20", overlay = True) c.plot(buy_price, "买入价格", overlay = True) c.plot(upperPrice, "区间上限", overlay = True) c.plot(bottomPrice, "区间下限", overlay = True) c.plot(stdValue, "标准差", overlay = False) c.close() else: LogStatus(_D(), "未连接CTP !")

这个代码是一个基于波动止盈止损的简单示例策略,适用于期货交易市场。同样使用20均线作为做多和做空的操作。使用标准差来计算止盈和止损价格。标准差是衡量价格波动性的指标,这里取当前标准差的两倍作为波动上限和波动下限的价格区间。

实时波动性体现在这段代码中的两个方面:标准差(std):在代码中使用了 talib.STDDEV() 函数计算标准差,以衡量价格的波动性。标准差是一种统计指标,用于测量数据的离散程度或波动性。通过计算价格数据的标准差,代码可以获取最近一段时间内价格的波动性信息,这里使用了当前标准差的两倍作为波动上限和波动下限,即买入价加上两倍标准差和减去两倍标准差。通过实时计算标准差并基于标准差设置多头和空头相应的止盈和止损价格,代码可以根据实时价格的波动性进行交易决策。如果当前价格达到相应的止盈价格和止损价格时,就进行相应的交易操作。这样就能够根据实时波动情况进行风险控制和收益保护,提高交易的效果。

可以在回测结果中看到,止盈和止损之间的距离并不是固定不变的,会随着价格的波动程度,区间进行动态的调整,因此在一定程度上具有更强的适用性。这里的标准差周期(period)和标准差倍数(multiple)都是可以实时调整的,可以针对于不同种类的期货品种,例如波动性较大的化工类(纯碱,甲醇等),和波动性较小的农产品类(玉米和淀粉类)进行参数的调整。

以上我们讲了三种止盈止损的设计,大家也不一定要严格纠结于这些概念的区别,并且市面上还存在许多止盈止损策略,重要的是理解这些策略的思想,以及如何用代码实现这些策略。希望通过本课程的学习,大家可以了解止盈止损策略的基本原理,理清其思路,并根据自己的交易理念,构建更适合自己的止盈止损策略。希望本课程能为你提供有益的指导和启发,帮助你在交易中更加有效地运用止盈止损策略。

11.2 建立概率思维,提升交易格局

期货有很多种交易方法,无论是价值投资、技术分析、事件热点、套利对冲等,表面上看起来逻辑严谨,但实际上往往相互矛盾。价值投资的优势是可以根据价值给价格波动划分一个安全边际,技术分析的优势是三大假设使交易具有一定的科学性。

但它们都有一个共同的特点,那就是对未来的价格分析,只能做到大概预测,而不能精准预测。即使将基本面分析与技术分析相结合,也不能解决提高“精准”的问题,所以自始至终交易都是一个概率游戏。

11.2.1 交易来自生活

不仅如此,人这一生,小到过马路,交什么样的朋友;大到从事什么样的事业,跟什么人结婚等等,都是评估风险与回报的概率游戏。因为我们没有未卜先知的能力,每做一件事即使再有把握,风险都始终存在,无法做到百分之百是对的。

许多人在交易中犯错的重要原因就是缺少概率思维,在做交易时过于感性而非理性。感性其实就是我们的原始本能,在市场中,这些原始本能可以激发人的许多弱点,并且成倍放大。

11.2.2 概率思维

概率思维,是一个文绉绉的名字,说得通俗点就是——赌博思维。“赌博”可能是被人们误解最深的词汇之一了。如果你的策略是负期望,就是赌徒;如果你的策略是正期望,就是赌博。

如果把“赌博”中的贬义去掉,将之理解为承担一定风险而获得一定回报的活动,那么人生真的处处是赌博。上学选择哪个专业、买不买房、项目上不上马、打工还是创业等等。甚至把钱存到银行也是赌博,因为你不确定未来是否会通货膨胀,银行是否会破产(参考希腊债务危机)。总之从摇篮到坟墓,生命的每个过程都是在赌博。

11.2.3 久赌必赢

在研究久赌必赢策略之前,先来研究一下,那些久赌必赢策略的原理。除了印钞机,还有什么是能久赌必赢的呢?

赌场里面的:百家乐、轮盘赌、老虎机、21点等等,不管怎样变换形式,都隐藏着一个赌场从来不说的秘密:就是在1比1的赔率下,庄家的胜率总是大于50%;另外,还有一种赔率很高胜率很低的赌博产品。就这样,赌场老板利用「大数定律」,持续不断地参与这样的游戏,那么长期一定是盈利的。

举个例子,三个骰子,押大小,4-10是小,11-17是大,押对了就赢钱。而骰宝有一种围骰,就是三个骰子点数相同,赌场庄家通杀,围骰出现的概率是2.8%。那么出现大和出现小的概率就各是48.6%。赌场就是靠这2.8%的概率,如果每个赌客每局都押100元,玩100局赌场就会赢280元。

math
(0.486+0.028)*100*100-0.486*100*100=280

但是这个赌场策略是有漏洞的,万一一个大玩家心血来潮押个几百亿,恰好又赢了,赌场就一下子破产了。所以,赌场会设置一个下注上限,本轮超过这个上限就不能再下注了,并且一次一结账。这样就算赌客可能一时运气好赢钱了,长期下去,还是会输给概率,在无限多次的骰宝游戏中,赌客就会输掉2.8%的钱。

赌场老板的优势仅仅比赌客多2%,在单次赌博中,老板可能是亏损的,甚至也可能遇到连续亏损。但是赌场老板并不会被亏损吓坏,因为他知道,自己之所以能赚钱,正是「大数定律」在其中起作用,只要有人继续在赌,只需要2%的微弱优势,就能长期稳定盈利下去。

类似的久赌必赢的例子还有:各种彩票。彩票的奖池资金,自上市以来是越积越多,这些钱当然来自于广大彩民。你知道双色球中500万的几率是多少吗?答案是1770万分之一,这是一种高赔率低胜率的赌博。

11.2.4 概率的变化

假设有一个正反面一样重的硬币,抛出字(背面)和花(正面)的概率都是50%,而且每次抛硬币与前次结果无关。连续地抛10000次这个硬币,那么出现正面的概率约等于50%。

但是如果只抛10次,则出现正面的概率就变了,这个概率就不一定是50%了。所以赌场庄家必须保证触发这个正期望策略的次数足够多,这个正期望的策略才有效。这也是私募机构在开启量化交易策略时,除非特殊条件,不能停止策略的原因。

11.2.5 交易中的大数定律

理解了赌场必赚的原因后,就可以把这些经验学过来,在期货市场“经营”属于自己的“赌场”。

  1. 在技术面或基本面,只做有胜率优势的交易。
  2. 长期来看胜率很难超过50%,所以策略的赔率越大越好。
  3. 赌场每次投注都有上限,做交易也要设置止损点。
  4. 赌场每次投注都是独立的事件,在交易中也一样,当次交易应该于上次交易无关。
  5. 交易次数一定足够多,才能发挥大数定律的优势,能多品种多策略最好。

注意:赌场不会因为某次或多次亏损而修改它的规则,量化交易也是如此。

如果交易策略是已经验证过的正期望策略,那么单笔的盈亏没有意义,甚至连续几次的交易盈亏的意义也不大,只要交易的次数足够多,最后的结果一定是赚钱的。大数定律对具有概率思维的交易者非常有帮助,因为知道赢钱是必然的结果,就不会对中间的浮盈浮亏所困扰,从而提高的策略的执行率。

结语

随着最后一个章节的完成,我们已经共同完成了一段激动人心的旅程——从Python编程的基础知识到商品期货量化交易的深入理解。在《Python商品期货量化入门教程》的陪伴下,我们一同探索了量化交易的世界,学习了如何利用Python这一强大的工具来构建和完善我们的交易策略。
通过11个精心编排的章节,我们不仅深入了解了优宽量化平台的强大功能,还通过《Python量化交易实战教程》的宝贵经验,以及优宽量化API文档的最新信息,掌握了从策略构思到实盘交易的每一个关键步骤。我们的目标是为大家提供一个坚实的基础,让大家能够在量化交易的领域中自信地迈出第一步。
量化交易是一个不断进化的领域,它要求我们持续学习、适应并创新。我们希望本书能够激发大家对量化交易的热情,并成为大家量化之路上一个可靠的伙伴。记住,实践是检验真理的唯一标准,因此,请大家不吝于将大家在本书中学到的知识应用于实际操作中,通过不断的实践来提升大家的技巧和策略。
在大家的量化之旅中,可能会遇到挑战和困难,但请保持好奇心和坚持不懈的精神。每一次的回测、策略优化和实盘交易都是向前迈出的一步。随着时间的推移,大家将能够见证自己的成长和进步。
最后,我们衷心希望《Python商品期货量化入门教程》能够成为大家量化交易学习之路上的一个重要里程碑。愿大家在量化交易的道路上越走越远,不断探索、学习和成长。祝愿大家交易顺利,未来充满无限可能。
欢迎来到量化交易的世界!

相关推荐
评论
全部评论 (2)

    企业微信加不上啊啊啊啊啊啊

    2 年前

    您好,可以加企业微信:

    img

    2 年前
  • 1
iPhone 下载
社区
回测系统
© 2015 - ∞ YouQuant 豫ICP备19046564号