行正套的交易,根据上一节课图像的显示,我们定义为110;第二个,价差波动的下限,当价差小于这个值的时候,我们进行反套的交易,我们定义为85;第三个,止盈点数,对于正套和反套,当价差回归于正常水平的时候,可以进行止盈平仓;最后一个,止损的点数,当价差持续扩大,超过一点阈值的时候,我们可以进行止损,防止进一步的风险。止盈和止损的点数我们都定义为10。
回到代码,这个策略中,我们使用交易类库进行下单的操作,创建一个名为p的单品种控制对象。
定义symbolA和symbolB分别为热卷和螺纹钢的主力合约。
我们将策略的主交易逻辑写在onTick函数中。
在onTick函数中,首先获取合约A和合约B的最新行情数据。
然后分别获取合约A和合约B的多头仓位和空头仓位的持仓数量。这里获取仓位的方法是使用单品种控制对象p,当指定合约和仓位类型后,就可以获取我们想要的仓位信息。
接着利用ticker数据计算合约A和合约B的价差diff。
下面就进入我们的交易逻辑了。利用持仓信息,价差,和我们设定的参数开始交易的操作:
当我们持仓为空,就是合约A和合约B的多头和空头的持仓信息都不存在,根据价差和价差阈值我们进行开仓的动作:
当判断价差大于给定的最大价差上限maxDiff的时候,在一定程度上,说明合约A的定价过高,合约B的定价过低,进行空合约A,同时多合约B的操作。
另一种情况,当判断价差小于给定的最大价差下限minDiff的时候,在一定程度上,说明合约A的定价过低,合约B的定价过高,进行多合约A,同时空合约B的操作。
当我们开仓完成以后,就要进行我们的平仓逻辑了。
当处于正套的时候,就是拥有A空头、B多头,当我们的价差缩小到一定的范围,就是实际的价差小于我们开仓的价差减去止盈阈值的时候,证明两者的价差恢复到了正常的区间,我们需要进行止盈,平掉我们的仓位。
另一种情况,如果价差持续扩大,大于我们开仓的价差加上止损的点数,证明市场的趋势可能发生了显著的变化,我们进行及时的止损,平掉我们的仓位。
当处于反套的时候,拥有A多头,B空头,当较小的价差恢复到正常的范围,当前的价差大于我们开仓的价差加上止盈点数,我们就要进行止盈平仓;而如果价差持续缩小,当前的价差小于开仓的价差减去止损点数,也说明市场趋势可能发生了改变,我们需要及时平仓。
最后使用$.PlotLine
方法画出当前价差的曲线图。
我们使用回测系统测试下,可以看到,在一周的时间内,整体的收益是比较稳定的。当捕捉到两个品种价差超过正常的波动区间的时候,我们进行开仓,而恢复到正常区间,我们进行平仓;因此在一定程度上,减少了风险,实现了较为稳定的收益。
通过这个套利策略的设计和止盈止损规则,我们可以尝试利用其他的品种之间的价差变动来获取利润。需要注意的是,这里参数的设定应该基于详细的市场分析和风险控制考虑,并经过充分的回测和验证。同时,我们还需注意市场流动性和交易成本等因素对套利策略的影响。
接着我们来看跨期套利模型的设计。跨期套利是对于同一商品但不同交割月份之间正常价格差距出现异常变化时,可以进行对冲交易而获利的一种交易方式。所以这里的关键因素,是确定价差的波动区间。
传统持有成本模型中,我们可以通过无风险利率(取十年国债收益率)来计算合理的远期合约价值(不考虑交易成本),公式为:
Far = Near *(1 + Rf * month / 12)
其中,Far为远期合约合理价值,Near为近期合约最新价,Rf为无风险利率,month为远近期货合约之间的时间差,month / 12用来转化年化利率为持有期间利率。
如果同一个标的资产的不同交割月份的两个期货合约之间的价差,和我们计算的这个理论值偏离过大,我们可以认为出现了不合理价差,也同时出现了交易机会。
/*backtest
start: 2023-07-03 09:00:00
end: 2023-08-07 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
args: [["openDiff",20],["coverDiff",10]]
*/
// 全局变量
var symbolNear = 'rb2310';
var symbolFar = 'rb2401';
var interest_rate = 1 + 0.03 * 3 / 12 // 远近合约月份之间的利率系数理论值
var q = $.NewTaskQueue() // 创建商品期货交易类库模版类库中的交易对象
function main() {
while (true) {
if (exchange.IO("status")) {
LogStatus(_D(), "已连接");
// 获取近期行情数据
exchange.SetContractType(symbolNear)
var tickerNear = exchange.GetTicker()
// 获取远期行情数据
exchange.SetContractType(symbolFar)
var tickerFar = exchange.GetTicker()
if (!tickerNear || !tickerFar) {
return
}
// 更新持仓
var nearSymbolHold = 0
var farSymbolHold = 0
var pos = _C(exchange.GetPosition)
for (var i = 0 ; i < pos.length ; i++) {
if (pos[i].ContractType == symbolNear) {
nearSymbolHold += pos[i].Amount
} else if (pos[i].ContractType == symbolFar) {
farSymbolHold += pos[i].Amount
}
}
// theory_price 理论远期合约价格
var theory_price = tickerNear.Last * interest_rate
// theory 近期实际价格和远期理论价格的价差
var theory = tickerNear.Last - theory_price
// 近期合约和远期合约实际价差
var real = tickerNear.Last - tickerFar.Last
// 触发下限
var floor = theory - openDiff
// 触发上限
var cap = theory + openDiff
// 平仓止盈线
var closeprofit_low = theory - coverprofitDiff
var closeprofit_high = theory + coverprofitDiff
// 平仓止损线
var closeloss_low = theory - coverlossDiff
var closeloss_high = theory + coverlossDiff
// 判断触发条件
if (nearSymbolHold == 0 && farSymbolHold == 0 && real < floor) { // 买近卖远
Log("买近卖远,real:", real, "floor:", floor, "#FF0000")
q.pushTask(exchange, symbolNear, "buy", 1, function(task, ret) {
Log(task.desc, ret)
if (ret) {
q.pushTask(exchange, symbolFar, "sell", 1, function(task, ret) {
Log(task.desc, ret)
})
}
})
} else if (nearSymbolHold == 0 && farSymbolHold == 0 && real > cap) { // 卖近买远
Log("卖近买远,real:", real, "floor:", floor, "#CD32CD")
q.pushTask(exchange, symbolNear, "sell", 1, function(task, ret) {
Log(task.desc, ret)
if (ret) {
q.pushTask(exchange, symbolFar, "buy", 1, function(task, ret) {
Log(task.desc, ret)
})
}
})
} else if (nearSymbolHold != 0 && farSymbolHold != 0 && real > closeprofit_low && real < closeprofit_high) { // 当差价进入设置的非套利区间,平仓
// coverall
Log("平仓止盈,real:", real, "closeprofit_low:", closeprofit_low, "closeprofit_high:", closeprofit_high)
q.pushTask(exchange, symbolNear, "coverall", -1, function(task, ret) {
Log(task.desc, ret)
if (ret) {
q.pushTask(exchange, symbolFar, "coverall", -1, function(task, ret) {
Log(task.desc, ret)
})
}
})
} else if (nearSymbolHold != 0 && farSymbolHold != 0 && (real > closeloss_high || real < closeloss_low)) { // 当差价进入设置的止损区间,平仓
// coverall
Log("平仓止损,real:", real, "closeloss_high:", closeloss_high, "closeloss_low:", closeloss_low)
q.pushTask(exchange, symbolNear, "coverall", -1, function(task, ret) {
Log(task.desc, ret)
if (ret) {
q.pushTask(exchange, symbolFar, "coverall", -1, function(task, ret) {
Log(task.desc, ret)
})
}
})
}
q.poll()
$.PlotLine("floor", floor)
$.PlotLine("cap", cap)
$.PlotLine("closeprofit_low", closeprofit_low)
$.PlotLine("closeprofit_high", closeprofit_high)
$.PlotLine("closeloss_low", closeloss_low)
$.PlotLine("closeloss_high", closeloss_high)
$.PlotLine("real", real)
$.PlotLine("theory", theory)
} else {
LogStatus(_D(), "未连接");
}
Sleep(500);
}
}
了解完跨期套利里参数设置的理论背景后,我们来看具体的代码设计。和上面跨品种套利策略设计的思路基本是一致的,这里我们设置三个参数,开仓偏移用来确定开仓的阈值,使用理论的价差加减这个偏移,就是预测理论价差的波动范围,当真实的价差向上或者向下突破理论的波动范围,就认为是开仓的信号。
第二个参数是平仓止盈参数的设置,使用理论的价差加减平仓止盈偏移,可以认为是非正常的价差回归到了正常的价差,也就是非套利区间,当真实的价差回归到了这个非套利区间,我们这时候需要进行止盈平仓。
第三个参数是平仓止损参数的设置,一个完整的策略必然需要风险的控制,当价差出现显著的偏移,这个时候需要我们止损。这里的止损上限和下限,是理论的价差加减平仓止损偏移,当真实的价差高于或者低于这个平仓阈值的时候,我们需要止损平仓。
在代码中,我们首先设计全局变量,近期合约,rb2310,远期合约,rb2401,远近合约月份之间的利率系数理论值,使用十年国债收益率0.03,乘以两个合约的月份差3,再除以12,加上1,就相当于我们以无风险利率存了一笔三个月份的钱,到期后我们获得的收益率。
来到我们的主函数,首先获取近月和远月合约的ticker数据。
接下来获取实际仓位,这里使用GetPosition,使用轮询当检查到实际合约信息的时候,就更新近月和远月的持仓信息。
然后,来到我们的重点,计算各个关键的价格点位。
当价格突破这两个点位的时候,我们进行开仓。
closeprofit_low和closeprofit_high,是平仓止盈线,也是非套利区间,当开仓过后,实际价差回落到这个区间的时候,我们进行平仓。
closeloss_low和closeloss_high,是平仓止损线,可以认为是风险边界,当开仓过后,实际价差越过这个边界的时候,我们需要进行止损平仓。
当获取到了实时的行情信息,不同合约的持仓信息,和关键的点位信息,就可以进入我们交易逻辑的判断了。
当我们没有持仓,并且真实的价差小于理论的下限,证明真实价差过小,证明我们需要做多近月合约,做空远月合约。这里我们使用pushtask交易函数首先下近月合约的多单,在回调函数里,我们下远月合约的空单。
另一种相反的情况,同样没有持仓,真实的价差大于理论的上限,证明近月合约的价格过高,应该做空,远月合约的价值过低,应该做多。使用pushtask,首先下近月的空单,然后下远月合约的多单。
接下来到了平仓的止盈和止损环节,当检测到我们持有仓位,并且真实的价差落到非套利区间的时候,我们进行平仓,在pushtask中对两个合约进行coverall的操作。
同样当持有仓位,这时候判断当真实的价差高于或者低于平仓止损线,我们进行平仓。
pushtask的小尾巴,q.poll()
,大家不要忘了。
最后,我们对上面讲过的关键点位进行画图的展示。
我们点击开始回测,我们利用该策略,获得了一个正的收益。在图形中显示,真实的价差确实波动在理论价差的范围之内。
请注意,这里理论价差的计算,使用的仅仅是国债收益率一个影响因素,在实际的市场中,近期合约和远期合约影响的因素有很多,比如货币政策,宏观经济政策,市场宏观预期等等。在进行跨期套利时,需要综合考虑这些因素,以及市场上其他相关因素,并基于相应的模型和分析方法进行决策和风险控制。这样可以更准确地确定跨期套利的区间波动范围,并提高套利策略的效果。
本节课我们介绍的策略都很简单,重点是让大家理解这两种套利模型的实现方式和不同类型的策略设计,希望大家在了解每一种套利策略概念的基础上,结合自己的交易理念,搭建出适合自己的套利模型。其实影响套利合约价差的因素有很多,因此应根据市场的实际情况,并结合自己的交易理念和风险承受能力,对参数和模型进行定期调整和优化。
在前面我们讲过的策略,为了讲解的方便,使用的大多数都是单品种的策略,并且每次交易的手数都是一手。然而,对于大资金的量化需求,我们需要的是一个多品种的,可以伴随盈利逐步加仓,达到盈利点位或者止损点位进行减仓,并且可以控制风险的量化模型。那么有没有一种策略可以满足我们的要求呢?多品种海龟交易策略不要错过。
海龟交易系统是一种经典的趋势追踪交易策略,由美国期货交易员Richard Dennis和William Eckhardt在1983年推出。该系统基于价格的突破和趋势跟随原理,在市场中追踪并参与长期趋势,用来获取较大的收益。
首先,我们介绍下海龟交易系统的主要组成部分和特点:
多个市场:海龟交易系统可以应用于多种交易市场,包括商品期货、股票和外汇等。
入市规则:基于价格的突破是海龟交易系统的核心概念。通过观察市场价格是否突破一定周期内的高点或低点,决定是否入市建立头寸。
逐步建仓:海龟交易系统采用逐步建仓的方式。根据市场的表现和波动情况,逐步扩大头寸规模,但也有严格的风控规则限制仓位大小。
止损规则:海龟交易系统非常重视风险控制,当市场走势反向达到止损位时,及时平仓以限制损失。
退出规则:海龟交易系统有多种退出规则,包括根据价格突破逆向信号、固定的离市周期或固定的利润目标等。这些规则用于判断何时平仓并退出头寸。
波动性管理:海龟交易系统会根据市场的波动性进行头寸规模的调整,通常使用ATR(平均真实波幅)指标来计算波动性,并根据波动性来决定头寸的大小。
接下来我们来具体解释下海龟策略的交易逻辑。海龟交易系统是一个完整的交易系统,它有一个完整的交易系统应该有的所有成分,涵盖了期货交易中的每一个必要决策:
首先,我们来看什么时候进行买卖?
海龟用两个相关的系统选择品种,这两个系统都以唐奇安的通道突破系统为基础。
系统一:以20日突破为基础的偏短线系统 系统二:以55日突破为基础的较简单的长线系统
这两种不同却有关系的突破系统法则,可以称为系统一和系统二。我们可以根据自己的交易理念,自行决定将资金配置在何种系统上。有些交易员选用单一的系统交易所有的资金,或者分别用资金的50%选择系统一,50%选择系统二,当然还有其他的不同的组合选择。海龟策略利用两个突破系统的触发情况确定最高价和最低价,并根据最新价格,与最高价和最低价的关系,选择对应的做多突破,或者做空突破,进而进行相应的做多或者做空操作。
第二点,我们来看买卖多少?
买卖期货的数量,用下面的公式计算:
交易头寸=帐户金额\*(1-保证金比率)\*风险系数/N值/合约乘数
账户金额:指账户当前可用资金或账户净值。 保证金比率:指期货合约所要求的初始保证金占总价值的比例。它表示每手合约所需的初始保证金占头寸价值的比例。 风险系数:代表个体风险承受的程度,由交易员根据自身的风险偏好设定。 N值:是市场波动性的度量指标,例如平均真实波幅(ATR)。它用于衡量价格的波动情况。 合约乘数:是期货合约中每手合约所代表的标的资产数量。
这个公式可以帮助我们根据账户资金、风险承受能力和市场波动性来确定每个交易头寸的大小。
第三点,我们来看什么时候加仓?
在建立头寸后,如果最新的价格突破成功,就是价差大于加仓系数(一般是1/2)乘以ATR的间隔,可以选择增加头寸,增加头寸的数量也是使用公式计算出来的。但是通常情况下,对于加仓次数会设置一个最大限制,如果超过,就不再加仓。这样的限制可以控制风险,避免过度加仓。
第四点,什么时候进行止损?
在具有仓位的情况下,当判断最新一笔的盈亏大于设定的亏损限制(止损系数(一般是2)乘以N值的时候,表示触发止损,我们进行该品种的止损清仓操作。
第五点,什么时候进行平仓离场?
在持有仓位的期间,我们会时刻记录一段周期内的最高价和最低价,定义为上线和下线。对于多头持仓,当前价格小于离场周期内的最低价,进行多头平仓。 对于空头持仓,当前价格大于离场周期内的最高价,进行空头平仓。
以上呢,就是海龟策略的交易逻辑。当然,一个完整的量化交易系统,并不是只有交易策略的逻辑,我们还有下面的一系列需求:
除了上述工作之外呢,我们还需要根据策略的运行情况,及时的进行不同品种和不同参数的设置,帮助优化策略表现,提升收益水平。
怎么样,在了解完海龟策略的基本概念和需求以后,有没有想尝试用代码搭建完成模型呢?来,让我们从打地基开始,着手开始吧。首先,提示一下,本节课代码内容确实比较多,因此逐句进行讲解可能会耽误大家太多的时间,因此,海龟策略的讲解重点将放在策略整体的设计框架,但是我也会讲解到每个函数的功能以及具体的代码逻辑。其实大部分的知识都是我们所讲过的,大家在哪里感到陌生,可以暂停,翻看以前的视频。
策略的设计架构在量化交易系统中起着重要的作用。作为初学者,我们常常过于关注策略的盈利能力,而忽视了设计架构的重要性。一个良好的架构,在升级功能,调试测试,和扩展优化都是非常方便的,并且不容易出现潜藏BUG。在同时,一个好的设计架构可以让,策略交易逻辑和策略下单处理逻辑等其它与策略不相关的功能代码,进行很好的分离。这些代码耦合很低,所以非常容易修改,当然前提是要在通读过策略,完全理解策略架构之后。除去海龟策略,还有很多的多品种交易策略,比如均线策略,R-Breaker策略等等,其实我们完全可以把原版策略中,和交易策略相关的内容,分离出来删除掉,只留下一个多品种策略框架,就可以根据自己的需求进行其他策略的开发。
首先,我们来设置策略的参数,在了解完这些参数以后,我们可以对策略的整体脉络有一个程序化的认识。
变量 | 描述 | 类型 | 默认值 |
---|---|---|---|
Instruments | 合约列表 | 字符串(string) | MA888,pp888,v888,rb888,jm888 |
RiskRatio | % Risk Per N (0 - 100) | 数字型(number) | 1 |
ATRLength | ATR计算周期 | 数字型(number) | 20 |
EnterPeriodA | 系统一入市周期 | 数字型(number) | 20 |
LeavePeriodA | 系统一离市周期 | 数字型(number) | 10 |
EnterPeriodB | 系统二入市周期 | 数字型(number) | 55 |
LeavePeriodB | 系统二离市周期 | 数字型(number) | 20 |
UseEnterFilter | 使用入市过滤 | 布尔型(true/false) | true |
IncSpace | 加仓间隔(N的倍数) | 数字型(number) | 0.5 |
StopLossRatio | 止损系数(N的倍数) | 数字型(number) | 2 |
MaxLots | 单品种加仓次数 | 数字型(number) | 4 |
WXPush | 推送交易信息 | 布尔型(true/false) | true |
KeepRatio | 预留保证金比例 | 数字型(number) | 20 |
RMode | 进度恢复模式 | 下拉框(selected) | 自动 |
VMStatus@RMode==1 | 手动恢复字符串 | 字符串(string) | {} |
对于海龟交易策略的各个参数,我们是这样设置的:
Instruments(合约列表):指定要交易的合约列表,以逗号分隔不同合约的代码。
RiskRatio(% Risk Per N):表示每个交易单元风险的百分比。N代表ATR(平均真实波动幅度)的值。用于确定每个交易单元的头寸规模。
ATRLength(ATR计算周期):用于计算ATR指标的时间周期长度。ATR是一种衡量市场波动性的指标,用于确定止损和加仓的位置。
EnterPeriodA(系统一入市周期):在系统一中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。
LeavePeriodA(系统一离市周期):在系统一中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。
EnterPeriodB(系统二入市周期):在系统二中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。
LeavePeriodB(系统二离市周期):在系统二中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。
IncSpace(加仓间隔):加仓的价格间隔,以N倍的ATR为单位。在每次加仓时,头寸规模将乘以IncSpace。
StopLossRatio(止损系数):止损价格相对于入场价格的距离。以N倍的ATR为单位。当市场价格跌破止损价格时,触发止损。
MaxLots(单品种加仓次数):允许的最大加仓次数。超过该次数后将不再进行加仓。
WXPush(推送交易信息):确定是否推送交易信息到微信或其他渠道。
KeepRatio(预留保证金比例):在计算可用保证金时的预留比例。用于确保保证金余额不会过低,以应对不利行情。
Mode和VMStatus@RMode,是选择进度恢复模式,手动还是自动,如果是自动的话,可以使用_G进行读取;如果是手动,可以填写字符串,然后使用JavaScriptON.parse函数进行读取。
这些参数可以根据具体的交易策略和市场情况进行调整,以达到更好的交易结果。
本节课我们了解了海龟交易策略的思路以及具体的需求,和策略参数的设置,下节课我们将真正的进入代码编写部分,一会儿见。
我们来继续海龟策略的编写。
进入我们的代码,这段代码确实比较复杂,具体的变量和函数有很多,我们先来看一下策略的框架。整体来看,海龟策略的代码分为两个板块,第一个是交易逻辑对象TTManager。作为一个交易逻辑对象的构造函数,这个对象主要就是用来构造海龟交易逻辑对象的。整个的「海龟交易法则」用代码表达的部分都封装在这个部分。
第二个部分是main主函数。在主函数中,主要包括了while循环之前的程序初始化部分的设置工作,在这里会调用交易逻辑对象TTManager,使用轮询的方式,构造每个要交易的合约对应的海龟交易逻辑对象。下面是while循环,该循环为策略的主要循环,这一部分主要是遍历所有的海龟交易逻辑对象,调用每个海龟交易逻辑对象的处理函数进行相应的交易操作,最后一部分进行了策略运行时的界面显示的设计。可以看到,这里把海龟交易逻辑相关的操作都完全独立了出来,让整个策略层次比较分明。
首先我们来看第一部分的代码。相对于以往我们在主函数中编写我们的策略逻辑,这段代码实现了一个名为TTManager的对象,其中包含一个New函数作为构造函数。通过调用TTManager.New(),可以创建一个新的合约的交易对象。
这里我们稍微补充下,使用构造函数在策略设计中的好处:
封装性:构造函数将需要的参数和逻辑封装在一个函数内部,并返回一个新的对象。这样可以避免全局变量的污染和冲突,提高代码的可维护性和可读性。
参数灵活性:构造函数接受多个参数,包括needRestore(是否需要恢复进度)、symbol(合约符号)等等。这些参数使得构造函数能够根据不同的需求创建不同的对象实例,提供更大的灵活性和定制化能力。
复用性:构造函数内部定义了一个obj对象,它包含了一些属性和方法。这些属性和方法可以在对象创建之后被访问和使用,实现了代码的复用。
因此呢,使用构造函数增加了代码的可维护性和可扩展性。它使得对象的创建和初始化过程更加简洁和清晰,并为后续在主函数中对于不同品种的操作提供了基础。
下面呢,我们进入这段代码,来看具体模块的功能。首先来看一下输入的参数,这里除了上述在策略参数里定义的参数之外,还有几个参数需要我们看下。第一个needRestore,是否需要恢复,initBalance和keepBalance,初始的资金和预留的保证金,还有最后一个index,合约的索引。这几个参数我们将在主函数中进行定义。
第一部分我们订阅合约,检查合约信息。这里我们检查VolumeMultiple:代表合约乘数,MaxLimitOrderVolume:最大下单数,MinLimitOrderVolume:最小下单数,LongMarginRatio:做多保证金比率,ShortMarginRatio:做空保证金比率,这些参数我们将在下面计算交易手数中使用到。
第二部分,我们定义一下对象obj的一些属性信息,包括合约名称和代码,账户资金,风险系数,atr周期,系统a和b的入场和离场周期,加仓系数和止损系数,合约索引,最大加仓次数,最新的价格,合约的细节信息;下面是海龟策略的状态变量,这里面主要为了记录策略的运行进度。重要的变量包括合约代码,k线长度,持仓状态,移仓,开仓,平仓,止损平仓,和加仓的次数,最新的成交价格,持仓的均价和数量,浮动盈亏,N值,上线和下线,止损价格和离场价格,以及是否正在交易,还有一些变量用来记录错误信息,lastErr和lastErrTime等。
setLastError方法:用来记录错误信息发生的具体描述和时间。每当错误发生的时候,我们就可以展示在状态栏,方便我们及时的对策略进行修改。
reset方法:用于检查具体合约和恢复仓位的信息,包括marketPosition:加仓次数,openPrice:最后一次加仓价,N:N值,leavePeriod:离市周期,preBreakoutFailure:是否上次突破失败。它是根据marketPosition的属性是否为未定义决定恢复的,没有传入参数,就是不恢复,全部初始化。
Status方法:注意这和这里的小写status是不同的,它的功能是把obj的一些属性值赋值给obj.status同样意义的属性。这里面添加了一个holdProfit的属性,如果有持仓,通过最近成交价,持仓价格,持仓量和一手合约份数,计算持仓盈亏,并根据marketPosition(加仓次数)属性的正负去修正。
Poll方法:海龟策略的交易逻辑函数。是我们的重点,我们一会儿再介绍。
Chart变量:设置图表的配置变量。这里面我们设置了k线图,止损价格和离场价格的线图。相对于以往我们根据品种的固定数量,然后设置同样数量的图表对象。因为多品种合约列表的长度是不固定的,所以这里我们针对于每一个交易的品种,都设置了一个图表对象。
preBarTime变量:用来在画图中确定时间戳。
PlotRecords方法:具体的画图函数。这里面用到了我们以前的画图方法,根据时间戳的更新判断应该增加数据,还是更新数据。因为图表中数据的添加,是有固定索引顺序的,这里面的index是具体的合约的索引,我们将在主函数中进行定义。
最后的状态恢复模块:首先判断进度的恢复模式,是使用自动恢复还是手动恢复。然后通过判断进度信息(vm)是否为空来进行选择,是恢复进度还是创建新的对象。如果存在进度信息,则将之前保存的状态信息传递给对象的reset()方法进行恢复;否则,首先检查当前该品种是否具有仓位,needRestore的真假,这个参数是在主函数中获取的,如果检查到该合约有仓位,但是却没有在vm中找到该合约的信息,将调用reset()方法创建一个新的对象。
最后TTManager会返回该合约构造完成的对象obj。
var _q = $.NewTaskQueue();
var TTManager = {
New: function(needRestore, symbol, initBalance, keepBalance, riskRatio, atrLen, enterPeriodA, leavePeriodA, enterPeriodB, leavePeriodB,
multiplierN, multiplierS, maxLots, index) {
// subscribe
var symbolDetail = _C(exchange.SetContractType, symbol);
if (symbolDetail.VolumeMultiple == 0 || symbolDetail.MaxLimitOrderVolume == 0 || symbolDetail.MinLimitOrderVolume == 0
|| symbolDetail.LongMarginRatio == 0 || symbolDetail.ShortMarginRatio == 0) {
Log(symbolDetail);
throw "合约信息异常";
} else {
Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下单量", symbolDetail.MaxLimitOrderVolume, "保证金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
}
//定义obj对象属性信息
var obj = {
symbol: symbol,
tradeSymbol: symbolDetail.InstrumentID,
initBalance: initBalance,
keepBalance: keepBalance,
riskRatio: riskRatio,
atrLen: atrLen,
enterPeriodA: enterPeriodA,
leavePeriodA: leavePeriodA,
enterPeriodB: enterPeriodB,
leavePeriodB: leavePeriodB,
multiplierN: multiplierN,
multiplierS: multiplierS,
index: index,
};
obj.maxLots = maxLots;
obj.lastPrice = 0;
obj.symbolDetail = symbolDetail;
obj.status = {
symbol: symbol, // 合约代码
recordsLen: 0, // K线长度
vm: [], // 持仓状态 ,用来储存每个品种的 ,手动恢复字符串。
switchCount :0,
open: 0, // 开仓次数
cover: 0, // 平仓次数
st: 0, // 止损平仓次数
marketPosition: 0, // 加仓次数
lastPrice: 0, // 最近成交价格
holdPrice: 0, // 持仓均价
holdAmount: 0, // 持仓数量
holdProfit: 0, // 浮动持仓盈亏
N: 0, // N值 , 即ATR
upLine: 0, // 上线
downLine: 0, // 下线
stopPrice: '', // 止损价格
leavePrice: '', // 离场价格
isTrading: false, // 是否在交易时间
lastErr: "", // 上次错误
lastErrTime: "" // 上次错误时间信息
};
//记录错误信息
obj.setLastError = function(err) {
if (typeof(err) === 'undefined' || err === '') {
obj.status.lastErr = "";
obj.status.lastErrTime = "";
return;
}
var t = new Date();
obj.status.lastErr = err;
obj.status.lastErrTime = t.toLocaleString();
};
// 恢复仓位的信息
obj.reset = function(marketPosition, openPrice, N, leavePeriod, preBreakoutFailure) {
if (typeof(marketPosition) !== 'undefined') {
obj.marketPosition = marketPosition;
obj.openPrice = openPrice;
obj.preBreakoutFailure = preBreakoutFailure;
obj.N = N;
obj.leavePeriod = leavePeriod;
var pos = _q.GetPosition(exchange, obj.tradeSymbol, marketPosition > 0 ? PD_LONG : PD_SHORT);
if (pos) {
obj.holdPrice = pos.Price;
obj.holdAmount = pos.Amount;
Log(obj.symbol, "仓位", pos);
} else {
throw "恢复" + obj.symbol + "的持仓状态出错, 没有找到仓位信息";
}
Log("恢复", obj.symbol, "加仓次数", obj.marketPosition, "持仓均价:", obj.holdPrice, "持仓数量:", obj.holdAmount, "最后一次加仓价", obj.openPrice, "N值", obj.N, "离市周期:", leavePeriod, "上次突破:", obj.preBreakoutFailure ? "失败" : "成功");
obj.status.open = 1;
obj.status.vm = [obj.marketPosition, obj.openPrice, obj.N, obj.leavePeriod, obj.preBreakoutFailure];
} else {
obj.marketPosition = 0;
obj.holdPrice = 0;
obj.openPrice = 0;
obj.holdAmount = 0;
obj.holdProfit = 0;
obj.preBreakoutFailure = true; // test system A
obj.N = 0;
obj.leavePeriod = leavePeriodA;
}
obj.holdProfit = 0;
obj.lastErr = "";
obj.lastErrTime = "";
};
//属性值赋值
obj.Status = function() {
obj.status.N = obj.N;
obj.status.marketPosition = obj.marketPosition;
obj.status.holdPrice = obj.holdPrice;
obj.status.holdAmount = obj.holdAmount;
obj.status.lastPrice = obj.lastPrice;
if (obj.lastPrice > 0 && obj.holdAmount > 0 && obj.marketPosition !== 0) {
obj.status.holdProfit = _N((obj.lastPrice - obj.holdPrice) * obj.holdAmount * obj.symbolDetail.VolumeMultiple, 4) * (obj.marketPosition > 0 ? 1 : -1);
} else {
obj.status.holdProfit = 0;
}
obj.status.symbolDetail = obj.symbolDetail;
return obj.status;
};
// 交易逻辑
obj.Poll = function() {
// trade
obj.status.isTrading = $.IsTrading(obj.symbol);
if (!obj.status.isTrading) {
return;
}
// busy
if (_q.hasTask(obj.tradeSymbol)) {
return
}
// 推送消息
var suffix = WXPush ? '@' : '';
// switch symbol
var insDetail = exchange.SetContractType(obj.symbol);
if (!insDetail) {
return
}
var records = exchange.GetRecords();
obj.records = records;
if (!records) {
obj.setLastError("获取K线失败");
return;
}
// update tradeSymbol
var tradeSymbol = insDetail.InstrumentID;
if (tradeSymbol != obj.tradeSymbol) {
var oldSymbol = obj.tradeSymbol;
var pos = _q.GetPosition(exchange, oldSymbol);
if (pos && pos.Amount > 0) {
Log("开始移仓", oldSymbol, "->", tradeSymbol, "数量:", pos.Amount, "#ff0000");
obj.status.switchCount++;
_q.pushTask(exchange, oldSymbol, (pos.Type == PD_LONG ? "closebuy" : "closesell"), pos.Amount, function(task, ret) {
if (!ret) {
Log(oldSymbol, "移仓平仓失败 #ff0000");
return;
}
Log("移仓进度平仓成功, 开始开仓", oldSymbol, "->", tradeSymbol, "数量:", pos.Amount, "#0000ff");
obj.tradeSymbol = tradeSymbol;
obj.symbolDetail = insDetail;
_q.pushTask(exchange, tradeSymbol, (pos.Type == PD_LONG ? "buy" : "sell"), pos.Amount, function(task, ret) {
if (!ret) {
Log(tradeSymbol, "移仓开仓失败, 重置品种进度 #ff0000");
obj.marketPosition = 0;
return;
}
Log("移仓成功", oldSymbol, "->", tradeSymbol, "#0000ff");
});
});
return;
} else {
obj.tradeSymbol = tradeSymbol;
obj.symbolDetail = insDetail;
}
}
// 记录k线长度
obj.status.recordsLen = records.length;
if (records.length < obj.atrLen) {
obj.setLastError("K线长度小于 " + obj.atrLen);
return;
}
var opCode = 0; // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL
var lastPrice = records[records.length - 1].Close;
obj.lastPrice = lastPrice;
if (obj.marketPosition === 0) {
obj.status.stopPrice = '--';
obj.status.leavePrice = '--';
obj.status.upLine = 0;
obj.status.downLine = 0;
for (var i = 0; i < 2; i++) {
if (i == 0 && !obj.preBreakoutFailure) {
continue;
}
var enterPeriod = i == 0 ? obj.enterPeriodA : obj.enterPeriodB;
if (records.length < (enterPeriod + 1)) {
continue;
}
var highest = TA.Highest(records, enterPeriod, 'High');
var lowest = TA.Lowest(records, enterPeriod, 'Low');
obj.status.upLine = obj.status.upLine == 0 ? highest : Math.min(obj.status.upLine, highest);
obj.status.downLine = obj.status.downLine == 0 ? lowest : Math.max(obj.status.downLine, lowest);
if (lastPrice > highest) {
opCode = 1;
} else if (lastPrice < lowest) {
opCode = 2;
}
if (opCode != 0) {
obj.leavePeriod = (enterPeriod == obj.enterPeriodA) ? obj.leavePeriodA : obj.leavePeriodB;
break;
}
}
}
else {
var spread = obj.marketPosition > 0 ? (obj.openPrice - lastPrice) : (lastPrice - obj.openPrice);
obj.status.stopPrice = _N(obj.openPrice + (obj.N * StopLossRatio * (obj.marketPosition > 0 ? -1 : 1)));
// 止损平仓
if (spread > (obj.N * StopLossRatio)) {
opCode = 3;
obj.preBreakoutFailure = true;
Log(obj.symbolDetail.InstrumentName, "止损平仓", suffix);
obj.status.st++;
}
// 加仓
else if (-spread > (IncSpace * obj.N) && Math.abs(obj.marketPosition) < obj.maxLots) {
opCode = obj.marketPosition > 0 ? 1 : 2;
}
// 离场平仓
if (opCode == 0 && records.length > obj.leavePeriod) {
obj.status.leavePrice = obj.marketPosition > 0 ? TA.Lowest(records, obj.leavePeriod, 'Low') : TA.Highest(records, obj.leavePeriod, 'High');
if ((obj.marketPosition > 0 && lastPrice < obj.status.leavePrice) ||
(obj.marketPosition < 0 && lastPrice > obj.status.leavePrice)) {
obj.preBreakoutFailure = false;
Log(obj.symbolDetail.InstrumentName, "正常平仓", suffix);
opCode = 3;
obj.status.cover++;
}
}
}
if (opCode == 0) {
return;
}
if (opCode == 3) {
_q.pushTask(exchange, obj.tradeSymbol, "coverall", 0, function(task, ret) {
obj.reset();
_G(obj.symbol, null);
var account = _q.GetAccount(exchange);
var accountInfo = JavaScriptON.parse(exchange.GetRawJavaScriptON());
LogProfit(accountInfo.Balance, obj.tradeSymbol, "平仓后权益");
});
return;
}
// Open
if (Math.abs(obj.marketPosition) >= obj.maxLots) {
obj.setLastError("禁止开仓, 超过最大持仓 " + obj.maxLots);
return;
}
var atrs = TA.ATR(records, atrLen);
var N = _N(atrs[atrs.length - 1], 4);
var unit = parseInt((obj.initBalance-obj.keepBalance) * (obj.riskRatio / 100) / N / obj.symbolDetail.VolumeMultiple);
var account = _q.GetAccount(exchange);
var canOpen = parseInt((account.Balance-obj.keepBalance) / (opCode == 1 ? obj.symbolDetail.LongMarginRatio : obj.symbolDetail.ShortMarginRatio)
/ (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple);
unit = Math.min(unit, canOpen);
if (unit < obj.symbolDetail.MinLimitOrderVolume) {
obj.setLastError("可开 " + unit + " 手 无法开仓, " + (canOpen >= obj.symbolDetail.MinLimitOrderVolume ? "风控触发" : "资金限制") + "可用: " + account.Balance);
return;
}
_q.pushTask(exchange, obj.tradeSymbol, (opCode == 1 ? "buy" : "sell"), unit, function(task, ret) {
if (!ret) {
obj.setLastError("下单失败");
return;
}
Log(obj.symbolDetail.InstrumentName, obj.marketPosition == 0 ? "开仓" : "加仓", "离市周期", obj.leavePeriod, suffix);
Log('obj.marketPosition: ', obj.marketPosition);
Log(obj.marketPosition == 0 ? '开仓数量:' : '加仓数量:', unit);
obj.N = N;
obj.openPrice = ret.price;
obj.holdPrice = ret.position.Price;
obj.holdAmount = ret.position.Amount;
if (obj.marketPosition == 0) {
obj.status.open++;
}
obj.marketPosition += opCode == 1 ? 1 : -1;
obj.status.vm = [obj.marketPosition, obj.openPrice, N, obj.leavePeriod, obj.preBreakoutFailure];
_G(obj.symbol, obj.status.vm);
});
};
// 图表对象
obj.Chart = {
__isStock : true,
extension : {
layout : "single",
height : 300,
},
title : {"text": obj.symbol},
xAxis : {"type" : "datetime"},
series : [
{
"type" : "candlestick",
"name" : "k",
"id" : "k",
"data" : []
}, {
"type" : "line",
"name" : "stopPrice",
"data" : [],
}, {
"type" : "line",
"name" : "leavePrice",
"data" : []
},
]
}
// 画图
obj.preBarTime = 0
obj.PlotRecords = function(chart) {
var records = obj.records;
if (records == null) {
return;
}
for (var j = 0; j < records.length; j++) {
if (records[j].Time > obj.preBarTime) {
// 增加
chart.add(obj.index, [records[j].Time, records[j].Open, records[j].High, records[j].Low, records[j].Close])
chart.add(obj.index + 1, [records[j].Time, obj.status.stopPrice])
chart.add(obj.index + 2, [records[j].Time, obj.status.leavePrice])
obj.preBarTime = records[j].Time
}
else if (records[j].Time == obj.preBarTime) {
// 更新
chart.add(obj.index, [records[j].Time, records[j].Open, records[j].High, records[j].Low, records[j].Close], -1)
chart.add(obj.index + 1, [records[j].Time, obj.status.stopPrice], -1)
chart.add(obj.index + 2, [records[j].Time, obj.status.leavePrice], -1)
}
}
};
var vm = null;
if (RMode === 0) {
vm = _G(obj.symbol);
} else {
vm = JavaScriptON.parse(VMStatus)[obj.symbol];
}
if (vm) {
Log("准备恢复进度, 当前合约状态为", vm);
obj.reset(vm[0], vm[1], vm[2], vm[3], vm[4]);
} else {
if (needRestore) {
Log("没有找到" + obj.symbol + "的进度恢复信息");
}
obj.reset();
}
return obj;
}
};
回到我们的重点,Poll方法。在上节课我们了解完海龟策略的交易逻辑以后,相信这里面的交易代码逻辑不会让你太过于陌生。首先,我们判断当前交易系统的状态,IsTrading判断当前的品种是否在交易时间,hasTask函数用来判断交易系统是否正在繁忙,如果出现这两种情况,就进行返回。接下来设置是否推送消息,这里是根据WXPush参数的真和假来决定的。我们稍微补充一下,在优宽量化平台,如何进行消息的推送呢,其实很简单,在消息中,最后加上一个@符号就可以。具体推送的设置,在账号设置这里,可以选择推送APP和邮箱。所以这里定义了一个变量suffix,用来决定是否推送。
接下来我们获取合约的具体信息和k线的数据。
下面,来到我们的移仓换月功能的模块。其实并不复杂,首先获取最新的主力合约,和我们obj对象的tradeSymbol判断是否一致就可以决定是否需要移仓。如果两者不一致,证明新的主力合约已经更新,我们根据旧的合约的仓位的大小进行移仓。这里我们使用switchCount记录了移仓的次数;并且针对平仓失败和开仓失败都进行了相应的处理。
这里是用来k线的数量,如果不满足atr计算的周期数的话,会记录错误的信息;
在前期进行完