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

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

[TOC]

img

教程指南:该教程为发明者平台《商品期货量化交易实践系列课程》配套教程文本。 请配合视频一起使用,如果有错误,请及时提醒,我们后续将会不断完善该教程。

1.设计实践超级趋势策略

Hello,各位喜欢量化的小伙伴们,前期我们系统讲解了使用pine语言和javascript语言的进行期货量化交易的学习,课程部分呢可能过于理论性,因此讲述的风格比较严谨。有些同学们反应,虽然了解了基本的概念和理论,但是应用于实战还是感觉无从下手,因此,针对于这一痛点,从本节课开始,我们将开始实践性的讲解一些课程,从一行行的代码轻轻敲起,打开量化交易的大门。

虽然我们试图从0开始教学,但是有一些在前面课程中基础性的知识,我们不会过多于阐述。有些地方,我会标注来源于哪节课程,大家可以翻看复习下(欢迎翻看复习,前期视频播放量太惨了~~)。如果有遗漏的话,大家也可以留言弹幕或者评论区,我们会细心一条条进行解释。

万事开头难,有许多想学习量化学习的同学,经常苦恼于一个好的量化交易学习平台的选择。交易的数据作为实时的时间流数据,不像我们做一些基本的统计分析,只需要下载数据,导入编程语言进行分析就可以。如果想进行交易数据的量化分析,我们需要海量的历史数据,下载到本地的电脑肯定是不划算的。另外,如果想进行实时的模拟或者真实交易的话,我们还需要对接API接口、搭建回测平台等。从底层开始搭建我们的量化交易系统。而在发明者平台,这些问题通通帮你搞定。发明者平台有着海量的数据可以让你任意调取分析,完善的回测系统帮你查看策略的运行效果,同时拥有模拟回测和实盘交易系统,帮助我们学习和研究。学习一段时间后,可以使用操作仿真账户的实盘进行真正的跟盘测试。等到策略真正的成熟,可以应用于真实的实盘交易。在发明者平台,可以让我们下定决心的开始,简单、轻松一点入门量化交易领域。

FMZ平台托管者和交易所配置

前面啰嗦的够多了,现在开始,我将手把手教大家搭建一个操作仿真账户的实盘策略。操作仿真账户的实盘就和期货软件上的模拟账户一样,不是真金白银,但是对接的是真实的市场。我们可以申请一个仿真账户,「上期模拟」「N视界」 都可以,这里我们选择N视界。申请好账号以后,点开个人主页,基本信息中有我们的交易账户和密码,我们需要记录下来。

前面的课程,我们大多时间使用的都是模拟回测系统。在策略库中写好策略,就可以使用历史的数据查看策略的有效性。本次,我们要搭建一个操作仿真账户的实盘策略。除了一个可运行的策略之外,我们需要进行托管者和交易所的配置。

首先来进行托管者的配置,托管者可以理解为我们的交易策略的执行者。托管者运行在我们配置的服务器上,即使FMZ量化交易平台网站出现网络故障也不影响您的托管者运行。托管者可运行在本地的电脑,或者云服务器上,系统支持Linux,Windows,Mac OS等主流操作系统。托管者的配置很简便,我们点击部署托管者,然后根据不同的系统进行安装,具体的配置教程请看评论链接第一条。大家也可以选择一键租用托管者,这样会更加方便一点。这里我配置的托管者使用的是云服务器,一个入门级的服务器就可以。

接着我们来看交易所的配置,交易所可以添加模拟的仿真账户或者真实的期货账户,但是对于真实的期货账户,我们还需要进行看穿式认证。教程也会放在评论区。这里我们添加N视界模拟账号,在添加交易所页面我们首先选择:N视界模拟(NSight)。然后填写N视界仿真账户的交易账号和密码。就是我们刚才记录下来的N视界的模拟账号和密码,这里的密码是交易所的(交易所是一个泛指概念,这里指的是:N视界仿真账户),请大家不要填写发明者平台的密码。点击添加交易所,再填写发明者平台的密码验证,交易所就配置完成了。

编写超级趋势策略

接下来到了我们的重点,编写超级趋势策略。首先来认识一下这个指标。超级趋势指标(SuperTrend Indicator)是一种技术分析工具,用于识别市场趋势和价格动量。超级趋势指标基于波动性原理,通过计算当前价格与最高价/最低价的偏离程度来确定趋势方向,并提供了可能的买入和卖出信号。该指标兼具趋势跟踪和止损保护的功能,适用于各种交易策略。

超级趋势指标的交易策略是这样的:

  • 当价格上穿超级趋势线时,产生买入信号;
  • 当价格下穿超级趋势线时,产生卖出信号。

超级趋势指标的特点是灵活性较高,可以根据不同的市场和交易策略进行参数调整。它可以帮助交易者捕捉到趋势行情并避免明显的市场调整。

了解完基本的概念以后,我们来手敲代码进行实现。打开策略编写页面,选择语言为javascript。首先设置策略参数,这里我们设置三个参数,分别为:

