商品期货量化交易实践系列课程

Author: 雨幕(youquant), Created: 2023-09-12 09:54:22, Updated: 2023-12-18 16:43:42

       lossprice = t.Sell - priceTick * maxCoverDeviation
                trade("closebuy", profitprice, pos[0].Amount);
                isLock = true //加锁
            }

            if (pos[1] && !shortCoverOrder && isLock == false) {
                // 有空头持仓
                profitprice = t.Buy - priceTick * profit
                lossprice = t.Buy + priceTick * maxCoverDeviation
                trade("closesell", profitprice, pos[1].Amount);
                isLock = true //加锁
            }

            // 重新平仓条件
            if (pos[0] && longCoverOrder && t.Sell < lossprice) {
                Log('多头止损');
                cancelOrders(symbol);
                p.Cover(symbol);
            }

            if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
                Log('空头止损');
                cancelOrders(symbol);
                p.Cover(symbol);
            }
        }

        $.PlotRecords(r, "K线数据")
        $.PlotLine("多头挂单", orderlong, r[r.length - 1].Time)
        $.PlotLine("空头挂单", ordershort, r[r.length - 1].Time)
        $.PlotLine("挂单盈利线", profitprice, r[r.length - 1].Time)
        $.PlotLine("挂单止损线", lossprice, r[r.length - 1].Time)

        tblStatus.rows = [];
        tblStatus.rows.push([ContractType, curprice, orderlong, ordershort, profitprice, lossprice, holdType, holdPrice, holdAmount, holdProfit, fail_count, success_count]);
        lastStatus = '`' + JSON.stringify([tblStatus]) + '`';

        LogStatus(lastStatus);

    } else {
        LogStatus("未连接", _D());
    }
    Sleep(interval);
}

}


视频参考链接:

