使用JavaScript入门商品期货量化交易

Author: 雨幕(youquant), Created: 2023-08-15 17:04:31, Updated: 2024-09-12 09:04:45

盘的暂停的时间要把握好,不然超过平台返回的k线数量,是无法进行k线数据拼接的,因为必然会有一段k线的空缺。

其实数据种类有很多,不仅仅是K线,比如以前我们讲过的交易记录,也可以进行实时的保存和读取,我们可以自行探索一下。

好了,这就是我们策略进度恢复的设计的一些范例,希望大家了解其中的思路。在自己的实盘运行中,永远有B计划拯救自己的量化交易系统。

30:多品种合约回调策略设计:区别于轮询架构的事件驱动架构

多品种(多个期货合约)量化策略是指同时在多个不同的期货合约上应用量化交易策略。这种策略基于对多个期货品种的市场行情数据进行分析和建模,用来制定交易信号和执行交易。

多品种量化策略的优点如下:

  • 分散风险:通过在多个期货品种上分散投资,降低了单一品种风险对整体投资组合的影响。当一个品种的价格波动较大或遭受损失时,其他品种可能表现更好,从而减少整体风险。

  • 增加机会:不同的期货合约在市场中具有不同的特点和波动性。通过同时研究并交易多个期货合约,可以捕捉到更多的交易机会,并且在市场变动时能够快速做出反应。

  • 提高收益稳定性:由于不同期货品种之间的相关性通常较低,因此,在一个品种表现不佳的时候,其他品种可能仍然能够带来正向收益,从而提高整体收益的稳定性。

  • 顺势交易:多品种策略可以根据不同期货合约的趋势和走势进行交易。当一个品种的趋势明显时,可以选择在该品种上建立仓位,从而跟随市场的走势而获取收益。

  • 套利机会:多品种量化策略可以通过不同期货合约之间的套利机会来获得利润。例如,通过同时买入一个合约并卖出另一个合约,从价差中获取利润。

需要注意的是,多品种(多个期货合约)量化策略也面临一些挑战,如数据处理和模型构建复杂性增加、风险控制的难度提高等。因此,在设计和实施多品种量化策略时,需要充分考虑到市场特点、投资者风险承受能力和相关技术工具的支持。

多品种策略设计的优点在于使用方便,一个策略程序控制交易多个品种,可以统一信息状态显示。交易多个品种相对分散了风险,增加了交易机会。缺点在于设计比较复杂,各个品种之间不能相互影响,对程序执行效率要求比较高。所以设计难度远大于设计一个单品种策略。优宽量化交易平台上提供了大量策略范例,给我们提供了丰富的参考代码,设计思路。

相对于以往使用轮询的多品种策略设计,策略的整体框架是基于不断循环合约列表,然后在检查到该品种最新的走势满足交易信号的时候,进行相应的交易操作,这样虽然易于设计,但是这并不是一个真正的多品种事件驱动策略。因为这是一个串联的模式,一次只能一个合约的信号判断和交易操作;如果在处理a合约的时候,b合约的信号现实触发的话,程序是无法顾及到的;这样对于趋势的策略确实影响不大,但是对于高频的策略,如果错过相应的信号触发,就不满足策略设计的初衷了。

因此本节课,我们从策略设计层面入手,剖析一个多品种合约回调策略设计,学习一些策略架构设计的经验。对于一个多品种合约的回调策略设计,我们首先需要了解行情的推送模式。在优宽量化平台,对于行情模式,可以使用mode参数进行切换:

  • exchange.IO("mode", 0) 立即返回模式,如果当前还没有接收到交易所最新的行情数据推送,就立即返回旧的行情数据,如果有新的数据就返回新的数据。

  • exchange.IO("mode", 1) 缓存模式(默认模式),如果当前还没有收到交易所最新的行情数据(同上一次接口获取的数据比较),就等待接收然后再返回,如果调用该函数之前收到了最新的行情数据,就立即返回最新的数据。

  • exchange.IO("mode", 2) 强制更新模式,进入等待一直到接收到交易所下一次的最新推送数据后返回。

在同时,可以使用wait参数,设置阻塞:

通过结合```exchange.IO("mode", 0)```函数使用,这样配合使用就可以使程序在有最新行情时进行响应,执行程序逻辑,这样的目的,是为了在程序中使用```exchange.GetTicker()```等函数调用时不阻塞)。

如果Timeout参数设置为-1,该函数设置成为了立即返回模式,在没有新事件的时候,返回空值,
如果Timeout参数设置为0,代表阻塞等待最新事件。

需要注意的是在使用```exchange.IO("wait")```时,必须至少已经订阅了一个当前处于交易状态的合约。还有这个函数,只支持商品期货实盘。