变量名 描述 类型
Symbol 合约 字符串
pd 长度 数字型
factor 因子 也是数字型
/*backtest
start: 2023-06-05 09:00:00
end: 2023-08-27 15:00:00
period: 15m
basePeriod: 5m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["Symbol","SA401"],["pd",35]]
*/

// 全局变量
var mp = 0;
var _q = $.NewTaskQueue();

function SuperTrend(r, period, multiplier) {
    // atr
    var atr = talib.ATR(r, period)

    // baseUp , baseDown
    var baseUp = []
    var baseDown = []
    for (var i = 0; i < r.length; i++) {
        if (isNaN(atr[i])) {
            baseUp.push(NaN)
            baseDown.push(NaN)
            continue
        }
        baseUp.push((r[i].High + r[i].Low) / 2 + multiplier * atr[i])
        baseDown.push((r[i].High + r[i].Low) / 2 - multiplier * atr[i])
    }

    // fiUp , fiDown
    var fiUp = []
    var fiDown = []
    var prevFiUp = 0
    var prevFiDown = 0
    for (var i = 0; i < r.length; i++) {
        if (isNaN(baseUp[i])) {
            fiUp.push(NaN)
        } else {
            fiUp.push(baseUp[i] < prevFiUp || r[i - 1].Close > prevFiUp ? baseUp[i] : prevFiUp)
            prevFiUp = fiUp[i]
        }

        if (isNaN(baseDown[i])) {
            fiDown.push(NaN)
        } else {
            fiDown.push(baseDown[i] > prevFiDown || r[i - 1].Close < prevFiDown ? baseDown[i] : prevFiDown)
            prevFiDown = fiDown[i]
        }
    }

    var st = []
    var prevSt = NaN
    for (var i = 0; i < r.length; i++) {
        if (i < period) {
            st.push(NaN)
            continue
        }

        var nowSt = 0
        if (((isNaN(prevSt) && isNaN(fiUp[i - 1])) || prevSt == fiUp[i - 1]) && r[i].Close <= fiUp[i]) {
            nowSt = fiUp[i]
        } else if (((isNaN(prevSt) && isNaN(fiUp[i - 1])) || prevSt == fiUp[i - 1]) && r[i].Close > fiUp[i]) {
            nowSt = fiDown[i]
        } else if (((isNaN(prevSt) && isNaN(fiDown[i - 1])) || prevSt == fiDown[i - 1]) && r[i].Close >= fiDown[i]) {
            nowSt = fiDown[i]
        } else if (((isNaN(prevSt) && isNaN(fiDown[i - 1])) || prevSt == fiDown[i - 1]) && r[i].Close < fiDown[i]) {
            nowSt = fiUp[i]
        }

        st.push(nowSt)
        prevSt = st[i]
    }

    var up = []
    var down = []
    for (var i = 0; i < r.length; i++) {
        if (isNaN(st[i])) {
            up.push(st[i])
            down.push(st[i])
        }

        if (r[i].Close < st[i]) {
            down.push(st[i])
            up.push(NaN)
        } else {
            down.push(NaN)
            up.push(st[i])
        }
    }

    return [up, down]
}

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

    while (true) {
        while (!exchange.IO("status")) {
            Sleep(3000);
            LogStatus("正在等待与交易服务器连接, " + _D());
        }

        exchange.SetContractType(Symbol)

        var r = _C(exchange.GetRecords)

        if (r.length < pd) {
            Sleep(1000)
            continue    
        }
        
        var st = SuperTrend(r, pd, factor)
             
        $.PlotRecords(r, "K")
        $.PlotLine("up", st[0][st[0].length - 2], r[r.length - 2].Time)
        $.PlotLine("down", st[1][st[1].length - 2], r[r.length - 2].Time)

        if (isNaN(st[0][st[0].length - 3]) &&  !isNaN(st[0][st[0].length - 2])) {
            if (mp == 0) {
                _q.pushTask(exchange, Symbol, "buy", 1, function (task, ret) {
                    mp = 1;
                    
                    if (ret) {
                        Log(Symbol, "开多 #ff0000");
                    }
                });
            }else if (mp == -1) {
                _q.pushTask(exchange, Symbol, "closesell", 1, function (task, ret) {
                    mp = 0;
                    
                    if (ret) {
                        Log(Symbol, "平空 #ff0000");
                    }
                });
            } 
        }

        if (isNaN(st[1][st[1].length - 3]) && !isNaN(st[1][st[1].length - 2])) {
            if (mp == 0) {
                _q.pushTask(exchange, Symbol, "sell", 1, function (task, ret) {
                    mp = -1;
                    
                    if (ret) {
                        Log(Symbol, "开空 #ff0000");
                    }
                });
            }else if (mp == 1) {
                _q.pushTask(exchange, Symbol, "closebuy", 1, function (task, ret) {
                    mp = 0;
                    
                    if (ret) {
                        Log(Symbol, "平多 #ff0000");
                    }
                });
            } 
        }

        _q.poll();

        LogStatus('策略正在运行中', _D());
        Sleep(1000)
    }
}