[《设计伪高频策略以及使用实盘级别回测来研究伪高频策略》](https://www.youquant.com/digest-topic/9268)
[《伪高频策略实现(PLUS)》](https://www.youquant.com/strategy/387602)

# 4.伪高频策略初探(二)

本节课我们继续针对于个体交易者伪高频策略的研究,首先我们先讲解在高频策略实操过程中,对于遇到的问题进行解决。

上节课,有些同学利用我们的代码进行尝试的时候,会出现“ERR_INVALID_POSITION”的错误,我们可以试着来解决一下,可以看到这个错误,只会在进行止盈平仓的时候出现,所以我们在止盈挂单的时候,打印当前的两个条件,exchange.GetPosition()当前的仓位和挂单。我们再次运行一下,

可以看到第一次挂单止盈的时候,仓位是存在的,而当错误出现的时候,这里会显示持有的仓位是0。

既然能进入这个条件,证明在if条件判断当时的仓位是存在的,但是执行到函数里,完成了交易的操作,重新设置平仓会产生错误。这个问题怎样解决呢,在止盈挂单期间和止盈完成期间,我们不希望进行额外的止盈挂单操作。其实我们可以一个类似锁的设置,在止盈挂单后将锁锁上,止盈平仓以后设置将锁打开,这样就可以避免再次进行止盈挂单。

```javascript
/*backtest
start: 2023-08-02 09:00:00
end: 2023-08-02 15:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
*/

//判断是否具有挂单
function getOrderBySymbol(symbol, orders) {
    var ret = [];
    _.each(orders, function(order) {
        if (order.ContractType == symbol) {
            ret.push(order);
        }
    });
    return ret;
}

//获取挂单类型
function hasOrder(orders, type, offset) {
    var ret = false;
    _.each(orders, function(order) {
        if (order.Offset == offset && order.Type == type) {
            ret = order;
        }
    });
    return ret;
}

//交易的函数
function trade(distance, price, amount) {
    var tradeFunc = null;
    if (distance == "buy") {
        tradeFunc = exchange.Buy;
    } else if (distance == "sell") {
        tradeFunc = exchange.Sell;
    } else if (distance == "closebuy" || distance == "closebuy_today") {
        tradeFunc = exchange.Sell;
    } else if (distance == "closesell" || distance == "closesell_today") {
        tradeFunc = exchange.Buy;
    }
    exchange.SetDirection(distance);
    return tradeFunc(price, amount);
}


//删除挂单
function cancelOrders(symbol, offset) {
    var orders = null;
    while (1) {
        orders = _C(exchange.GetOrders);
        if (orders.length == 0) {
            break;
        }
        for (var i = 0; i < orders.length; i++) {
            if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
                exchange.CancelOrder(orders[i].Id, orders[i]);
                Sleep(interval);
            }
        }
        Sleep(interval);
    }
    return orders;
}

var p = $.NewPositionManager(); //交易类库函数
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格

var tblStatus = {
    type: "table",
    title: "策略运行状态信息",
    cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格",  "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数"],
    rows: []
};

//主函数
function main() {
    var isLock = false //止盈挂单锁
    while (true) {
        if (exchange.IO("status")) {
            exchange.SetContractType(symbol);
            var t = exchange.GetTicker(); 
            var r = exchange.GetRecords();
            var positions = _C(exchange.GetPosition);
            var pos = [p.GetPosition(symbol, PD_LONG, positions), p.GetPosition(symbol, PD_SHORT, positions)];
            var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders));

            if (orders.length == 0 && (!pos[0] && !pos[1])) {
                isLock = false //解锁
                trade("buy", t.Buy - priceTick * deviation, 1);
                trade("sell", t.Sell + priceTick * deviation, 1);
            }else if (pos[0] || pos[1]) {
                if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
                    cancelOrders(symbol, ORDER_OFFSET_OPEN);
                    Log('删除挂单:', pos[0] ? '空单' : '多单');
                }
                var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
                var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);
                if (pos[0] && !longCoverOrder && isLock == false) { 
                    profitprice = t.Sell + priceTick * profit
                    lossprice = t.Sell - priceTick * maxCoverDeviation
                    trade("closebuy", profitprice, pos[0].Amount);
                    Log("止盈单挂单结束", "closebuy", exchange.GetPosition())
                    isLock = true //加锁
                }
                if (pos[1] && !shortCoverOrder && isLock == false) { 
                    profitprice = t.Buy - priceTick * profit
                    lossprice = t.Buy + priceTick * maxCoverDeviation
                    trade("closesell", profitprice, pos[1].Amount);
                    Log("止盈单挂单结束", "closesell", exchange.GetPosition())
                    isLock = true //加锁
                }
                if (pos[0] && longCoverOrder && t.Sell < lossprice) {
                    Log('多头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }
                if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
                    Log('空头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }
            }
        } else {
            LogStatus("未连接", _D());
        }
        Sleep(interval);
    }
        
}

所以在主函数中,设置islock,初始值为false,在进行止盈挂单的时候,这里增加一个条件islock为false,进行止盈挂单,然后设置islock为true进行加锁。什么时候解锁呢,在该笔交易完成以后,就是完成平仓,进行islock的解锁。这点确实让人比较困惑,但是在程序化交易中,我们需要考虑到这些问题的存在,然后试着去解决它,这样我们才能真正的获得成长。

盘口偏移策略

下面我们继续来高频策略的优化的尝试。上节课,我们了解了根据盘口价格做市maker策略。整体来看,这个策略确实比较粗糙,根据盘口的价格进行上下等间距的挂单,然后依据条件进行止盈和止损的操作。止盈的利润点数是固定的,而当止损的点数大于止盈的时候,亏损就产生了。因此,即使等概率成功和失败的次数,加上手续费的消耗,这个策略是亏损的。因此,我们策略优化的重点在哪里呢?我们要做的是,移动这个盘口的天平,朝着盈利的方向偏斜一点点,在大样本高频交易的基础上,努力实现一个正的期望(正收益)。

本节课我们就基于上节课讲述的策略框架,提出一些优化应用的实际例子,当然也是属于抛砖引玉,更多的是为大家提供优化的思路。让我们现在开始。

今天我们提出的第一个优化思路是利用盘口深度数据,进行开仓挂单价格的偏移。基于盘口深度数据进行挂单偏移,就是意味着使用当前市场价格为基准,通过观察挂单价格和数量的变化,来尝试判断市场走势,在开仓入市的时候调整挂单价格和数量,用来增加成交的概率,并获取更多的利润。

具体而言,可以对市场上的最优买卖挂单进行监控,买单和卖单会包含不同的数量,如果哪一方的数量较多,证明市场可能会朝着这个方向进行倾斜,因此我们对应这个方向的挂单价格可以设置的离盘口价格更近一点,而与此对应的,另一个方向的挂单设置的离盘口价格远一点,这样呢,可以增加有利方向的成交概率和获利的机会,从而在大样本下努力的提高正收益的期望。

image

我们来使用代码演示下,基本的框架是不需要改变的。在策略编写页面,我们可以选择复制策略,这样就可以复制一份原有的策略,包括具体的参数设置和策略的源码。

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

function cancelOrders(symbol, offset) {
    var orders = null;
    while (1) {
        orders = _C(exchange.GetOrders);
        if (orders.length == 0) {
            break;
        }
        for (var i = 0; i < orders.length; i++) {
            if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
                exchange.CancelOrder(orders[i].Id, orders[i]);
                Sleep(interval);
            }
        }
        Sleep(interval);
    }
    return orders;
}

function trade(distance, price, amount) {
    var tradeFunc = null;
    if (distance == "buy") {
        tradeFunc = exchange.Buy;
    } else if (distance == "sell") {
        tradeFunc = exchange.Sell;
    } else if (distance == "closebuy" || distance == "closebuy_today") {
        tradeFunc = exchange.Sell;
    } else if (distance == "closesell" || distance == "closesell_today") {
        tradeFunc = exchange.Buy;
    }
    exchange.SetDirection(distance);
    return tradeFunc(price, amount);
}

function getOrderBySymbol(symbol, orders) {
    var ret = [];
    _.each(orders, function(order) {
        if (order.ContractType == symbol) {
            ret.push(order);
        }
    });
    return ret;
}

function hasOrder(orders, type, offset) {
    var ret = false;
    _.each(orders, function(order) {
        if (order.Offset == offset && order.Type == type) {
            ret = order;
        }
    });
    return ret;
}

var p = $.NewPositionManager(); //交易类库函数

var success_count = 0; //成功次数统计
var fail_count = 0; //止损次数统计

var orderlong = null;  //多头挂单价格
var ordershort = null; //空头挂单价格
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格

var tblStatus = {
    type: "table",
    title: "策略运行状态信息",
    cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格",  "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数"],
    rows: []
};
 
function main() {
    var initAccount = _C(exchange.GetAccount);
    var preprofit = 0 //先前权益
    var curprofit = 0 //当前权益
    var holdPrice = null //持仓价格
    var holdType = null //持仓类型
    var holdAmount = null //持仓数量
    var holdProfit = null //持仓盈亏
    var isLock = false //止盈挂单锁

    while (true) {
        if (exchange.IO("status")) {
            
            exchange.SetContractType(symbol);
            var t = exchange.GetTicker(); 
            var r = exchange.GetRecords();
            var ContractType = symbol
            var curprice = r[r.length-1].Close

            var positions = _C(exchange.GetPosition);
            var pos = [p.GetPosition(symbol, PD_LONG, positions), p.GetPosition(symbol, PD_SHORT, positions)];
            var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders));

            // 获取深度数据
            var dep = exchange.GetDepth()

            // 计算买方数量和卖方数量
            if (dep.Asks.length >= 5) {
                var AskAmount = dep.Asks.reduce(function(sum, ask) {
                    return sum + ask.Amount;
                }, 0);

                var BidAmount = dep.Bids.reduce(function(sum, bid) {
                    return sum + bid.Amount;
                }, 0);
            }

            if (orders.length == 0 && (!pos[0] && !pos[1])) {
                profitprice = null
                lossprice = null
                isLock = false //解锁

                holdPrice = ''
                holdType = ''
                holdAmount = ''
                holdProfit = ''

                preprofit = curprofit
                curprofit = exchange.GetAccount().Balance - initAccount.Balance
                LogProfit(curprofit, "权益", '&');

                if(preprofit < curprofit){
                    success_count += 1
                    $.PlotFlag(r[r.length-2].Time, '止盈离场', '止盈离场')
                }

                if(preprofit > curprofit){
                    fail_count += 1
                    $.PlotFlag(r[r.length-2].Time, '止损离场', '止损离场')
                }

                // 按照买方/卖方数量确定盘口偏移
                var longDev = Math.round(priceTick * deviation * AskAmount / BidAmount)
                var shortDev = Math.round(priceTick * deviation * BidAmount / AskAmount) 

                orderlong = t.Buy - longDev
                ordershort = t.Sell + shortDev

                Log('买方偏移:', longDev)
                Log('卖方偏移:', shortDev)

                // 当前没有挂单,没有持仓,基于盘口挂单
                trade("buy", orderlong, 1);
                trade("sell", ordershort, 1);
                
            } else if (pos[0] || pos[1]) {
                // 只要有持仓
                orderlong = null
                ordershort = null

                var cur_pos = pos[0] ? pos[0] : pos[1]
                holdPrice = cur_pos.Price 
                holdType = pos[0] ? '多头方向' : '空头方向'
                holdAmount = cur_pos.Amount
                holdProfit = cur_pos.Profit

                if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
                    $.PlotFlag(r[r.length-2].Time, pos[0] ? '多单进场' : '空单进场', pos[0] ? '多单进场' : '空单进场')
                    cancelOrders(symbol, ORDER_OFFSET_OPEN);
                    Log('删除挂单:', pos[0] ? '空单' : '多单');
                }

                var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
                var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);

                if (pos[0] && !longCoverOrder && isLock == false) {
                    profitprice = t.Sell + priceTick * profit
                    lossprice = t.Sell - priceTick * maxCoverDeviation
                    trade("closebuy", profitprice, pos[0].Amount);
                    isLock = true //加锁
                }

                if (pos[1] && !shortCoverOrder && isLock == false) {
                    // 有空头持仓
                    profitprice = t.Buy - priceTick * profit
                    lossprice = t.Buy + priceTick * maxCoverDeviation
                    trade("closesell", profitprice, pos[1].Amount);
                    isLock = true //加锁
                }

                // 重新平仓条件
                if (pos[0] && longCoverOrder && t.Sell < lossprice) {
                    Log('多头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }

                if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
                    Log('空头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }
            }

            $.PlotRecords(r, "K线数据")
            $.PlotLine("多头挂单", orderlong, r[r.length - 1].Time)
            $.PlotLine("空头挂单", ordershort, r[r.length - 1].Time)
            $.PlotLine("挂单盈利线", profitprice, r[r.length - 1].Time)
            $.PlotLine("挂单止损线", lossprice, r[r.length - 1].Time)

            tblStatus.rows = [];
            tblStatus.rows.push([ContractType, curprice, orderlong, ordershort, profitprice, lossprice, holdType, holdPrice, holdAmount, holdProfit, fail_count, success_count]);
            lastStatus = '`' + JSON.stringify([tblStatus]) + '`';

            LogStatus(lastStatus);

        } else {
            LogStatus("未连接", _D());
        }
        Sleep(interval);
    }
}

策略的整体交易逻辑是不变的,这里我们需要的仅仅是利用盘口的深度数据,进行盘口价格的偏移设置。首先,我们挑选合适的品种,上交所的品种有五档的深度数据,可以帮助我们更好的了解市场的趋势。所以在策略参数里,我们设置的合约是螺纹钢2310合约。

接着在代码中,我们首先使用GetDepth获取深度数据。在确保获取的深度数据是五档的时候,分别计算卖单数量AskAmount,和买单数量BidAmount。接下来,我们就要设置价格偏移了,

因为我们希望哪一方的数量越多,该方向的偏移就越小,所以定义longDev买单的偏移为,Math.round(priceTick * deviation * AskAmount / BidAmount),使用卖方的深度数量除以买方的深度数量乘以设置的偏移区间;对应的卖单偏移是BidAmount / AskAmount。

接下来,就是用计算出来的偏移值代替原始的偏移值,并打印出来。这样基本的设置就完成了,我们运行一下看看回测结果。

设定日期为8月1号上午9点到下午三点,日志信息里显示首先计算挂单的偏移,然后进行挂单,根据回测的结果,这一段时间,止盈了15次,止损了5次。但是因为止损的损失比止盈多,所以加上手续费,最后的收益为负的。其实这也是正常的,因为对于盘口的多单和空单的数量是实时变化的,我们挂单时候的多空数量比,和下单成功时候的多空数量比,可能发生了显著的变化,所以这个策略还有优化的地方。

Penny Jump策略

接下来,我们提出第二种优化的思路。我们在上面的高频策略中,挂单价格的偏移是人为设置的。但是,还有一种情况,就是盘口的买一价和卖一价本身就有一定距离的偏移,我们可以使用这个偏移进行挂单。正常情况下商品期货主力合约交易比较频繁,盘口买一与卖一只有一跳的价差,几乎没有下手机会。所以我们把精力放到交易不是太活跃的次主力合约上面,次主力合约偶尔会有两跳甚至三跳的机会出现。比如在MA次主力909合约的盘口就出现下面这种情况:卖一为2225量551,买一2223量565,在几个tick推送后,这个价差消失,变成2225,2224,这种情况,我们视为市场的自我纠正。我们要做的就是去填补盘口差价空隙,如果速度够快,就可以排在委托单的最前位置,做为Maker以最快的速度成交后反手卖出,这个持仓时间很短。这种逻辑使用人工盯盘确实比较困难,因为商品期货盘口差价较大的情况很少出现,当出现的时候,市场也很快会去弥补。

image

这种策略有一个很可爱的名字,Penny Jump 策略,翻译成中文就是微量加价的意思,其原理是跟踪盘口买价和卖价,然后不停的根据盘口价格加上或减去微量价格挂单,很明显这是一个被动成交的挂单策略,属于卖方做市策略的一种。

了解完这个策略逻辑以后,我们来使用代码进行实现。同样可以使用我们原有的代码框架。在这里我们需要修改下参数的名称,原有的跳动区间改为盘口间隔。

这里需要获取实时的买价和卖价,然后计算两者的差额。当差额大于3的时候,我们立刻挂单,以当时买一价加一进行多头的挂单,以当时卖一价减一进行空头的挂单,这里就相当于填补盘口的差价。相对于前两种策略,这种下单的方式速度会更快一些。

如果成功下单,这时候就要设置对应的止盈价格和止损价格,对于多单来说,止盈价格是入场的卖价curask加上盈利的点数,止损点数是curask减去止损的点数;空单的止盈价格是入场的买价curbid减去盈利的点数,止损价格是curbid加上盈利的点数。

我们来试着运行一下,在回测页面,我们挑选非主力合约,玻璃403,盘口间隔设置为2,最大止损间隔为5,盈利区间可以设置为1。根据回测结果,可以发现策略似乎运行到一半停止了。这是什么原因呢,我们检查一下,我们打印下持有的仓位,结果发现,这里同时拥有了两个仓位,证明我们的开仓挂单同时交易成功,因此策略的逻辑被打乱了;这是因为Penny Jump策略下单的价格确实很接近,而前两种策略下单的距离我们设置为20,所以没有发生这种情况?具体应该怎样解决呢,其实也很简单,当检查到同时拥有仓位的时候,我们可以简单粗暴的同时平仓,当然也更好的办法也欢迎大家提出来。

/*backtest
start: 2023-08-01 09:00:00
end: 2023-08-02 15:00:11
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
mode: 1
args: [["symbol","FG403"],["deviation",2],["maxCoverDeviation",4]]
*/

function cancelOrders(symbol, offset) {
    var orders = null;
    while (1) {
        orders = _C(exchange.GetOrders);
        if (orders.length == 0) {
            break;
        }
        for (var i = 0; i < orders.length; i++) {
            if ((orders[i].ContractType == symbol && orders[i].Offset == offset) || typeof(offset) == "undefined") {
                exchange.CancelOrder(orders[i].Id, orders[i]);
                Sleep(interval);
            }
        }
        Sleep(interval);
    }
    return orders;
}

function trade(distance, price, amount) {
    var tradeFunc = null;
    if (distance == "buy") {
        tradeFunc = exchange.Buy;
    } else if (distance == "sell") {
        tradeFunc = exchange.Sell;
    } else if (distance == "closebuy" || distance == "closebuy_today") {
        tradeFunc = exchange.Sell;
    } else if (distance == "closesell" || distance == "closesell_today") {
        tradeFunc = exchange.Buy;
    }
    exchange.SetDirection(distance);
    return tradeFunc(price, amount);
}

function getOrderBySymbol(symbol, orders) {
    var ret = [];
    _.each(orders, function(order) {
        if (order.ContractType == symbol) {
            ret.push(order);
        }
    });
    return ret;
}

function hasOrder(orders, type, offset) {
    var ret = false;
    _.each(orders, function(order) {
        if (order.Offset == offset && order.Type == type) {
            ret = order;
        }
    });
    return ret;
}

var p = $.NewPositionManager(); //交易类库函数

var success_count = 0; //成功次数统计
var fail_count = 0; //止损次数统计

var orderlong = null;  //多头挂单价格
var ordershort = null; //空头挂单价格
var profitprice = null; //止盈挂单价格
var lossprice = null; //止损挂单价格

var tblStatus = {
    type: "table",
    title: "策略运行状态信息",
    cols: ["合约名称", "当前价格", "多头挂单", "空头挂单", "止盈价格","止损价格",  "持仓方向", "持仓价格", "持仓数量", "持仓盈亏", "止损次数", "成功次数", '时间'],
    rows: []
};

var curbid = null //开仓挂单买价
var curask = null //开仓挂单卖价
 
function main() {
    var initAccount = _C(exchange.GetAccount);
    var preprofit = 0 //先前权益
    var curprofit = 0 //当前权益
    var holdPrice = null //持仓价格
    var holdType = null //持仓类型
    var holdAmount = null //持仓数量
    var holdProfit = null //持仓盈亏
    var isLock = false //止盈挂单锁
    var cur_pos = null

    while (true) {
        if (exchange.IO("status")) {
            
            exchange.SetContractType(symbol);
            var t = exchange.GetTicker(); 
            var r = exchange.GetRecords();
            var ContractType = symbol
            var curprice = r[r.length-1].Close

            var positions = _C(exchange.GetPosition);
            var pos = [p.GetPosition(symbol, PD_LONG , positions), p.GetPosition(symbol, PD_SHORT, positions)];
            var orders = getOrderBySymbol(symbol, _C(exchange.GetOrders))
            var diff = t.Sell - t.Buy

            if (orders.length == 0 && (!pos[0] && !pos[1]) && diff >= deviation * priceTick) {
                profitprice = null
                lossprice = null
                isLock = false //解锁

                holdPrice = ''
                holdType = ''
                holdAmount = ''
                holdProfit = ''
                cur_pos = null

                preprofit = curprofit
                curprofit = exchange.GetAccount().Balance - initAccount.Balance
                LogProfit(curprofit, "权益", '&');

                if(preprofit < curprofit){
                    success_count += 1
                    $.PlotFlag(r[r.length-2].Time, '止盈离场', '止盈离场')
                }

                if(preprofit > curprofit){
                    fail_count += 1
                    $.PlotFlag(r[r.length-1].Time, '止损离场', '止损离场')
                }

                curbid = t.Buy
                curask = t.Sell

                // 当前没有挂单,没有持仓,基于盘口挂单
                trade("buy", curbid + 1, 1);
                trade("sell", curask -1, 1);

                orderlong = curbid + 1
                ordershort = curask -1
                
            } else if (pos[0] || pos[1]) {
                // 只要有持仓
                orderlong = null
                ordershort = null

                cur_pos = pos[0] ? pos[0] : pos[1]
                holdPrice = cur_pos.Price 
                holdType = pos[0] ? '多头方向' : '空头方向'
                holdAmount = cur_pos.Amount
                holdProfit = cur_pos.Profit

                if ((pos[1] && hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_OPEN)) || (pos[0] && hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_OPEN))) {
                    $.PlotFlag(r[r.length-1].Time, pos[0] ? '多单进场' : '空单进场', pos[0] ? '多单进场' : '空单进场')
                    cancelOrders(symbol, ORDER_OFFSET_OPEN);
                    Log('删除挂单:', pos[0] ? '空单' : '多单');
                }

                var longCoverOrder = hasOrder(orders, ORDER_TYPE_SELL, ORDER_OFFSET_CLOSE);
                var shortCoverOrder = hasOrder(orders, ORDER_TYPE_BUY, ORDER_OFFSET_CLOSE);

                if (pos[0] && !longCoverOrder && isLock == false) {
                    profitprice = curask + priceTick * profit
                    lossprice = curask - priceTick * maxCoverDeviation
                    trade("closebuy", profitprice, pos[0].Amount);
                    isLock = true //加锁
                }

                if (pos[1] && !shortCoverOrder && isLock == false) {
                    // 有空头持仓
                    profitprice = curbid - priceTick * profit
                    lossprice = curbid + priceTick * maxCoverDeviation
                    trade("closesell", profitprice, pos[1].Amount);
                    isLock = true //加锁
                }

                // 重新平仓条件
                if (pos[0] && longCoverOrder && t.Sell < lossprice) {
                    Log('多头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }

                if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
                    Log('空头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }

                // 双向持仓
                if (pos[0] && pos[1]) {
                    profitprice = null
                    lossprice = null
                    profitprice = null
                    lossprice = null
                    Log('双向持仓')
                    cancelOrders(symbol);
                    p.CoverAll(symbol);
                }
            } 

            $.PlotRecords(r, "K线数据")
            $.PlotLine("多头挂单", orderlong, r[r.length - 1].Time)
            $.PlotLine("空头挂单", ordershort, r[r.length - 1].Time)
            $.PlotLine("挂单盈利线", profitprice, r[r.length - 1].Time)
            $.PlotLine("挂单止损线", lossprice, r[r.length - 1].Time)

            tblStatus.rows = [];
            tblStatus.rows.push([ContractType, curprice, orderlong, ordershort, profitprice, lossprice, holdType, holdPrice, holdAmount, holdProfit, fail_count, success_count, _D()]);
            lastStatus = '`' + JSON.stringify([tblStatus]) + '`';
            LogStatus(lastStatus);
        } else {
            LogStatus("未连接", _D());
        }
        Sleep(interval);
    }
}

我们再次运行一下,设置盘口距离为3,止损点数是5,止盈点数为0,就是填补盘口间隔的盈利。根据最后统计结果,虽然成功了72次,失败24次,但是加上手续费的消耗,结果预估收益更为惨烈。

总结一下,我们讲述的这些高频策略并不是十分的完善,课程的目的呢主要是为了让大家了解高频做市策略的基础,其中可以优化的地方还有很多。比如具体挂单的时间,挂单的价格间隔,以及止盈和止损的设置,欢迎大家集思广益,提出更好的解决办法,大家可以发在评论区,我们也会争取为大家进行实现。

视频参考链接:

《C++商品期货高频交易策略之Penny Jump》

5.交易插件:定制半自动交易工具

FMZ身为量化交易平台,主要是为了服务程序化交易者。但也提供了基础的交易终端,虽然功能简单,但我们也可以通过终端进行下单,撤单,和查看行情账户等基本的交易操作。

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

这些自定义的交易插件,我们自身可以编码实现。并且这个交易终端是定制化的,这就意味着我们可以根据我们的交易习惯,自定义交易功能辅助面板。

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

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

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

我们来看下插件是怎样编写的,在新建策略页面,设置策略类型为:交易插件。语言编写支持JavaScript、Python、C++、My语言。 插件的main函数return的结果会在运行结束后,在终端弹出,支持字符串、画图和表格。因为插件执行看不到日志,可以将插件的执行结果return返回。

插件的用途很广泛的,在很多时候手动交易需要很多重复执行的操作,其实这些操作都可以用插件实现。今天,我们讲解的插件使用,是辅助手动期货跨期对冲的插件套装。

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

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

首先介绍的是画跨期差价插件,这里我们首先设置好外部的参数,期货合约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循环。

var chart = { 
    __isStock: true,    
    title : { text : '差价分析图'},      
    xAxis: { type: 'datetime'},         
    yAxis : {                       
        title: {text: '差价'},             
        opposite: false,             
    },
    series : [                    
        {name : "diff", data : []}, 
        {name : "meandiff", data : []}, 
    ]
}

function main() {
    exchange.SetContractType(Contract_A)
    var recordsA = exchange.GetRecords(1)
    exchange.SetContractType(Contract_B)
    var recordsB = exchange.GetRecords(1)

    var rlength = Math.min(recordsA.length,recordsB.length)
    var difflist = []
    
    for(var i = 0; i < rlength; i++){
        var diff = recordsA[recordsA.length - rlength + i].Close - recordsB[recordsB.length - rlength + i].Close
        chart.series[0].data.push([recordsA[recordsA.length - rlength + i].Time, diff])
        difflist.push(diff)
        var meandiff = TA.MA(difflist, 20)
        chart.series[1].data.push([recordsA[recordsA.length - rlength + i].Time, meandiff[meandiff.length - 1]])
    }
    return chart
}

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

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

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

在插件中,我们不想呈现多余的信息,所以SetErrorFilter设置一下,这在前面的海龟交易策略里面我们有提到过。这样一键对冲开仓插件就设置好了。

function main(){
    var tbl = { 
        type: 'table', 
        title: '开仓信息 ' + _D(), 
        cols: ['合约A', '合约A价格', '合约A方向', '合约B', '合约B价格', '合约B方向', '价差'], 
        rows: []
    }

    SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder"); 
    exchange.SetContractType(Reverse ? Contract_B : Contract_A)
    var ticker_A = exchange.GetTicker()
    if(!ticker_A){return '无法获取数据'}
    exchange.SetDirection('buy')
    exchange.Buy(ticker_A.Sell+Slip, Amount)
    exchange.SetContractType(Reverse ? Contract_A : Contract_B)
    var ticker_B = exchange.GetTicker()
    if(!ticker_B){return '无法获取数据'}
    exchange.SetDirection('sell')
    exchange.Sell(ticker_B.Buy-Slip, Amount)

    Sleep(1000)

    var pos = _C(exchange.GetPosition)
    if(pos){
        for(var i=0;i < pos.length; i++){
            if(pos[i].ContractType == Contract_A){
                var contractA = pos[i].ContractType
                var typeA = pos[i].Type == 0 ? '多头' : '空头'
                var priceA = pos[i].Price
            }
            if(pos[i].ContractType == Contract_B){
                var contractB = pos[i].ContractType
                var typeB = pos[i].Type == 0 ? '多头' : '空头'
                var priceB = pos[i].Price
            }
        }
        var diff = priceA - priceB
        tbl.rows.push([contractA, priceA, typeA, contractB, priceB, typeB, diff])
        return tbl
    }
}

接下来是最后一个插件的设置,一键平仓。这里我们只需要设置一个参数,滑价Slip。在主函数里,首先获取起始状态的仓位信息,如果当前没有仓位,直接返回’已无持仓’。在判断具有仓位信息的情况下,使用轮询的方式,找到对应的多头仓位和空头仓位,然后设置对应的合约,获取最新的ticker数据,然后设置对应的交易方向和函数,同样为了保证交易,这里使用加减滑价的设置。这里稍微提醒一下,这里的SetDirection需要设置为closebuy_today和closesell_today,如果我们没有加today,会有报错,没有平昨的仓位。注意:在回测系统,平今和平昨没有加以区分,都可以使用。

接下来,休息1s。然后获取结束状态的仓位pos_end,如果判断仓位已经全部平掉,打印’平仓完成’的信息。

function main(){
    
    SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|CancelOrder"); 

    var pos_start = exchange.GetPosition()
    if(!pos_start || pos_start.length == 0 ){return '已无持仓'}

    for(var i=0;i < pos_start.length; i++){
        if(pos_start[i].Type == PD_LONG){
            exchange.SetContractType(pos_start[i].ContractType)
            var tickerA = exchange.GetTicker()
            if(!tickerA){return '无法获取tickerA'}
            exchange.SetDirection('closebuy_today')
            exchange.Sell(tickerA.Buy - Slip, pos_start[i].Amount)
        }
        if(pos_start[i].Type == PD_SHORT){
            exchange.SetContractType(pos_start[i].ContractType)
            var tickerB = exchange.GetTicker()
            if(!tickerB){return '无法获取tickerB'}
            exchange.SetDirection('closesell_today')
            exchange.Buy(tickerB.Sell + Slip, pos_start[i].Amount)
        }
    }

    Sleep(1000)

    var pos_end = exchange.GetPosition()
    if(!pos_end || pos_end.length == 0 ){
        return '平仓完成'
    }
}

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

看了这个插件的使用,我们应该也有了自己的想法,大家可以根据自己的交易习惯,不妨写成插件方便自己的手动交易。大家如果有好的想法和需求,也可以提出来,我们尽力也会为你实现!

视频参考链接: 《商品期货预定开仓后平仓机器人》

6.托管者部署、Docker部署

大家好,FMZ平台最近更新了,可以看到,网页画面更加的清新,各个板块的布局和功能也在各位用户的建议下,设计的也更加的完善。但是有很多老朋友反应对新版UI的托管者设置不太熟悉,当然还有很多想进行仿真或者实盘交易的新朋友,对托管者的设置也存在一定的疑问。因此,本视频就来展示一下在各个系统进行托管者设置的详细步骤。

首先来讲解下托管者的功能,托管者是交易实盘的载体,是管理实盘的程序,主要负责处理底层数据、访问接口、通信等任务,所以可以理解为是交易策略的执行者。托管者运行在我们配置的服务器上,即使FMZ量化交易平台网站出现网络故障也不影响您的托管者运行。托管者可运行在本地的电脑,云服务器或者docker部署,系统支持Linux,Windows,Mac OS,android等等。需要注意的是,在策略运行期间,托管者也是需要一直运行的,当托管者关闭,实盘运行的策略也会停止运行。

下面我们就来讲解下在各个平台的部署步骤。打开托管者,点击部署托管者,这里有一键租用托管者和手动部署托管者。如果大家嫌手动部署比较麻烦的话,可以选择一键部署托管者。这里有入门级和内存性两种不同种类的托管者,适合于不同层次的量化选手。点击选择购买后,云服务器会自动的为我们进行托管者程序的安装和部署,并且对于python语言,也会安装一些量化交易常用的包和工具。稍微等待片刻,可以看到当托管者展示部署完成的时候,我们就可以直接使用。当我们不想使用的话,可以直接删除,扣费也会自动停止。

接下来进入手动部署的页面,手动部署托管者可以在我们自己购买的云服务器或者本地的电脑。本地电脑部署托管者可以根据策略运行的需要,随用随停,不需要额外的花费;但是当我们需要长时间运行一个策略的时候,相对来说云服务器是更加适合的。云服务器更新稳定,不容易受外部条件,比如硬件故障,断网或者停电的影响。关于云服务器的选择,一个初级入门,价格低廉的服务器就可以满足基本的策略运行需要。

可以看到这里有三大栏: Linux&Mac, Windows,和Docker部署。每一栏下面都有相应的安装包和具体安装步骤的讲解,并且也很贴心的在需要代码的地方,为我们提供了复制的按钮。

Windows系统部署

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

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

Mac OS系统部署(苹果电脑)

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

Linux服务器

以上都是部署在本地电脑上的,下面我们使用云服务器进行一下Linux系统的部署。这里有一个托管中部署的帮助文档大家可以参考一下,我给大家展示一下。首先登陆云服务器,具体登录云服务器的方法,大家可以根据自己的习惯,这里我们选择在腾讯云直接登录:

  • 第一步,在服务器输入wget下载托管者程序

wget https://www.youquant.com/dist/robot_linux_amd64.tar.gz

  • 第二步,输入这行代码进行robot解压;

tar -xzvf robot_linux_amd64.tar.gz

  • 第三步,测试托管者运行,输入这两行代码,然后添加发明者登录密码;
chmod +x robot
./robot -s node.youquant.com/数字串
  • 第四

更多内容