```JavaScript
EventTick:{Event:"tick", Index:交易所索引, Nano:事件纳秒级时间, Symbol:合约名称, Ticker:行情数据}。
OrderTick:{Event:"order", Index:交易所索引, Nano:事件纳秒级时间, Order:订单信息}。

我们举例一个多品种行情回调的例子示范下:

var rTblMA = {
  type: "table",
  title: "hc2309",
  cols: ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume","OpenInterest", 'Symbol'],
  rows: []
};

var rTblrb = {
  type: "table",
  title: "rb2309",
  cols: ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume","OpenInterest", 'Symbol'],
  rows: []
};


var rTbli = {
  type: "table",
  title: "i2309",
  cols: ["strTime", "Time", "High", "Low", "Sell", "Buy", "Last", "Volume","OpenInterest", 'Symbol'],
  rows: []
};


function on_tick(symbol, ticker) {
  
    switch (symbol) {
    case "hc2309":
        rTblMA.rows.push([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]);
        if (rTblMA.rows.length > 10) {
            rTblMA.rows.shift();
        }
        break;
    case "rb2309":
        rTblrb.rows.push([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]);
        if (rTblrb.rows.length > 10) {
            rTblrb.rows.shift();
        }
        break;
    case "i2309":
        rTbli.rows.push([_D(ticker.Time), ticker.Time, ticker.High, ticker.Low, ticker.Sell, ticker.Buy, ticker.Last, ticker.Volume, ticker.OpenInterest, symbol]);
        if (rTbli.rows.length > 10) {
            rTbli.rows.shift();
        }
        break;
    }
}


function main() {
    while(!exchange.IO("status")) {
        Sleep(1000);
    }

    _C(exchange.SetContractType, "hc2309");
    _C(exchange.SetContractType, "rb2309");
    _C(exchange.SetContractType, "i2309");

    while(true) {
        var e = exchange.IO("wait", -1)

        if(e) {
            if(e.Event == "tick") {
                on_tick(e.Symbol, e.Ticker);
            } 
        }
        
        LogStatus('`' + JavaScriptON.stringify([rTblMA, rTblrb, rTbli]) + '`')
    }
  
}

我们举例一个多品种行情回调的例子示范下:

这里我们设置的多品种合约是黑色系类,包括热卷,螺纹钢,和铁矿石,然后设置wait函数,参数填写为-1,代表立即返回,因此当检查到最新的tick信息的时候,就是e.Event == “tick”,该函数会执行on_tick函数,这里我们设置的on_tick函数是在我们初始设置的空图表中,不断的填充对应品种的最新10条的tick数据,然后使用LogStatus进行展示。

我们在实盘中运行下,可以看到由于我们设置了exchange.IO("wait", -1),是立即返回模式,如果有新的信息就立即更新,在没有新事件时返回空值。我们在实盘中可以看到,三个品种的信息不是伴随轮询,一条条逐渐更新的,而是实时更新的,实现了多品种行情的并联展示。这对于交易决策和监控多个品种的交易机会非常有益。

下面我们就来举一个实例示范下,多品种合约的回调策略设计。上面的例子我们只使用到了tick数据的事件驱动的更新,这可以作为交易信号的判断,而交易操作,还需要另外一种状态变量的辨别,就是持仓状态,实时的持仓状态的获取,我们可以通过OrderEvent数据结构。

/*backtest
start: 2022-07-27 09:00:00
end: 2022-07-27 09:05:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

function main() {
    var symbol = 'MA309';
    var rTbl = {
        type: "table",
        title: "数据",
        cols: ["strTime", "Time", "High", "Open", "Low", "Close", "Volume"],
        rows: []
    };

    var timezone = []; //实时k线时间戳
    var gtimezone = []; //保存k线时间戳

    //连接CTP
    while(!exchange.IO("status")) {
        LogStatus(_D(), "未连接CTP!");
        Sleep(1000);
    }
    
    exchange.SetContractType(symbol);
    var r = _C(exchange.GetRecords);

    //读取保存k线;
    var getRdata = _G('rTbl');

    //保存k线为空;
    if (getRdata == null) {
        Log('无存储数据');
        
        for (var i = 0; i < r.length; i++) {
            var bar = r[i];
            rTbl.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
            timezone.push(bar.Time);
        }

        Log('rTbl.rows储存长度', rTbl.rows.length);
        _G('rTbl', rTbl);
        Log('已储存新的r数据到rTbl中');
    }

    if (getRdata != null) {

        Log('已有存储数据');
        for (var j = 0; j < getRdata.rows.length; j++) {
            var gBartime = getRdata.rows[j][1];
            gtimezone.push(gBartime);
        }

        for (var i = 0; i < r.length; i++) {
            var bar = r[i];
            timezone.push(bar.Time);
        }

        var gLastTime = gtimezone[gtimezone.length - 1];
        var nLastTime = timezone[timezone.length - 1];

        Log('gtimezone时间', gLastTime);
        Log('timezone时间', nLastTime);

        if (gLastTime != nLastTime) {
            Log('k线有空缺,开始进行k线拼接');

            var overlapIndex = -1; //交叉索引flag;
            for (var k = timezone.length - 2; k >= 0; k--) {
                if (timezone[k] == gLastTime) {
                    overlapIndex = k;
                    Log('找到最后重叠时间节点:' + timezone[overlapIndex] + ',开始拼接');
                    break;
                }
            }

            Log('停止时间:', _D(timezone[overlapIndex]))
            Log('最新时间:', _D(timezone[timezone.length - 1]))
            Log('间隔时间:', (nLastTime - timezone[overlapIndex])/60000)

            if (overlapIndex === -1) {
                throw('实盘暂停时间过长,无法进行K线拼接');
            } else {
                for (var m = overlapIndex + 1; m < timezone.length; m++) { 
                    var bar = r[m];
                    getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
                }
                _G('rTbl', getRdata);
                Log('拼接长度:', getRdata.rows.length);
                Log('k线拼接已完成');
            }

        } else {
            Log('储存数据和k线数据已同步,不需要进行k线拼接');
        }
    }

    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已经连接到CTP!\n", '储存数据和k线数据同步工作已完成');
            var getRdata = _G('rTbl');

            var r = _C(exchange.GetRecords);
            var bar = r[r.length - 2]; //添加倒数第二根,以防最新的k线没有走完

            if (!timezone.includes(bar.Time)) {
                getRdata.rows.push([_D(bar.Time), bar.Time, bar.High, bar.Open, bar.Low, bar.Close, bar.Volume]);
                timezone.push(bar.Time);
                _G('rTbl', getRdata);
                Log('rTbl.rows更新长度', getRdata.rows.length);
            }

            //策略逻辑
            //...

        }else{
            LogStatus(_D(), "未连接CTP!");
        }
        Sleep(1000);
    }
}

在先前的课程中,我们在回测系统中,order结构返回的信息比较少,其实在实际的交易场景中,作为一个交易的完整操作,从交易信号辨别,决定下单,向交易所发送请求,到最后完成下单,orderevent会返回一系列的事件:

这就是一条完整的order返回数据,这里面有几个重要的属性可以帮助我们判断订单的方向和完成的阶段状态:

  1. 第一阶段,订单提交,”StatusMsg”返回报单已提交,这里的重要属性Status代表(订单状态),Type表示(订单买卖类型),Offset表示(期货开平仓方向),Type和Offset决定操作的方向,而返回Status判断交易是否完成,这个时候Status是0,表示未完成状态;
  2. 在报单提交和报单完成之间,其实还有一系列的状态,”StatusMsg”会返回”未成交”,这个时候Status仍旧是0,表示未完成;
  3. 最后交易完成,Status回返回1,表示订单完成状态;

这里的order事件返回也是事件驱动返回的,所以我们可以使用order事件进行持仓状态变量的实时判断;

我们实现了信号判断和持仓状态判断的事件驱动,对于交易的操作,我们也可以实现非堵塞的模式,还记得前面我们讲过的交易类库中的pushtask函数吗?这里我们可以使用上。每当信号触发,使用pushtask接受交易任务,放进任务队列;然后继续进行其他品种的信号判断和交易任务的执行。通过这样的方式,我们就可以实现一个多品种合约的事件驱动架构的设计。

我们来看下这个策略的具体架构:

首先我们来讲下这个策略的具体交易逻辑:作为一个事件驱动的策略,这个策略使用了ticker双均线作为基准。当持仓量为空,最新的ticker价格超过ticker慢线的时候,我们进行开多仓,跌破ticker慢线的时候,我们开空仓;当具有多仓的时候,最新的价格小于ticker快线,我们进行平多仓;具有空仓,价格大于ticker快线,我们平掉空仓。策略的交易确实很简单,下面我们来看怎样实现。

这里我们设置的多品种合约依旧是热卷,螺纹钢,和铁矿石,首先设置两个字典对象tickerList和SymbolPos,用来保存这三个品种的ticker数据和持仓数据。这里的持仓数据,每个品种有三个索引,在下面的讲解中我会为大家进行介绍。接下来我们也要使用交易类库中的多任务对象NewTaskQueue。

我们的交易操作是根据tick信息和order信息实时事件驱动的。这里的exchange.IO("wait", -1)设计表示是实时返回的机制,有信息立马进行返回。 接下来,我们就要设置on_tick和on_order函数了,当交易所返回tick或者order信息的时候,我们需要怎样的操作;

第一个on_tick函数,当不同品种tick数据更新的时候,用来搜集相应的ticker数据,并计算均线,决定交易信号的触发。这里我们使用对应的合约品种的键向tickerList中添加最新的ticker数据,我们设置的快线和慢线周期分别为50和20,然后等收集够足够的数量,我们计算相应的ticker均值。然后比较最新的价格ticker.Last和均值的突破作为交易的信号。在交易信号判断完成以后,我们接下来获取持仓状态的数据。这里我们获取了一个finmp和lock,分别代表交易完成时候的仓位状态和交易锁。finmp变量的设置我们放在on_order函数中进行讲解。这里我们首先讲下交易锁的功能。

当交易信号触发,在向交易所发送请求,等待order信息返回的中间,如果最新的ticker信息返回,这个时候由于交易信号的再次触发,交易函数会进行二次下单,这个时候我们可以设置一个类似锁的功能,在对应交易信息触发以后,pushtask进行下单,然后就设置该信号对应的操作已经是完成状态,不需要再进行重复的下单。这里的开多,开空,平多和平空,我们都使用这样的交易锁的设置。一个锁既然会锁上,必然也是需要打开的。下面在on_order函数中,我们进行持仓状态的判断和交易锁的解锁功能。

我们前面讲过,一个交易操作的完成,会返回不同类型的操作信息,最开始是”报单已提交”状态,如果这个时候,当利用Type,Status和Offset判断是开多的时候,我们就可以将持仓状态的第一个索引,代表“申请订单”的状态定义为1,但是这个时候,并不是实际完成了一手开多,需要等到Status变为 1,证明我们完成了该笔订单,这个时候可以将持仓状态的第二个索引定义为1,也就是实际的持仓状态为1,代表完成了多单;当交易完成,这个时候交易锁可以进行解锁了,也就是第三个索引从1变为0。

下面开空,平多和平空对应的持仓状态和交易锁状态的设置,整体的思路也是一致的。这样就可以实现了基于order事件驱动的持仓状态的改变。

当设计完成on_tick和on_order函数,我们就可以带入我们的主循环,当事件更新时,驱动不同的函数进行运行,完成相应的交易操作;不要忘了这里的q.poll()函数,执行任务队列。这样就是一个区别于轮询架构的事件驱动架构的多品种合约的设计。

为了展示不同的状态,我们这里设置了不同合约的ticker表和order事件表,可以伴随策略的更新,观察相应的状态变量的变化。

我们在实盘中看下,可以看到伴随不同的tick或者order事件,不同合约的事件信息状态栏不断进行更新,我们可以具体了解到基于时间的驱动架构是怎样完成的。

本策略为了教学讲解,因此个别地方设计的比较冗余,大家可以根据自己的想法对这个策略进行更好的优化。作为一个区别于轮询架构的策略,该策略的细节确实比较多,我们在应用于这类策略的时候,可以在实盘中多次模拟检验,搭建自己的事件驱动架构的多品种策略。

31:半自动化策略设计:商品期货计划委托工具

在做商品期货交量化易的时候,并非所有的都是全自动的交易策略,还有很多半自动的程序化交易工具,代替人工盯盘。半自动化,商品期货计划委托工具,作为一种用于执行商品期货交易策略的工具,它结合了人工决策,和自动化执行的特点。这个工具可以通过事先设置的条件,来自动触发交易委托,并且提供一定程度的自定义和灵活性,使我们能够根据自己的需求,进行调整。这类工具虽然算不上完整的策略,但是也是基于使用者的交易意图,有条理的进行交易,可以算的上是一种半自动化的交易工具。

使用半自动化商品期货计划委托工具的好处如下:

  1. 提高交易效率
  2. 减少情绪干扰
  3. 实现精确控制
  4. 提供数据分析

提高交易效率:半自动化工具允许交易者事先设置交易策略和条件,当市场满足这些条件时,工具会自动触发委托,减少了人工干预和执行的时间,提高了交易的效率。

减少情绪干扰:交易过程中情绪对决策的影响是普遍存在的,而半自动化工具能够帮助交易者避免情绪干扰,按照设定的规则执行交易,从而减少了冲动和情绪驱使的交易行为。

实现精确控制:半自动化工具可以根据设定的交易策略和条件执行委托,能够更加精确地控制入场点、止损点和止盈点等,避免过度依赖人工判断,提高了交易的准确性和一致性。

提供交易数据分析:半自动化工具通常会记录和保存交易数据,可以对历史交易进行分析和回顾,帮助交易者评估交易策略的有效性,并作出相应的优化和调整。

总之,半自动化商品期货计划委托工具具有执行效率高、减少情绪干扰、精确控制交易和提供数据分析等优点,能够帮助交易者更好地执行交易策略,并提高交易的效果和效率。

下面我们就一起在优宽量化平台来实现一个这样的策略设计。

对于半自动的交易工具可能会有很多需求,我们简单整理一些需求实现出来,对于更加高级、复杂的需求可以后续优化升级。

商品期货计划委托止盈止损工具(教学版)

  • 计划委托: 制定委托任务,由策略参数设置的价格线,下单手数,多空方向,触发方式,确定任务。

  • 止盈 计划委托订单成交以后,根据设置的止盈价格,创建计划止盈任务。

  • 止损 计划委托订单成交以后,根据设置的止损价格,创建计划止损任务。

  • 策略进度的保存和恢复

一个完整的策略在实盘中是可以循环使用的,在止盈止损任务触发结束本轮交易以后,需要清空任务进度,为准备下一次开仓做好条件;在同时,如果遇到突发状况,实盘停止,需要及时保存策略进度,在实盘再次开启以后,继续运行策略。

有了以上需求,我们就可以逐一把功能实现,首先分析一下,止盈、止损动作是建立在开始的计划委托订单成交,有持仓以后,再产生的动作,所以止盈、止损是基于,第一个计划委托订单成交以后再创建。止损反手同样也是基于止损完成以后再产生的动作。

所以这里遇到第一个问题,我们设计的时候,如何让一个任务完成以后,自动创建另一个后续任务呢? 这个问题解决很简单,YOUQUANT量化交易平台提供了强大的模板类库,用自带的商品期货交易类库就可以轻松解决。还记得我们前面讲过的多任务对象$.NewTaskQueue(),它可以创建交易队列,用来控制多个对象。

我们来复习下如何使用这个对象 q?在pushtask函数中,在确定好交易品种,方向和数量以后,这里的function就是解决我们问题的回调函数。它可以在当前交易任务完成后,触发执行这个回调函数,这样我们把后续任务的创建操作,就可以写在这个回调函数中。

q.pushTask(exchange, task.taskSymbol, task.taskDirection, task.taskAmount, function(tradeTask, ret) {
    Log(tradeTask.desc, ret, "XX委托完成")
    if (ret) {
        // 回调,创建后续任务
        // ...
        // ..
        // .
    }
})

本节课,我们就尝试将以前的移动止盈止损策略放进我们的计划委托工具中,通过这样的方式,我们可以使用人为判断作为入场点,然后使用移动止盈止损策略,在争取实现较高收益的同时,又避免因为情绪引起的扛单。下面我们讲解下半自动化的移动止盈止损策略的设计。

/*backtest
start: 2023-08-11 09:00:00
end: 2023-08-11 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["_EntrustSymbol","rb2310"],["_EntrustPrice",3600],["_EntrustAmount",1],["_IsRecovery",false]]
*/

// 全局变量

var q = $.NewTaskQueue();

var proTbl = {
  type: "table",
  title: "止盈止损状态变量更新",
  cols: ["strTime", "品种", "开仓价格", "开仓方向", "盈利等级", "盈利等级比率", "止盈止损比率", "止盈止损价格"],
  rows: []
};

// 任务处理对象
function TaskQueueProcess () {
    // 获取行情
    exchange.SetContractType(_EntrustSymbol)
    var ticker = _C(exchange.GetTicker)

    var task = _TaskQueue[0]

    if (task.taskFinished == false && ticker.Last == task.taskPrice) {
        q.pushTask(exchange, task.taskSymbol, task.taskDirection, task.taskAmount, function(tradeTask, ret) {
            Log(tradeTask.desc, ret, "开仓委托完成")
            task.taskFinished = true;

            if (ret) {
                task.taskBuyprice = ret.price;
                task.taskSign = task.taskDirection == "buy" ? 1 : -1;
                task.level = 0;
                task.takeProfitRatio = 0.01 * task.taskSign;
                task.stopLossRatio = 0.01 * task.taskSign;
                task.taskStopPrice = task.taskBuyprice * (1 + task.takeProfitRatio) * (1 - task.stopLossRatio);

                // 回调,创建后续任务
                var newTask = {
                    taskType: 'STOPLOSS',
                    taskSymbol: task.taskSymbol,
                    taskDirection: task.taskDirection == "buy" ? "closebuy" : "closesell",
                    taskAmount: task.taskAmount,
                    taskFinished: false
                }
                
                _TaskQueue.push(newTask)
                proTbl.rows.push([_D(), task.taskSymbol, task.taskBuyprice, task.taskDirection, task.level, task.takeProfitRatio, task.stopLossRatio, task.taskStopPrice]);

                _G("_TaskQueue", _TaskQueue)
                Log("创建止盈止损任务", newTask, "#FF0000")
            }
        });
    }


    if(_TaskQueue.length == 2){

        var newTask = _TaskQueue[1]
        
        if (newTask.taskFinished == false && (newTask.taskDirection == "closebuy" ? ticker.Last > task.taskStopPrice : ticker.Last < task.taskStopPrice)) {
            
            //止盈止损线更新

            task.level += 1;
            task.takeProfitRatio = _N(0.01 * task.taskSign + 0.001 * task.taskSign * task.level, 5);
            task.stopLossRatio = _N(0.01 * task.taskSign - 0.001 * task.taskSign * task.level, 5);
            task.taskStopPrice = task.taskBuyprice * (1 + task.takeProfitRatio) * (1 - task.stopLossRatio);

            Log("最新点位", ticker.Last)
            Log("止盈止损点更新", task, "#FF0000")
            proTbl.rows.push([_D(), task.taskSymbol, task.taskBuyprice, task.taskDirection, task.level, task.takeProfitRatio, task.stopLossRatio, task.taskStopPrice])
            _G("_TaskQueue", _TaskQueue)
 
        } else if (newTask.taskFinished == false && (newTask.taskDirection == "closebuy" ? ticker.Last < task.taskStopPrice : ticker.Last > task.taskStopPrice)) {
            Log("到达止盈止损点位") 
            
            q.pushTask(exchange, newTask.taskSymbol, newTask.taskDirection, newTask.taskAmount, function(tradeTask, ret) {
                newTask.taskFinished = true
                Log(tradeTask.desc, ret, "到达止盈止损点位")
                // 关闭止盈止损任务,清空状态变量
                if(ret){
                    _G(null)
                    throw('止盈止损任务已完成,请重新设置入场价格和方向#00FF00')
                }
            })
        } 
    }

    q.poll()
}


var _TaskQueue = [];

function main() {
    if (_IsRecovery) {
        recoveryData = _G("_TaskQueue")
        if (recoveryData) {
            Log("恢复数据")
            _TaskQueue = recoveryData
        } else {
            _TaskQueue = []
            Log("没有可用于恢复的数据")
        }
    }else{
        _TaskQueue = []
        Log("不使用状态变量恢复")
    }

    // 根据参数生成任务
    if(_TaskQueue = []){
        if (_EntrustSymbol == "null" || _EntrustPrice <= 0 || _EntrustAmount <= 0) {
            throw("没有设置委托合约或者委托价格无效或者委托数量无效")
        } else {
            var task = {
                taskType : 'ENTRUST',
                taskSymbol : _EntrustSymbol,
                taskPrice : _EntrustPrice, 
                taskAmount : _EntrustAmount,
                taskDirection : _EntrustDirection == 0 ? "buy" : "sell",                       
                taskFinished : false
            }
            Log("请注意,创建委托任务", task, "#FF0000")
            _TaskQueue.push(task)
            _G("_TaskQueue", _TaskQueue)
        }
    }

    while (true) {
        if (exchange.IO("status")) {
            TaskQueueProcess()
            // 状态栏显示
            LogStatus('`' + JavaScriptON.stringify(proTbl) + '`')
        } else {
            LogStatus(_D(), "未连接")
        }
        Sleep(1000*30)
    }
}

相对于以往使用均线突破作为入场点,在半自动策略中,我们使用人为设置的入场点位,这就需要我们进行参数的设置;

一个入场点的判断,需要品种,价格,交易方向(开多还是开空),和数量的要求。所以我们在参数界面设置_EntrustSymbol代表合约,_EntrustPrice代表价格,_EntrustDirection代表方向,_EntrustAmount代表数量,在同时,以防实盘停止,丢失我们的状态变量,我们可以设置是否需要恢复策略进度,设置_IsRecovery变量。

接着我们来设置我们的main函数,在开头,我们设置策略进度恢复内容,如果设置_IsRecovery为true,证明需要恢复策略进度,我们使用_G进行任务队列的读取,_TaskQueue包含了移动止盈止损策略一系列的状态变量和交易任务。如果读取的状态变量recoveryData不为空,那么_TaskQueue使用我们保存的状态变量;如果保存的状态变量为空,或者选择不使用策略进度恢复,那么将_TaskQueue设置为空的子集,重启策略的运行。

在读取到_TaskQueue为空,我们需要根据委托的任务进行入场开仓的操作。这个时候需要做一个判断,如果初始参数设置的有误,包括开仓的品种,方向和数量,就要抛出错误;如果填写的参数没有错误的话,创建task任务对象,里面包括任务的类型,开仓的品种,价格,方向,数量,这些都是委托任务的变量,最后一个taskFinished,判断任务是否完成,这一个变量的设置很重要,我们将在策略主体循环中进行讲解。接着,我们需要向任务队列_TaskQueue中push这个task对象,然后进行保存。这样就完成了我们的开仓委托任务。

当然如果_TaskQueue不为空,就可以跳过这一步,直接读取我们先前的任务进度,直接进入我们的止盈止损的策略主题循环部分。

在设置好开仓委托任务以后,接下来,我们进入我们的主循环,在判断连接交易所的状态下,运行我们的交易逻辑函数TaskQueueProcess()。

在TaskQueueProcess()函数中,按照固定程序,设置好合约,因为我们想交易设置的更加灵敏,所以这里获取的是ticker数据。然后进入我们开仓交易的操作,获取_TaskQueue第一个元素task。在判断该开仓动作没有完成的情况下,当最新的ticker数据等于我们设置的买入价的时候,我们进行开仓;使用pushtask函数,根据设置好的品种taskSymbol,方向taskDirection,和数量taskAmount进行开仓的动作。开仓动作完成以后,就要立即设置我们的止盈止损的操作,所以设置我们回调函数。

这里为避免重复的开仓,需要设置taskFinished为true,然后在获取到回调结果以后,就是if(ret)为真,设置我们的买入价,就是ret.price,开仓方向有两个,因此对应的盈利等级线和止盈止损线的设置也是不同的,在多仓条件下,盈利等级线大于止盈止损线;而在空仓环境下,盈利等级线小于止盈止损线。所以我们设置了一个变量,taskSign,用来处理多仓和空仓下止盈止损线的计算;当开仓方向为buy,设置为正1,开仓方向为sell,设置为负1;然后设置一个等级,level,表明移动止盈止损的等级;

对于多仓和空仓不同方向的盈利等级比率takeProfitRatio,初始等级为0.01* task.taskSign。

对于止盈止损比率stopLossRatio,同样的思路,初级等级为0.01* task.taskSign。

最后来计算我们的止盈止损线了,使用开仓价乘以盈利等级率(1 + task.takeProfitRatio),再乘以止盈止损率(1 - task.stopLossRatio),就可以获得我们的初始止盈止损线了。

状态变量设置完成以后,接着创建我们的止盈止损平仓任务newTask,设置type为stoploss,设置好合约,数量,方向(根据开仓方向进行判断,如果开仓为buy开多,设置为closebuy,否则为平空closesell),最重要的设置taskFinished为false。然后在_TaskQueue中push这个newTask。这样就完成了后续任务的创建。记得使用_G保存更新的TaskQueue。

接下来就要进入我们的止盈止损线的更新,和到达止盈止损线后的平仓工作了。在判断后续任务添加完成以后,_TaskQueue.length是2。使用索引_TaskQueue[1]获取后续任务对象newTask。在判断newTask没有完成的情况下,taskFinished为false,根据开仓价和最新价格的走势,判断进行更新或者平仓的工作。

下面我们来看下止盈止损线更新的逻辑:在多仓环境下,taskDirection == “closebuy”,如果最新的价格大于我们先前设置的止盈止损线,说明新的盈利等级已到达,我们需要更新止盈止损线;而对于空仓环境,如果最新的价格小于我们先前设置的止盈止损线,我们进行更新。

第一步更新等级变量level,每次递增1,然后随之更新盈利等级率,止盈止损率和止盈止损线。

对于多仓的盈利等级率,我们设置的taskSign都是正数,初始等级为0.01,然后随着最新价格的提升,加上0.001 * task.taskSign * task.level。

对于止盈止损比率,同样的思路,初级等级为0.01,然后随着盈利等级线的提升,这个时候需要减去0.001 * task.taskSign * task.level。

止盈止损线同样进行更新,按照开仓价乘以更新后的的盈利等级率,和止盈止损比率。

下面我们来看止盈止损线到达后的平仓操作:在多仓的环境下,如果最新的价格小于我们先前设置的止盈止损线,或者空仓环境,大于止盈止损线,我们就要进行平仓。使用pushtask,按照newtask对象中的种类,方向和数量进行平仓,然后设置newTask.taskFinished为真,关闭止盈止损任务,清空状态变量。因为该笔交易已经完成,这个时候我们可以选择使用throw停止实盘,防止继续计费。

因为我们使用的是pushtask交易函数,不要忘了设置q.poll()。

这里为了展示移动止盈止损不同等级和止盈止损线的更新,在每一步,TaskQueue更新的时候,包括后续任务创建和状态变量更新,我们记录下来,然后使用状态栏进行实时的动态展示。

我们回测测试下,首先根据自身对于行情的判断,人为设置我们理想的买入价格,开仓方向和数量,就可以等待到达点位后,进行止盈止损的操作。

在日志信息里可以看到,首先我们设置委托任务,所以当ticker价格达到这个点位,我们进行开仓,然后系统进行开仓和后续的止盈止损操作,伴随后续价格的走势,止盈止损点位不断更新。当到达止盈止损平仓条件以后,系统自动选择平仓,停止实盘。这样呢,就可以免去我们人工盯盘的烦恼,使用半自动化的方式进行我们的交易决策。这里我们设置的止盈止损线和买入价很接近,这对于谨慎型的交易者非常有利;如果我们是一个冒险型的交易者,对自己的点位判断比较自信,可以将止盈止损线的振幅更宽一点,在承受较大风险的同时,也可能获取更高的收益。

在先前的课程中,为了教学的方便,我们只使用了多仓的止盈止损的操作,在本节课程,我们讲解了完整的多仓和空仓的止盈止损操作。但是,策略仍然具有改进的地方,第一,初始的盈利等级比率和更新的盈利等级率的步长的设置,可以根据不同的品种和交易理解进行改进,第二,我们这里开仓的操作是使用参数完成的,其实我们可以使用交互控件,在不停止实盘的情况下,定义开仓的具体参数。这些问题都可以根据我们对于市场的理解,和前面所学过的知识,对这个系统进行更好的设置和完善。

本节课,我们介绍的是一个半自动化策略设计的框架,其实除去移动止盈止损策略以外,固定比例的止盈和止损操作通过回调函数也可以实现,具有交互功能的跨期套利也可以实现,在我们的文库中,都有现成的代码,大家可以尝试下。

32:关于商品期货套利的策略设计

商品期货的套利模型大家一定都有所耳闻,其实“套利”在现实生活中很常见。比如:便利店老板从批发市场以 0.5 元买入一瓶矿泉水,然后在店里以 1 元的价格出售,最后赚取 0.5 元的差价。这个过程其实就类似套利。商品期货的套利策略是指通过同时买入或卖出两个相关商品期货合约,来实现利差差价的交易策略。套利策略旨在利用不同市场之间,或者同一市场上不同合约之间的,价格差异,从中获取稳定的利润。

根据套利策略的具体方式和实施方式,商品期货的套利策略可以分为多种类型。以下是几种常见的商品期货套利策略:

  • 跨品种套利:基于不同但相关的商品期货合约之间的价格差异进行套利。例如,通过同时买入大豆期货合约和豆油期货合约,利用它们之间的相关性来获得利润。

  • 跨期套利:基于同一商品不同到期月份的,期货合约之间价格差异,进行套利。例如,买入近期到期的合约,同时卖出远期到期的合约,利用时间价值的变化来获取利润。

  • 跨市场套利:基于不同地理位置,或不同交易所的,同一商品期货合约之间的价格差异,进行套利。例如,通过在两个不同的市场上,同时进行买卖来获得利润。

  • 期现套利:基于同一商品现货市场与期货市场之间的价格差异,进行套利。例如,通过买入现货,并卖出期货合约,或买入期货合约,并同时卖出现货来获得利润。

套利策略通常是市场中的低风险交易,但是套利机会在市场中往往存在较短的时间,并且价格差异,通常会很快被市场参与者纠正。所以可以通过量化交易这种自动化和编程实施的方式,在减少人为错误和情绪影响的干扰下,捕捉到短暂的交易机会,从而提供稳定的回报。

在实施套利策略时,以下几点值得注意:

  • 选择适合自己的市场和品种:根据自身知识、经验以及对各个市场和品种的了解,选择适合自己的套利市场和品种。 控制风险:无论采用何种套利模型,都需要进行有效的风险管理。设置适当的止损点和盈利目标,合理分配资金,控制仓位,避免过度杠杆操作。
  • 深入研究和分析:了解商品市场的基本面和技术面,掌握相关指标和数据,进行详尽的研究和分析。同时,关注市场消息和事件,灵活调整套利策略。
  • 效率和执行力:在套利交易中,时间和速度至关重要。快速反应市场变化,确保交易的执行效率和准确性。
  • 持续学习和改进:套利策略是一个不断学习和改进的过程。及时总结经验教训,保持学习的状态,根据市场情况进行调整和优化策略。

在上面讲到的四种套利模型中,并不是所有的套利策略都适用于散户投资者,相对于机构投资者,散户在进行跨市场套利和期现套利方面面临一些限制和困难。

首先,散户可能面临资金规模的限制。跨市场套利和期现套利通常需要较大的投资额来达到可观的利润,而散户的资金规模通常较小,难以承担这样的交易规模。此外,散户还需要面对市场准入的限制。某些市场或交易所可能对散户的准入设置了门槛,例如最低资金要求、特定的投资经验或专业资格认证等。这也增加了散户参与跨市场套利和期现套利的难度。

本节课我们将介绍两种常见的策略设计:跨品种套利和跨期套利。

首先我们需要来理解下这两种套利策略的合理性。套利策略通常蕴含着均值回归的思想。均值回归是一种常见的市场现象,指的是价格或者其他指标在一段时间内呈现出波动和偏离均值的趋势,但最终会回归到其长期平均水平。

在套利策略中,跨品种套利和跨期套利都利用了价格差异,存在的时机进行交易。一定时间内,价差是相关的跨期和跨品种之间是稳定的。在当价格差异扩大超过它的正常范围的时候,套利者会采取相应的行动,预测价格将回归到平均水平,从而获取利润。

因此首先我们可以使用可视化展示来验证均值回归在不同品种、不同期限合约的普遍存在性。

对于跨品种套利来说螺纹钢和热卷是最好的两个合约品种了。由于rb(螺纹钢)和hc(热卷)每张合约都是代表10吨货物,并且生产成本、原料等因素导致这两个品种价格相关性是很强的。当价格差出现异常时,是可以进行对冲套利的。 我们来看螺纹钢和热卷的主力价格差值图像。差值主要分布在85到110之间,并且以95为水平均线进行上下波动。

image

关于跨期套利,我们选择的是螺纹钢的两个临近的主力合约rb2310和rb2401,可以看到这两张合约的差价图在-30和-10区间波动,平均价差为-20左右。

image

需要注意的是,跨期和跨品种的差值并不是持续固定的,可能由于不同的市场变化,差值均线会发生变动。因此,当使用差值均值作为策略参数的时候,需要按照固定的时间间隔进行调参。

套利策略的逻辑

下面,我们来看下套利策略具体的实现逻辑。

套利品种:symbolA , symbolB 
价差定义:Price(symbolA) - Price(symbolB)

开仓:
正套:价差大于阈值 A 时 , 做空symbolA,做多symbolB
反套:价差小于阈值 B 时 , 做多symbolA,做空symbolB

止盈:
正套;价差小于阈值 C 时,止盈。
反套;价差大于阈值 D 时,止盈。

止损:
正套;价差大于阈值 E时,止损。
反套;价差小于阈值 F时,止损。

这里我们挑选的套利品种为symbolA和symbolB。该套利原理是通过观察symbolA和symbolB的价格差异,进行正套或反套交易。

首先,定义价差为symbolA价格减去symbolB价格(Price - Price)。价差一般是分布在一个正常的稳定范围内的,所以可以设置参数:

A: 正套阈值
B: 反套阈值
C: 正套止盈阈值
D: 反套止盈阈值
E: 正套止损阈值
F: 反套止损阈值

根据这个价差与各个阈值参数的关系,可以采取以下操作:

正套交易:当价差大于阈值 A时,意味着symbolA价格相对于symbolB价格偏高,此时可以做空symbolA(卖出symbolA),同时做多symbolB(买入symbolB)。

反套交易:当价差小于阈值 B时,意味着symbolA价格相对于symbolB价格偏低,此时可以做多symbolA(买入symbolA),同时做空symbolB(卖出symbolB)。

止盈操作:对于正套交易,当价差小于阈值 C时,表示价差由较大回归到了正常水平,可以考虑止盈(平仓)。对于反套交易,当价差大于阈值 D时,表示价差由较小回归到了正常水平,可以考虑止盈(平仓)。

止损操作:对于正套交易,当价差大于阈值 E时,表示价差是在继续扩大的,可能市场发生了显著的变化,存在一定的风险,可以考虑止损(平仓)。对于反套交易,当价差小于阈值F时,表示价差一直在持续缩小,这也是风险的标志,可以考虑止损(平仓)。

这些操作和阈值的设定可以帮助我们在价差波动时识别套利机会,并决定何时进场、何时离场以获得利润或控制风险。请注意,具体的阈值参数需要根据实际市场情况和回测结果进行调整,以获得更好的交易效果。

了解完策略的交易思路以后,这里有一个关键问题,模型的参数应该怎样设置?在跨品种套利和跨期套利策略中,阈值参数的确定方法与一般套利策略有所不同。以下是一些常见的确定阈值参数的方法:

  • 相关性分析:对于跨品种套利策略,可以使用相关性分析来确定阈值参数。通过计算不同品种之间的价格相关性,可以找到相关性较高的品种组合。然后,根据历史数据分析,确定合适的价差或相关性阈值。

  • 协整检验:对于跨期套利策略,可以使用协整检验来确定阈值参数。协整关系指的是一组时间序列之间存在长期稳定的线性关系。通过对不同到期日的期货合约进行协整检验,可以确定合适的价差阈值。

  • 理论计算:对于跨期套利,可以使用利用国债利率可以帮助确定具体的价差阈值或套利条件。将套利交易的价差与国债利率之间的差异进行比较,当价差超过一定阈值或超过国债利率时,可以认为存在套利机会,请注意:这只限于正向市场,就是远期合约的价格高于近期合约。

  • 统计分析:对于跨品种套利和跨期套利策略,也可以使用统计分析方法来确定阈值参数。根据历史数据,计算价格差或价差比率的均值、标准差等统计指标,并根据统计学方法确定合理的阈值范围。

与一般套利策略类似,跨品种套利和跨期套利策略也需要进行实时监控和调整阈值参数。根据市场的变化和实际交易情况,可以对阈值参数进行动态调整,以适应不同的市场环境。

需要注意的是,阈值参数的确定过程可能需要经过多次尝试和优化。在实际应用中,建议使用回测和模拟交易等方法来评估不同阈值参数设置下的策略表现,并选择表现较好的参数组合。同时,还需要谨慎考虑交易成本、市场流动性和风险管理等因素,以确保策略的可行性和盈利能力。

跨品种套利模型

首先我们来看跨品种套利模型。这里我们挑选的套利品种为热卷和螺纹。价差的定义为热卷的价格减去螺纹钢的价格。

/*backtest
start: 2023-06-13 09:00:00
end: 2023-06-19 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

var p = $.NewPositionManager();
var symbolA = 'hc888';
var symbolB = 'rb888';

function onTick() {
    // 获取合约A行情数据
    exchange.SetContractType(symbolA);
    var tickA = exchange.GetTicker();
    
    // 获取合约B行情数据
    exchange.SetContractType(symbolB);
    var tickB = exchange.GetTicker();
    
    if (!tickA || !tickB) {
        return;
    }

    // 分析持仓
    var pos = exchange.GetPosition();

    if (!pos) {
        return;
    }

    var longPosOfSymbolA = p.GetPosition(symbolA, PD_LONG);
    var shortPosOfSymbolA = p.GetPosition(symbolA, PD_SHORT);
    var longPosOfSymbolB = p.GetPosition(symbolB, PD_LONG);
    var shortPosOfSymbolB = p.GetPosition(symbolB, PD_SHORT);

    // 计算价差
    var diff = tickA['Last'] - tickB['Last'];

    // 开仓
    if (!longPosOfSymbolA && !shortPosOfSymbolA && !longPosOfSymbolB && !shortPosOfSymbolB) {
        if (diff > maxDiff) {
            // 空A合约,多B合约
            Log("空A合约:", symbolA, ",多B合约:", symbolB, ", diff:", diff, ", maxDiff:", maxDiff, "#FF0000");
            p.OpenShort(symbolA, 1);
            p.OpenLong(symbolB, 1);
        } else if (diff < minDiff) {
            // 多A合约,空B合约
            Log("多A合约:", symbolA, ",空B合约:", symbolB, ", diff:", diff, ", minDiff:", minDiff, "#FF0000");
            p.OpenLong(symbolA, 1);
            p.OpenShort(symbolB, 1);
        }
    }

    // 平仓
    if (shortPosOfSymbolA && longPosOfSymbolB && !longPosOfSymbolA && !shortPosOfSymbolB) {
        // 持有A空头、B多头
        if (diff < shortPosOfSymbolA.Price - longPosOfSymbolB.Price - stopProfit) {
            // 止盈
            Log("持有A空头、B多头,止盈。", "diff:", diff, "持有差价:", shortPosOfSymbolA.Price - longPosOfSymbolB.Price);
            p.Cover(symbolA);
            p.Cover(symbolB);
        } else if (diff > shortPosOfSymbolA.Price - longPosOfSymbolB.Price + stopLoss) {
            // 止损
            Log("持有A空头、B多头,止损。", "diff:", diff, "持有差价:", shortPosOfSymbolA.Price - longPosOfSymbolB.Price);
            p.Cover(symbolA);
            p.Cover(symbolB);
        }
    }else if(longPosOfSymbolA && shortPosOfSymbolB && !shortPosOfSymbolA && !longPosOfSymbolB) {
        // 持有A多头、B空头
        if (diff > longPosOfSymbolA.Price - shortPosOfSymbolB.Price + stopProfit) {
            // 止盈
            Log("持有A多头、B空头,止盈。", "diff:", diff, "持有差价:", longPosOfSymbolA.Price - shortPosOfSymbolB.Price);
            p.Cover(symbolA);
            p.Cover(symbolB);
        } else if (diff < longPosOfSymbolA.Price - shortPosOfSymbolB.Price - stopLoss) {
            // 止损
            Log("持有A多头、B空头,止损。", "diff:", diff, "持有差价:", longPosOfSymbolA.Price - shortPosOfSymbolB.Price);
            p.Cover(symbolA);
            p.Cover(symbolB);
        }
    }

    // 画图
    $.PlotLine("差价", diff);
}

function main() {
    while (true) {
        if (exchange.IO("status")) {
            onTick();
            LogStatus(_D(), "已连接");
        } else {
            LogStatus(_D(), "未连接");
        }
        Sleep(500);
    }
}

第一步,我们需要设置一些外部的参数,第一个,价差波动的上限,当价差大于这个值的时候,我们进


更多内容