我们回到代码部分,设置全局变量,虚拟持仓mp为0,_q代表多品种控制对象。然后编写我们的主函数,在主函数中,设置while循环,首先连接交易所,如果不成功,就输出状态"正在等待与交易服务器连接,",并使用_D()打印时间。exchange.SetContractType设置我们的合约,合约的名称是策略参数Symbol。然后获取我们的k线,接着设置如果k线长度不满足要求的话,设置休息1秒钟,然后continue;这样交易逻辑前面的部分我们就设置完成了。

接下来,交易逻辑部分,首先获取超级趋势指标。超级趋势指标在成熟的内置函数中是没有的,这里我们直接复制小小梦大神的计算超级趋势的指标函数。这段指标计算的函数还是比较复杂的,如果大家不理解的话,可以直接复制这段代码到chatgpt中,让它帮你进行梳理和解释。chatgpt作为提高生产力的好伙伴,大家有什么问题都可以向它请教,它的专业性和耐心都是相当可以的。这个超级趋势指标会返回一个数组,分别是up和down值。

接下来,我们进行画图的展示,原生的chart函数是比较复杂的,这里我们直接使用画线类库。使用$.PlotRecords画出k线,为了防止最新的k线没有走完,这里使用$.PlotLine画出倒数第二根k线的up和down值,然后对应的时间戳也是r[r.length - 2].Time

这里我们直接画图展示一下。模版应用勾选画线类库和商品期货交易类库,时间设置为今年6月到8月,合约名称为最近比较火的纯碱主力SA401合约。周期为15分钟。

我们点击回测,研究一下交易信号的获取和具体交易操作的执行逻辑。在图像中可以看到,上升的趋势是由up线主导的,而下降的趋势是由down线主导的,因此在up线和down线交汇的地方,可以认为是趋势转换的地方,我们可以进行相应的交易操作。

所以我们初步构思:

  • (1)由down线转换为up线,意味着从下降的趋势转变为了上升的趋势,这个时候可以进行平空开多的操作。
  • (2)由up线转换为down线,上升趋势改为下降趋势,这个时候进行平多开空。

使用程序化的语言怎么表达呢,由down线转换为up线,就是前一时刻的up值为空,这一时刻up值不为空,这里我们使用的是倒数第三根和倒数第二根。使用虚拟持仓,如果当前没有持仓,mp为0,使用pushTask进行下多单的操作,这里填写为buy,这里我们设置回调函数,为了防止重复的开仓,mp设置为1,然后设置开仓成功,打印出开多的信息;如果当前持仓为空头方向,mp为-1,使用pushTask,进行closesell平空的操作,设置mp为0,打印出平空的消息。

同样的思路,由up线转换为down线,上一时刻的down值为空,这一时刻的down值不为空,我们进行相应的开空和平多的操作。

最后,设置程序轮询间隔时间1000毫秒。这样基本的程序编写已经完成。这时刻要测试我们的代码运行效果了,根据回测结果,可以看到设置了交易任务,但是并没有设置具体的操作。具体的原因在哪里呢?回到代码,这里我们使用pushtask函数,忘了加上poll()函数执行具体的交易操作。

注意:这里策略参数是经过调参确定下来的,根据不同的合约,可以使用历史的数据确定策略的最终使用参数。

我们再次点击运行。根据回测结果,可以看到,策略代码根据我们的设想进行了相应的交易操作。根据收益曲线,在行情平稳的时候,策略的收益也是比较平稳,而在8月15号到8月21号行情出现剧烈上涨,策略这一波的上涨趋势抓的还是不错的。因此,这个指标对于趋势的把握还是有一定参考依据的。

在回测结果中应用的结果不错,我们可以应用于模拟实盘看下。设置实盘名称为模拟超级趋势实盘,k线周期为15分钟,运行策略为刚才我们编写的超级趋势策略。托管主机选择服务器,交易平台选择N视界。点击创建实盘。这个实盘就创建成功了。目前这个策略已经在发明者平台公开了,大家可以打开围观板块检查这个策略的效果。

本节课我们从托管者和交易所的配置的开始,交易理念的构思和交易策略的编写,回测和确定,以及最后实盘的创建。希望大家从这个流程中了解到一个完整的实盘创建流程。当然这个策略属于比较简单的策略,后续我们将会学习更多复杂策略的设置。如果大家有哪些编写策略的好的想法和电子,可以留在评论区,我们也将试图将他们编写成为成熟的策略进行量化展示,欢迎大家来投稿。

视频参考链接:

《托管者配置视频教程》

《看穿式认证教程》

《超级趋势实盘公开地址》

《JavaScript版本SuperTrend策略地址》

2.Pine脚本移植为多品种JavaScript策略

Trading ViewPine脚本语言相信大家都听说过,它是专门为Trading View平台设计的一种脚本语言,用来开发自定义的技术分析指标和策略。它结合了易读易写的特点和广泛的技术分析工具,使得用户呢,可以快速实现各种个性化的交易观点。TradingView上有很多受欢迎的策略都是由 Pine 脚本语言编写的。但是Pine 脚本确实在某些方面存在一定的局限性,其中之一就是它的单品种限制。目前的版本中,每个Pine脚本只能针对单个品种进行策略编写和回测。如果要在多个品种上使用相同的策略,通常需要为每个品种创建一个独立的实盘运行,确定不太方便。因此,有的同学就想使用pine语言的热门的交易指标同时管理多个品种,可以做到吗?可以,今天我们使用javascript语言来尝试移植一个Pine语言的单品种策略,成为多品种策略尝试一下。

指标讲解

今天我们介绍的热门的pine语言策略,是“Bottom and top hunter”,"顶部和底部猎人"指标。它结合了两个流行的技术分析工具,斐波那契回撤水平和相对强度指数(RSI),用来识别市场的潜在交易机会。

我们简单的介绍一下。斐波那契回撤水平是基于斐波那契数列。在交易中,斐波那契回撤水平根据最近的价格行为,确定潜在的支撑和阻力水平。该指标使用两个斐波那契水平值,通常设置为0.3820.6180.618黄金分割数,相信大家都听说过,自然界的很多比率都是黄金分割数。这里我们使用到了价格的涨跌规律中,使用这两个水平值0.3820.618,用来确定常见的回撤比率。

为了计算斐波那契水平,该指标考虑一个指定范围内的最高和最低价格,通常是最后一段时间周期内的的最高和最低价格。它计算出最高价和最低价之间的间隔。然后,通过从最高价中减去斐波那契比率来确定顶部阻力和底部支撑。

另外一个指标是RSI,他也是我们的老朋友,RSI值基于是收盘价计算出来的,用于衡量价格运动的速度和变化。它有助于识别市场中的超买和超卖状态。该指标中使用的RSI参数是RSI计算的长度,超买状态的上限阈值)和r超卖状态的下限阈值。

我们来看下该指标根据特定条件生成的买入和卖出信号:

  • 买入条件:当RSI穿过超卖水平并且收盘价高于底部支撑线。

  • 卖出条件:当RSI穿过超买水平并且收盘价低于顶部阻力线撤。

  • 买入条件:当RSI穿过超卖水平并且收盘价高于底部支撑线时,触发买入信号。这表明潜在的反转,代表斐波那契支撑水平反弹。

  • 卖出条件:当RSI穿过超买水平并且收盘价低于顶部阻力线时,触发卖出信号。这表明潜在的反转,代表斐波那契阻力水平回撤。

作为一个震荡指标,该指标结合了斐波那契回撤水平和RSI的力量,用来识别潜在的交易机会。它帮助我们,找到斐波那契支撑或阻力水平与RSI读数之间的共振,表明潜在的趋势反转或反弹。我们可以利用这些信息来做出关于进出市场位置的明智决策。

Pine语言实现

首先,我们来试着编写一下这个Pine语言策略。不熟悉Pine语言的朋友可以翻看,我们前面的视频,详细讲解了在fmz平台使用Pine语言进行商品期货的量化交易。

打开策略编辑页面,选择这个小松树,就是pine语言。命名为斐波那契教学pine。

/*backtest
start: 2023-06-01 09:00:00
end: 2023-08-29 15:00:00
period: 15m
basePeriod: 5m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["RunMode",1,360008],["ContractType","rb888",360008]]
*/

//@version=5
indicator("Top and bottom Hunter")

// Fibonacci Levels
fib_0 = input.float(0.382, "Fib Level 0")
fib_1 = input.float(0.618, "Fib Level 1")
range = input(4, "range")

// Calculate Fibonacci levels
fib_range = ta.highest(high, range) - ta.lowest(low, range) 
fib_level_0 = ta.highest(high, range) - (fib_range * fib_0) 
fib_level_1 = ta.highest(high, range) - (fib_range * fib_1) 

// RSI
rsi_length = input(14, "RSI Length")
rsi_overbought = input(70, "RSI Overbought Level")
rsi_oversold = input(30, "RSI Oversold Level")

rsi_value = ta.rsi(close, rsi_length)

// Determine buy and sell conditions
buy_condition = ta.crossover(rsi_value, rsi_oversold) and close > fib_level_1
sell_condition = ta.crossunder(rsi_value, rsi_overbought) and close < fib_level_0

// Plot Fibonacci levels
plot(fib_level_0, "fib_level_0", color=color.red, linewidth=2, overlay=true)
plot(fib_level_1, "fib_level_1", color=color.green, linewidth=2, overlay=true)

// Trade
if buy_condition
    strategy.entry("Enter Long", strategy.long)
else if sell_condition
    strategy.entry("Enter Short", strategy.short)

第一部分,编写斐波那契顶部和底部价格线。首先需要三个策略参数,使用input进行设置,分别是fib_0,初始值为0.382,fib_1,初始值为0.618,计算周期range初始值定义为4。接下来,我们来计算,第一个fib_range,它是range周期内的最高价减去range内的最低价,然后使用周期内最高价减去斐波区间乘以fib_0的值为0.382,确定顶部阻力价格;最高价减去fib_1,0.618确定底部支撑线。

接下来我们来计算rsi,也需要使用input进行三个参数的设置,第一个rsi计算周期,第二个rsi超买阈值,定义为70,第三个rsi超卖阈值,定义为30。计算rsi,使用内置函数ta.rsi,参数填写close 和 rsi_length。

两个指标计算过后,我们来确定交易的信号,第一个buy_condition,如果rsi上穿超卖阈值,使用ta.crossover,并且最新的收盘价大于底部支撑线,我们定义为可以平空开多。第二个sell_condition,刚好相反,如果rsi下穿超买阈值,使用ta.crossunder,并且最新的收盘价小于顶部阻力线,我们定义为可以平多开空。在一定意义上,这是一个震荡策略,而不是突破策略。

我们画图展示一下,使用plot,将指标,名称,颜色,宽度,overlay,是否设置在主图呈现,分别画出顶部线和底部线。

最后,我们进行交易的操作,如果满足buy_condition,使用strategy.entry进行long的操作;满足sell_condition,使用strategy.short进行short的操作。有些同学可能会好奇,不需要进行平多和平空么?在pine语言上,单向持仓,所以程序在开多和开空的时候,会自动的进行相反仓位的平仓操作。

我们回测测试一下,选择日期为最近的两个月,周期为15分钟,选择品种为最近走势比较平稳的螺纹钢。

在日志信息里可以看到,pine语言代码顺利的执行了我们的交易策略。有没有感觉pine语言的语法确定很简洁,这是因为pine作为专门的交易语言,对很多地方都进行了封装。大家感兴趣的话,可以观看我们前面的教程。有些同学好奇,把单品种策略改为多品种策略会不会很麻烦,尤其是刚刚看完我们用三节课讲完的多品种海龟策略以后。不用担心,今天呢,我们使用交易类库的CTA函数,同样实现简洁优雅。

CTA函数移植

我们前面讲过的cta函数,更多的是使用它作为单品种的趋势策略。其实它也可以实现多品种的策略设计。我们来尝试一下。在交易类库的源码中,有一段单品种的cta示例代码。品种这里我们只是使用了一个品种(这里的斜杠是映射,指K线信息看MA000, 下单映射到MA888主力连续上),其实这里可以填写一个合约的列表(使用逗号进行分割),cta函数会使用轮询进行处理。这里的处理逻辑在交易类库源码中,有具体的解释,大家可以翻阅下。将这里的单个合约,改为合约列表以后,会使用轮询的方式,对每个品种进行相应的交易处理。

/*backtest
start: 2023-06-01 09:00:00
end: 2023-08-31 14:20:00
period: 15m
basePeriod: 5m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","rb2310,hc2310,i2401"]]
*/

function main() {
    $.CTA(symbol, function(st) {
        var r = st.records
        if (r.length < rsi_length) {
            return
        }

        var fib_range = TA.Highest(r, range, 'High') - TA.Lowest(r, range, 'Low')
        var fib_level_0 = TA.Highest(r, range, 'High') - (fib_range * fib_0)
        var fib_level_1 = TA.Highest(r, range, 'High') - (fib_range * fib_1)

        var rsi_value = TA.RSI(r, rsi_length)

        $.PlotMultRecords("期货合约" + st.symbol, "k线", r, {layout: 'single', col: 12, height: '600px'})
        $.PlotMultLine("期货合约" + st.symbol, "斐波那契顶部", fib_level_0, r[r.length - 2].Time)
        $.PlotMultLine("期货合约" + st.symbol, "斐波那契底部", fib_level_1, r[r.length - 2].Time)

        var buy_condition = r[r.length-2].Close > fib_level_1 && rsi_value[rsi_value.length - 3] < rsi_oversold && rsi_value[rsi_value.length - 2] > rsi_oversold;
        var sell_condition = r[r.length-2].Close < fib_level_0 && rsi_value[rsi_value.length - 3] > rsi_overbought && rsi_value[rsi_value.length - 2] < rsi_overbought;

        if (st.position.amount <= 0 && buy_condition) {
            Log("当前持仓", st.position);
            return st.position.amount < 0 ? 2 : 1
        } else if (st.position.amount >= 0 && sell_condition) {
            Log("当前持仓", st.position);
            return st.position.amount > 0 ? -2 : -1
        }
    });
}

我们可以复制这段代码到策略编辑界面进行改写,相对于pine语言将参数设置在代码内部,javascript语言需要设置外部参数,基本和pine语言一样,只不过这里我们设置的symbol是合约列表。回到代码部分,我们删掉策略主体部分,只保留框架,这个函数中,会用到cta中一些简便的用法,对cta不熟悉的同学可以观看前面的视频,这里的合约改为合约列表,symbol。第一步获取k线,使用st.records,st是回调函数,包含轮询合约的k线,仓位,账户等信息,如果不满足长度要求,进行返回。接下来也是首先计算斐波那契顶部和底部阻力支撑线,使用的语法和pine语言几乎一致,只不过函数的大小写有些改变,第一个指标range,周期内最高价减去周期内最低价,然后计算顶部阻力fib_level_0,最高价减去fib_0乘以range,底部支撑线,最高价减去fib_1乘以range。

然后是rsi指标的获取,使用TA.RSI

画图展示,这次我们不使用原生的chart函数了,这次使用多图表画线类库,定义好图表的名称,“期货合约” + st.symbol,然后使用PlotMultRecordsPlotMultLine往图表上加上"k线", r,“斐波那契顶部”, fib_level_0,这里设置时间r[r.length - 2].Time,"斐波那契底部", fib_level_1。因此,我们想每行只展示一幅画,所以layout设置为’single’, col宽度为12,也就是全部, height为 ‘600px’。

接下来,根据两个指标决定买入信号和卖出信号。当判断最新的价格大于底部支撑的时候,并且rsi上穿超买rsi阈值,就是倒数第三根k线rsi小于30,倒数第二根k线大于30,我们定义为buy_condition。类似的,sell_condition,是当最新的价格小于顶部压力线,rsi下穿超买阈值,倒数第三根大于70,倒数第二根小于70。

定义好交易信号以后,我们进行交易的操作。需要注意的是,在cta框架中,下单操作是根据return返回值决定的,这部分的逻辑我们前面课程中着重将结果。如果当前品种的position.Amount仓位数量小于等于0,代表目前没有持仓或者持有空仓,当买入信号出现的时候,使用return进行交易的操作。如果判断当前的仓位如果小于0,证明是持有空仓,想要持有多仓,就要先平掉空仓,再开一手多仓,所以return返回正数 2;而如果当前仓位是0,直接开一手多仓就好,return返回正数1。

卖出信号出现,操作的思路也是一致的,根据当前的仓位如果是多仓,大于0,就要先平多仓,再开空仓,return 2;如果没有持仓,直接开一手空仓。这里的逻辑如果大家不清晰的话,大家可以翻看我们以前的视频。

下面我们进行一下回测,不要忘记勾选模版引用,这里我们使用了多图表画线类库和交易类库,勾选上。

在回测系统里,配置参数设置好时间,k线周期,策略的参数,可以使用默认值,最后symbol填写合约列表,螺纹钢,热卷,和铁矿石,黑色三兄弟,注意这里合约之间不要有空格。在回测结果里,看到cta函数很贴心的为我们进行了持仓状态和资金信息的展示,可以时刻关注策略的运行状态。这里我们看到铁矿石亏损是比较多的,所以我们可以更换一个合约尝试下。在图表显示里,可以看到使用多图表画线类库,也实现了多图表的呈现,这里可以切换下按钮,查看最近8小时的铁矿石走势。

这样我们就实现了将一个单品种pine语言策略,移植成为多品种javascript语言的策略,当然这只是一个简单的策略,所以改编的难度并不高,如果遇到复杂的pine语言策略,我们可以借助chatgpt首先理解策略的原理,然后再进行改写。可以发现,使用cta函数,同样实现了简洁优雅的特点,并且实现了多品种的交易。

可以发现两个语言的语法结构具有相当多的一致性,其实pine语言就是基于JavaScript语法,并添加了一些专门用于金融市场的扩展和函数。所以,如果我们熟悉JavaScript,学习Pine语言将更加容易。当然如果你是个pine语言高手,也可以尝试在fmz平台,使用javascript语言尝试实现更多的pine语言策略。

视频参考链接:

《TV斐波那契顶部底部猎人策略》

3.伪高频策略初探(一)

高频交易(High-Frequency Trading,HFT)是一种利用计算机算法进行快速、大量的交易的策略。高频策略依赖于快速执行交易订单,通常通过使用低延迟的交易系统和高速网络连接来实现。目标是以极快的速度抓取市场机会,并在瞬息万变的市场环境中进行交易。高频策略分为以下几种类型:

  • 套利交易:高频策略的一个主要目标是通过利用市场的微小价格差异来进行套利交易。这些差异可以是不同交易所、期货合约或其他相关资产之间的价格差异。高频交易员会迅速识别出这些差异并执行相应的交易,从而获得利润。

  • 做市策略:高频策略的另一种方式是充当市场交易商(Market Maker)。Maker通过同时提供买入和卖出报价,并从买方和卖方之间的价格差(即买卖价差)中获取利润。这种策略通常需要快速的交易决策和高度的流动性。

  • 统计套利:高频策略还可以基于统计模型和算法来寻找价格变动中的统计套利机会。这些算法会分析历史数据和市场信息,以发现可能的价格趋势、反转和其他模式,并根据这些模式执行相应的交易。

所以,可以总结呢,高频策略依赖于快速的执行和低延迟的交易系统,对硬件基础设施的要求非常高。对于我们个体散户来说,参与高频量化交易可能相对困难,因为高频交易通常需要专业的硬件系统、大量的市场数据和高速的交易执行能力。但是呢,个体投资者也可以通过获取可靠、实时和低延迟的市场数据,建立起快速且稳定的交易系统,以便在短时间内执行交易,这一点呢,在FMZ平台可以满足;同时针对于策略要求,可以选择开发自己的高频交易策略,基于特定的市场洞察、价格模式或其他统计模型。策略开发涉及到编写和测试算法代码,并进行回测和优化来验证其有效性。今天呢,我们就在fmz平台实现一个伪高频的做市策略,来尝试一下高频交易的魅力。

注:为什么叫做伪高频呢?因为真正的高频是毫秒级别的,本课程的高频策略是以百毫秒为时间间隔。

框架介绍

在FMZ上回测系统分为「模拟级别回测」、「实盘级别回测」。一般来说对于趋势策略,使用模拟级别回测比较合适,因为数据量小,回测速度也快。而对于伪高频策略,使用实盘级别回测则比较合适,接下来我们就一起来探讨,使用实盘级别回测设计伪高频策略。

我们挑选一种最简单的高频做市策略思路来设计。注意,本节课目的不是设计一个行之有效的策略。针对于个体投资者,有效的高频做市策略确实,难以发掘。本节课呢,我们使用高频做市的策略思路来设计策略,来了解高频策略的设计原理和程序的实现。策略原理是比较简单的。在盘口买单、卖单列表中挂单提供流动性做市,不对价格做任何预测。这样在市场流动性较好的时候,我们快速地获利了解,这样的风险在于市场单边运行时,手上会有亏损的单边头寸。

下面,我们来具体说明一下策略的设计思路。交易时间开启,我们根据当前的价格分别挂一个低于当前买一价偏移点位的多单,和高于当前卖一价偏移单位的空单,等待哪边的挂单先成交,然后取消相反方向的挂单。下单成功以后,这时候我们第二次挂单,多单情况下,以当前卖一价加上盈利点位的价格,进行多单平仓的挂单,或者空单情况下,以当前的买一价减去盈利点位的价格,进行空单平仓的挂单,当挂单成交以后,我们就获利了解。当然,也要考虑止损的情况,如果持仓以后,出现相反的单边行情,当亏损的价格达到亏损点位的时候,我们需要及时的进行止损。这里的挂单偏移点位,盈利点位,和亏损点位的设置都可以作为外部的参数进行调参。一笔交易完成以后,我们就再次开始以当前价格上下点位进行双向挂单的操作,继续循环上面的逻辑。

下面,我们来编写代码来尝试实现这样一个maker高频策略。首先设置策略参数,interval间隔,symbol合约,priceTick价格一跳点数,deviation跳动区间,maxCoverDeviation最大止损间隔,profit盈利区间。

我们可以先设想一下,如果想实现刚才讲述的交易逻辑。除了主函数外,需要几个辅助的功能函数。首先判断当前有没有挂单,没有挂单并且没有持仓才能进行挂单的操作。所以第一个我们需要一个查找目标品种order的函数,getOrderBySymbol,然后需要一个判断order类型的函数hasOrder,当成功判断当前order后,我们需要根据条件进行交易的操作,所以需要设置交易的函数,由于具有四个方向(开多,开空,平多和平空)所以也可以封装起来,作为第三个函数trade,当然我们还需要一个可以删除挂单的函数cancelOrders。这样伴随策略的主逻辑,我们的高频策略就可以进行了。这些功能函数的编写,我们将伴随主函数策略的运行进行完善。

代码解释

/*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() {
    
    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])) {
                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 ) { 
                    profitprice = t.Sell + priceTick * profit
                    lossprice = t.Sell - priceTick * maxCoverDeviation
                    trade("closebuy", profitprice, pos[0].Amount);
                }
                if (pos[1] && !shortCoverOrder) { 
                    profitprice = t.Buy - priceTick * profit
                    lossprice = t.Buy + priceTick * maxCoverDeviation
                    trade("closesell", profitprice, pos[1].Amount);
                }
                if (pos[0] && longCoverOrder) {
                    Log('多头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }
                if (pos[1] && shortCoverOrder && t.Buy > lossprice) {
                    Log('空头止损');
                    cancelOrders(symbol);
                    p.Cover(symbol);
                }
            }
        } else {
            LogStatus("未连接", _D());
        }
        Sleep(interval);
    }
}

回到代码部分,首先设置全局变量,p是交易类库函数,方便我们进行查询持仓和止损的交易操作,profitprice,和lossprice,代表下单过后设定的止盈价格和止损价格。

进入主函数, while (true) ,使用判断连接交易所的状态下,设置合约,由于是高频策略,需要我们获取ticker数据,和k线数据。起始阶段的挂单需要在没有持仓和挂单情况下,持仓我们可以通过GetPosition获取,具体的两个类型的仓位可以通过Position结构中的Type属性是PD_LONG还是PD_SHORT获取。然后我们需要获取对应品种的挂单信息,这里我们需要设置第一个函数getOrderBySymbol,这里的参数是合约,和orders列表,可以使用exchange.GetOrders获取到,首先设置ret作为返回结果,因为当前挂单的品种可能有很多,所以我们需要在所有order信息中获取到目标品种的挂单信息,使用_.each轮询,当查找到对应品种时,添加到ret,最后进行返回。这样就设置好了第一个模块函数getOrderBySymbol

获取到两个信息,我们开始交易逻辑的进行,如果当前没有挂单orders.length == 0,没有持仓,pos[0]和pos[1]都为空,会基于盘口挂单。这里我们使用trade功能函数进行挂单,首先我们来完善下这个函数,trade函数需要三个参数,设置参数为distance下单方向, price下单价格, amount下单价格,在函数体内,设置返回结果tradeFunc。接着根据distance,设置对应下单的函数。最后再根据distance设置exchange.SetDirection,在返回tradeFunc结果,里面填入价格和数量,这样我们的下单函数就封装好了。

回到主函数,继续刚才判断为没有持仓,没有挂单,使用trade进行下单的操作,分别设置为buy和sell。对于开多单,价格为当前的买价减去跳动区间;空单是卖价加上跳动区间。这样我们的开仓挂单就设置好了。

等待一个方向下单成功以后,就是pos[0] 或者pos[1]其中一个为真,这个时候就要取消另一个方向的挂单。这个时候需要先判断持仓和对应方向挂单的存在,使用hasOrder函数检查对应方向挂单。在hasOrder函数里,参数为orders挂单列表,type是类型,offset是开平仓方向。遍历挂单列表,找到对应方向挂单的话,进行返回。回到我们的主逻辑,检查持有空仓和多单挂单或者持有多仓和空单列表,我们就要取消对应方向的挂单,使用最后的一个功能函数。cancelOrders,参数为symbol合约和offset,开平仓方向,设置函数的主体部分,通过检查目前的挂单列表,如果长度为0,直接返回;否则进入轮询,找到对应合约的挂单,使用exchange.CancelOrder进行取消。 回到主函数,参数填写合约,和开仓订单的类型ORDER_OFFSET_OPEN,就可以取消对应方向的开仓挂单列表。然后我们打印出删除对应方向挂的信息。

接下来就要进入我们的平仓逻辑的设置了。在持有仓位以后,我们就要挂一个止盈的单子进行平仓。所以在检查到持有仓位并且没有挂止盈单的情况下,进行挂单。这个时候首先需要检查是否已经挂单,使用刚才讲过的hasOrder函数, 分别检查多单平仓挂单longCoverOrder,和空单平仓挂单shortCoverOrder的存在。

接下来就进入止盈挂单的设置,如果检查到有多头仓位并且没有挂单,首先设置止盈价格,为当前的卖价加上盈利的点数,止损价格为当前卖价减去止损区间的点数,然后使用trade函数设置参数closebuy, 价格止盈价格 ,数量是多头仓位的数量。

对于空头仓位处理的逻辑也是一致的,检测到没有挂单,计算相应的止盈价格和止损价格,然后使用trade进行挂单。

止盈的挂单并不是一定能成交的,当遇到单边损失的行情,我们就要及时的进行止损。同样分为多头止损和空头止损两种情况。

当判断条件拥有多头仓位,和止盈挂单,但是最新的价格小于止损点数,就要进行多头止损的操作,打印出需要进行止损,然后cancel止盈的挂单,使用cover交易类库函数进行迅速的平仓。

空头仓位的逻辑也是一样的,判断是否达到止损的位置,如果是删除挂单,使用cover进行平仓。

最后我们设置好未连接状态的信息显示,和策略的休息间隔。到这里就是我们做市进行伪高频交易的逻辑了。我们可以先运行一下。

我们回测运行一下,设置时间是8月1日,上午9点到下午3点,k线周期为1分钟,这里选择实盘级tick。策略参数合约选择rb2310,其它参数设置为初始值就好。 从回测结果数据中可以看到平仓盈亏是正数,但是亏损都在手续费上,这确实符合高频交易的特点,频繁地交易确实会花费更多的手续费。在日志信息里,可以看到准时9点开盘,我们进行双向的挂单,当其中一个方向挂单成交以后,撤销另一个方向的挂单,然后挂止盈的单子,当止盈单子成交以后。就要重新挂单,当然并不全是止盈的交易,也会遇到止损的情况,当最新的价格达到止损点数的时候,会撤掉止盈的单子,进行止损平仓的操作。

附加plus版本:

/*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));

            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, '止损离场', '止损离场')
                }

                orderlong = t.Buy - priceTick * deviation
                ordershort = t.Sell + priceTick * deviation

                // 当前没有挂单,没有持仓,基于盘口挂单
                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

更多内容