资源加载中... loading...

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

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

字串


* 第四步,当出现Login OK就代表云端托管者设置成功。

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

## Docker镜像部署

最后,我们来看下使用docker部署托管者,这是我们使用云服务器进行部署,首先在云服务器下载安装docker,不熟悉的朋友可以询问chatgpt,按照步骤安装docker。

* 在确保docker安装完成后,按照教程首先下载镜像,运行这段代码:
```docker pull youquantcom/docker:latest```

* 运行镜像,输入这段代码: 
```docker run -d --name 优宽Docker -e UID=数字串 -e ZONE=CN -e PASSWORD=密码  youquantcom/docker```

* 输入这段代码查看docker日志:
```优宽Docker logs```

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

以上呢,就是在不同平台部署托管者的详细步骤,大家可以参考一下,如果有问题,也可以留言评论区,我们会为大家热心解答。

视频参考链接:
[《优宽量化的托管者概念》](https://www.bilibili.com/video/BV1Lb4y1q7KN/?share_source=copy_web)
[《优宽量化交易平台托管者部署简介》](https://www.bilibili.com/video/BV1VD4y1c7fa/?share_source=copy_web)

# 7. 热门Pine语言移植:抓住那个涨停板!

最近有小伙伴联系我们,说在```Trading View```上有一个策略很神奇,可以抓住一个涨停的品种,并且可以逐步加仓,在数字货币取得了很好的收益。刚好,最近八月份纯碱的期货市场很是热闹,出现了移仓换月时的“多头逼仓现象”,问我们能不能改造成为商品期货的版本,应用于国内的期货市场。我们的答案当然是可以!今天我们就来看一下,怎样将TradingView上的热门Pine语言策略,应用于商品期货市场。

我们首先来看一下这个策略,可以看到这个策略名称叫做“BTC金字塔策略”,小火箭有500多个,确实很受欢迎。通过策略回测图像可以看到,这个策略对于单边上涨的趋势确实抓的很好,并且也可以实现较好的入场和出场。

我们来看下策略的源码。代码长度一共有94行。优宽平台也是可以运行Pine语言的,我们复制这段代码到优宽,看这段代码能否应用于国内的期货市场。

```Pine
//@version=4

strategy(title='Pyramiding BTC 5 min no security', overlay=true, pyramiding=7, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=20, commission_type=strategy.commission.percent, commission_value=0.075)

//
fastLength = input(250, title="Fast filter length ", minval=1)
slowLength = input(500,title="Slow filter length",  minval=1)
source=close
v1=ema(source,fastLength)
v2=ema(source,slowLength)
//
//Backtest dates
fromMonth = input(defval=1, title="From Month")
fromDay = input(defval=10, title="From Day")
fromYear = input(defval=2020, title="From Year")
thruMonth = input(defval=1, title="Thru Month")
thruDay = input(defval=1, title="Thru Day")
thruYear = input(defval=2112, title="Thru Year")

showDate = input(defval=true, title="Show Date Range")

start = timestamp(fromYear, fromMonth, fromDay, 00, 00)  // backtest start window
finish = timestamp(thruYear, thruMonth, thruDay, 23, 59)  // backtest finish window
window() =>  // create function "within window of time"
    time >= start and time <= finish ? true : false

//

length = input(50, title = "Period", type = input.integer)

hma(_src, _length)=>
    wma((2 * wma(_src, _length / 2)) - wma(_src, _length), round(sqrt(_length)))
    
hma3(_src, _length)=>
    p = length/2
    wma(wma(close,p/3)*3 - wma(close,p/2) - wma(close,p),p)

b =hma3(close[1], length)
//plot(a,color=color.gray)
//plot(b,color=color.yellow)
close_price = close[0]
len = input(25)

linear_reg = linreg(close_price, len, 0)


filter=input(true)

buy=crossover(linear_reg, b)

longsignal = (v1 > v2 or filter == false ) and buy and window()

//set take profit

ProfitTarget_Percent = input(3)
Profit_Ticks = close * (ProfitTarget_Percent / 100) / syminfo.mintick

//set take profit

LossTarget_Percent = input(10)
Loss_Ticks = close * (LossTarget_Percent / 100) / syminfo.mintick


//Order Placing

strategy.entry("Entry 1", strategy.long, when=strategy.opentrades == 0 and longsignal)

strategy.entry("Entry 2", strategy.long, when=strategy.opentrades == 1 and longsignal)

strategy.entry("Entry 3", strategy.long, when=strategy.opentrades == 2 and longsignal)

strategy.entry("Entry 4", strategy.long, when=strategy.opentrades == 3 and longsignal)

strategy.entry("Entry 5", strategy.long, when=strategy.opentrades == 4 and longsignal)

strategy.entry("Entry 6", strategy.long, when=strategy.opentrades == 5 and longsignal)

strategy.entry("Entry 7", strategy.long, when=strategy.opentrades == 6 and longsignal)


if strategy.position_size > 0
    strategy.exit(id="Exit 1", from_entry="Entry 1", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 2", from_entry="Entry 2", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 3", from_entry="Entry 3", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 4", from_entry="Entry 4", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 5", from_entry="Entry 5", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 6", from_entry="Entry 6", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 7", from_entry="Entry 7", profit=Profit_Ticks, loss=Loss_Ticks)

我们复制到优宽策略编辑页面,策略开头说明使用的pine语言版本是version4,因此它的语法和version5有一些小的区别,大家需要注意一下,但是在优宽平台这两个版本的pine语言都是支持的。

原始的strategy里面定义了很多参数,但是这些参数有的不适合期货市场,有的在回测系统有设置,所以我们只需要改变保留pyramiding最大加仓次数参数就可以。

我们继续向下看,这里代码前面的斜杠删除,首先定义快速滤波器长度fastLength,和慢速滤波器长度slowLength参数,初始值为250和500,然后设置source为收盘价。使用ema指数移动平均值分别计算快速滤波器v1和慢速滤波器v2。

第二部分,定义了回测周期,这在优宽的模拟回测页面可以定义,所以这部分可以删除。

继续往下看,这里首先定义周期参数length,然后定义了Hull移动平均线函数hma,和改进的Hull移动平均线函数hma3,经过检查发现,hma函数是没有使用到的,我们可以直接删除;并且hma3中的_src参数没有使用到,我们也要改下,将这里的close改为_src;然后使用hma3函数计算移动平均线,定义为变量b。

第四部分,获取当前收盘价定义为close_price,定义线性回归的长度参数,默认值是25,接着利用linreg内置函数计算线性回归值linea_reg。

这里定义一个过滤器参数,默认为true。

指标计算完成以后,接着就要定义交易信号了。当linear_reg上穿Hull移动平均值b的时候,定义为buy。然后买入信号的发出还可以添加过滤器的确定,当设置过滤器filter为真,我们需要检查快速滤波器的指数移动平均值v1是否大于慢速值v2;当过滤器信号和buy双向确定,这时候确定发出交易信号longsignal,这里的回测周期window删除。如果设置filter为假,只需要buy信号确定就可以了。

然后再止盈止损的设置了,设置止盈百分比参数ProfitTarget_Percent,计算止盈目标点数,使用当前收盘价乘以百分比,这里的syminfo.mintick是每跳的点数;接着止损参数的设置也是一样的步骤,初始值为10,然后计算止损目标点数。

当买入信号设置完成以后,接着就是交易的操作了,第一笔是没有持仓strategy.opentrades等于0,并且买入信号成立,产生第一笔买入订单,随后当已有一笔买入订单且满足买入条件时,买入第二笔,接着也就是第三到第七笔交易的完成。当强劲的上涨信号连续出现的时候,我们就要及时的加仓,直到完成加仓的上限。

接下来就是止盈和止损的操作,分别设置好七笔的止盈目标和止损目标,这样原始的pine语言代码我们就拆解完成了。

//@version=4
strategy(title='鉴别涨停品种策略', overlay=true, pyramiding=5)

fastLength = input(250, title="Fast filter length ", minval=1)
slowLength = input(500,title="Slow filter length",  minval=1)
source=close
v1=ema(source,fastLength)
v2=ema(source,slowLength)

length = input(50, title = "Period", type = input.integer)
hma3(_src, _length)=>
    p = length/2
    wma(wma(_src,p/3)*3 - wma(_src,p/2) - wma(_src,p),p)
b =hma3(close[1], length)
plot(b,color=color.yellow)

close_price = close[0]
len = input(25)
linear_reg = linreg(close_price, len, 0)

filter=input(true)
buy=crossover(linear_reg, b)
longsignal = (v1 > v2 or filter == false ) and buy 

//set take profit

ProfitTarget_Percent = input(3)
Profit_Ticks = close * (ProfitTarget_Percent / 100) / syminfo.mintick

//set take loss

LossTarget_Percent = input(10)
Loss_Ticks = close * (LossTarget_Percent / 100) / syminfo.mintick


//Order Placing

strategy.entry("Entry 1", strategy.long, when=strategy.opentrades == 0 and longsignal)
strategy.entry("Entry 2", strategy.long, when=strategy.opentrades == 1 and longsignal)
strategy.entry("Entry 3", strategy.long, when=strategy.opentrades == 2 and longsignal)
strategy.entry("Entry 4", strategy.long, when=strategy.opentrades == 3 and longsignal)
strategy.entry("Entry 5", strategy.long, when=strategy.opentrades == 4 and longsignal)
strategy.entry("Entry 6", strategy.long, when=strategy.opentrades == 5 and longsignal)
strategy.entry("Entry 7", strategy.long, when=strategy.opentrades == 6 and longsignal)

if strategy.position_size > 0
    strategy.exit(id="Exit 1", from_entry="Entry 1", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 2", from_entry="Entry 2", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 3", from_entry="Entry 3", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 4", from_entry="Entry 4", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 5", from_entry="Entry 5", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 6", from_entry="Entry 6", profit=Profit_Ticks, loss=Loss_Ticks)
    strategy.exit(id="Exit 7", from_entry="Entry 7", profit=Profit_Ticks, loss=Loss_Ticks)

可以发现,我们的改动确实很少,接着就要使用改写完成的策略,使用到国内的期货市场,看看运行效果怎么样?我们在回测页面定义一下时间,8月1号到8月31号,k线周期使用建议的5分钟。策略的原始参数我们不加以改变,执行方式是实时价模型,品种代码是SA888,我们来运行一下。可以看到在这一个月的时间,在行情比较平稳的时候8月1号到8月20号,确定没有进行任何的交易,当行情出现剧烈上涨,也就是8月21日,我们开始了交易,并且在8月26日达到了收益顶峰,虽然之后产生了比较大的亏损,但是最后的盈利仍然是27000多元,证明比较好的抓住了一个品种的上涨单边行情。但是这个策略是完美的吗,我可能要给大家泼一盆冷水了,当我们延长回测时间,到9月15的时候,可以发现策略收益回撤到了4000多元,证明该策略并不能保证稳定的获得盈利,根据回测图像发现,当行情出现剧烈下跌的时候,该策略没有实现及时的止损。因此,该策略作为单边上涨的入场信号判断,还是比较准确的,但是对于出场信号,设计的可能比较草率,大家可以思考一下,怎么更好的设置出场?如果有好的想法,大家也可以提出来,我们一起来实现。

Pine语言语法确实很优雅,作为专门的交易语言,它对于各个信号的计算和交易操作的设置,都是简洁明朗。当我们使用Javascript语言,有时候会嫌弃过于它过于繁琐。我相信把这个Pine语言策略改为Javascript语言,代码的长度只会只多不少,大家也不一定感兴趣。但是如果我们发挥Javascript语言的优势,大家有没有什么好的想法。

我们看到上面这个策略对于单边上涨的趋势判断的还是比较准确的,但是Pine语言一次只能针对于一个品种,而如今期货市场高达上百个品种,每个品种在一个月的时间内总会有几个出现剧烈上涨或者下跌的行情,我们使用这个策略一个个去判断,确实不太方便。如果我们建立一个实盘,使用轮询的方式,对期货市场的所有品种进行检测,然后给出重点关注的品种,也许可以帮助大家抓住更多的盈利机会。

下节课呢,我们就将这种想法使用Javascript进行实现,大家稍微等待一下,等我们设计完善,马上发布!

视频参考链接: 《BTC五分钟金字塔策略》

8. 商品期货多品种涨停预测推送策略

在各位小伙伴们的催促下,Javascript版的多品种涨停鉴定策略终于出炉了。相对于Pine语言只能实现单品种涨停信号的识别和交易,Javascript语言发挥自身的优势,可以实现多品种合约涨停信号的判断和信息的及时推送。大家在进行期货交易的过程中,经常会收到类似大师每日涨停品种推荐的信息,有不少的朋友都付费尝试过,今天呢,我们就使用量化策略打造一个大师,帮助我们进行每日交易的参考。需要注意的是,本策略只是进行涨停信号的判断,至于具体的交易操作,大家可以自行添加,有不明白的地方可以留言。

/*backtest
start: 2023-08-01 09:00:00
end: 2023-08-31 23:00:00
period: 5m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["Instruments","SA888"]]
*/

var DetectManager = {
    New: function(symbol) {

        var obj = {
            symbol: symbol,
            isFirst: true,
            PreBarTime: 0,
            buySignal: false,
            rData: [],
        };
    
        obj.Show = function() {
            return [obj.symbol, obj.buySignal]
        };

        // 信号鉴定逻辑
        obj.Detect = function() {

            _C(exchange.SetContractType, obj.symbol);

            var r = exchange.GetRecords()

            if(filter){
                if(r.length < slowLength) return
            }else{
                if(r.length < length) return
            }

            // hma均线计算
            function hma3(_src, _length) {
                var p = _length/2
                var p_3 = talib.WMA(_src,p/3) 
                var p_2 = talib.WMA(_src,p/2)
                var p_1 = talib.WMA(_src,p)
                var ret = []
                for (var i = 0; i < p_1.length; i++) {
                    if(p_1[i] > 0){
                        ret.push(p_3[i] * 3 - p_2[i] - p_1[i])
                    }
                }
                var hmares = talib.WMA(ret, p)
                return hmares
            }

            if(obj.isFirst){
                for(var i = 0; i < r.length - 1; i++){
                    obj.rData.push(r[i])
                }
                obj.PreBarTime = r[r.length - 2].Time
                obj.isFirst = false
    
            } else {
                if(obj.PreBarTime != r[r.length - 2].Time){

                    Log('K线更新')
                     obj.rData.push(r[r.length - 2])
                    var v1 = TA.EMA(obj.rData,fastLength)
                    var v2 = TA.EMA(obj.rData,slowLength)

                    var hmavalue =  hma3(obj.rData, length)

                    //  线性回归值
                    var linear_reg = talib.LINEARREG(obj.rData, len, 0)
                    var buy = linear_reg[linear_reg.length - 2] < hmavalue[hmavalue.length - 2] && linear_reg[linear_reg.length - 1] > hmavalue[hmavalue.length - 1]
                    obj.buySignal = (v1[v1.length - 1] > v2[v2.length - 1] || filter == false ) && buy

                    Log('检查品种:', obj.symbol, '检查结果:', obj.buySignal)

                    obj.PreBarTime = r[r.length - 2].Time
                }else{
                    obj.buySignal = false
                }
            }
        }
        return obj
    }
}

function main() {

    var arr = Instruments.split(',')
    var detectlist = []

    for (var i = 0; i < arr.length; i++) {
        var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, "")
        var obj = DetectManager.New(symbol)
        detectlist.push(obj)
    }

    while(true){
        if(exchange.IO('status')){
            LogStatus("已经连接CTP, " + new Date())
            var tblRecommand = {
                type: "table",
                title: "推荐品种",
                cols: ["时间", "合约名称"],
                rows: []
            }

            var recommandlist = []
            for (var i = 0; i < detectlist.length; i++) {
                detectlist[i].Detect();
                var res = detectlist[i].Show();

                if(res[1] == true){
                    tblRecommand.rows.push([_D(), res[0]])
                    LogStatus(tblRecommand)
                    recommandlist.push(res[0])
                }
            }

            if(recommandlist.length > 0) Log(_D(), '推荐品种:', recommandlist, "@")
        }else{
            LogStatus('未连接CTP', _D())
        }
        Sleep(1000);
    }
}

话不多说,我们直接开始。首先我们进行的还是多品种策略框架的设计,这里我们参考了多品种海龟策略的框架,将信号计算和品种轮询进行分割,这样代码各部分更加清晰,完善起来也更加方便。第一部分,我们创建DetectManager变量,这个对象主要就是用来鉴别各品种的涨停信号;第二部分是main主函数,这里的主函数首先创建各品种涨停信号计算的逻辑对象,然后是while循环,该循环为策略的主要循环,这一部分主要是遍历所有合约信号计算的逻辑对象,进行涨停信号的判断,然后实现及时的推送。还记得我们前面讲过的CTA函数的多品种策略吗,大家也可以选择使用,将最后的return结果返回0,不进行任何操作就可以。

多品种策略框架搭建好以后,第一部分我们来编写DetectManager变量。首先创造New函数,作为构造函数。这里我们设置参数是合约的名称symbol。然后创建不同合约对象的属性信息,包括合约名称symbol,isFirst代表是否为第一次获取k线,PreBarTime先前的k线时间,buySignal涨停信号的初始值,rData是获取的k线值。接着我们定义Show方法,用来展示当前品种的名称和涨停信号的真假。

最后来定义Detect方法,用来处理信号鉴定逻辑。这里我们首先合约的设置和k线的获取。这里设置好合约。接着来处理k线。为了防止未完成的k线造成的假性信号,这里我们决定舍弃最新的一根k线进行信号的计算。在策略初始阶段,obj.isFirst为true,将倒数第二根k线之前的数据添加到obj.rData中,赋值PreBarTime为倒数第二根k线时间戳,然后定义obj.isFirst为false。接下来,只有倒数第二根k线更新,我们再进行添加。这里不要忘了,也要更新PreBarTime。

K线获取好以后,接下来就要参考Pine语言,进行涨停信号的判断。我们来看下Pine语言的源代码,第一部分是快速和慢速过滤器的设置,这里需要过滤器filter参数,快速滤波器周期fastLength,和慢速周期slowLength参数,我们设置好。具体v1,和v2的计算,Pine语言使用ema内置函数计算出来的,在Javascript语言中,使用TA.EMA可以计算。第二部分,是Hull移动平均值,通过构造hma3函数进行计算。这里我们需要定义一个参数,length。Pine语言函数里面使用到了wma进行计算,Javascript中talib函数有内置函数,我们可以直接使用。我们来进行hma3函数的编写,这里定义两个参数_src和_length。定义p为_length除以2,接着计算不同周期的WMA值,定义为p_3,p_2和p_1。接着来计算ret,作为最后一个wma计算使用的数组,使用p_3,p_2和p_1中的每个元素,因为在Javascript中,talib.WMA计算使用的数组中不允许出现空值,所以当空值最多的p_1数组不为空,这里定义为大于0的时候,我们再进行添加。然后最后计算hmares进行返回。这里的hma3计算确实比较麻烦一点,大家需要注意一下。回到主逻辑,这里的第二部分,使用构造的函数,填写参数rData和hma周期,进行hma数值的计算。

第三部分,是线性回归信号的判断,这里也可以使用内置函数talib.LINEARREG,参数填写为k线数据,周期len,我们再添加一个参数len。最后的offset偏移值为0。

最后是涨停信号的判断了,如果linear_reg上穿hma均线,判断为buy,这里定义倒数第二根linear_reg小于hma均线,最新linear_reg大于hma均线。不要忘了还有过滤器的设置,如果过滤器为真,还需要判断快速滤波器v1是否大于v2,过滤器为假的话,直接判断buy信号就可以。

因为想实现不同品种的合约涨停信号的计算和信息的推送,是在k线更新的那一刻完成的,所以涨停信号的计算移动到当k线更新的时候,这一部分进行计算。另外还需要设置当k线没有更新的时候,我们需要及时的将buySignal设置为假,这样就可以避免在buySignal为true的k线周期内,重复的推送信息。

最后return obj这样就完成了第一部分DetectManager的设置。接下来来到我们的主函数,首先,这里我们需要定义参数Instruments,放入我们感兴趣的品种名称。回到主函数,这里将合约列表Instruments进行分割成arr,然后定义detectlist列表,用来存放信号计算的逻辑对象,接着使用for循环,首先进行正则化处理,然后使用DetectManager.New构造不同合约的逻辑对象,添加进入detectlist列表中。

接着进入主循环,创建变量recommandlist推荐列表变量,使用for循环进行不同合约的信号判断,首先使用Detect方法,接着Show()方法获取信号判断的结果,当信号判断为真,就是res的第二个值,将合约的名称添加进入recommandlist推荐列表当中。

如果recommandlist推荐列表不为空,证明该时刻判断某些品种出现了涨停信号,这个时候我们进行信息的及时推送。在TV上有一个报警机制,当我们设置的信号触发的时候,可以发送APP提醒或者通过webhook进行推送。在优宽平台,我们也可以实现这个功能。

推送设置在账号设置里面,可以选择APP,邮箱和webhook推送,这样当今日推荐的涨停品种信号触发的时候,可以设置推送到我们指定的app,邮箱或者webhook,帮助我们及时接受信息。APP,邮箱我们都很熟悉,这里通过设置webhook接口,可以帮助我们将最新的推送消息发送到我们设定的渠道,这里有一篇调用钉钉接口实现机器人推送消息的,大家有兴趣可以实践一下;同时,我们还可以利用python创建一个服务,用来接收优宽的推送消息。推送渠道设置好以后,在我们的代码中,当推荐列表不为空,我们打印当前的时间,推荐的品种。最后加上@符号就可以完成推送。

这里我们也可以设置状态表进行展示,创建tblRecommand,当有品种判断信号为真,添加进入tblRecommand进行展示。结尾部分,设置未连接状态的状态栏展示,和Sleep。

以上呢,就是多品种涨停鉴定策略的代码编写。我们可以回测运行一下,根据回测日志,可以看到使用黑色三兄弟,在8月1号到8月31号,我们获取到了29次涨停信号的提醒。我们会发布这个策略代码到策略广场,大家有兴趣可以更换不同的参数和合约品种进行更多的尝试!

我们也建立了相应的实盘,这里我们稍微修改了下代码。每次K线更新,会打印检查的品种和结果,这样可以让我们实时了解策略的运行进度,并且当有信号触发的时候,会发送到APP和邮箱,作为交易的参考。这样呢,我们就可以实现多品种涨停信号的监测和推送。其实,这样类似的应用场景还有很多,欢迎大家提出来,我们会热心的帮你实现!

视频参考链接: 《热门Pine语言策略--抓住那个涨停板!》 《升级商品期货多品种海龟交易策略以及回测说明》

9.基于TradingView信号执行交易

在TradingView上有很多可以选择使用的指标、策略、代码等,这些都可以在TradingView上直接运行,可以画线、计算、显示交易信号等。并且TradingView有实时的价格数据、充足的K线数据方便各种指标计算。TradingView上这些脚本代码叫做PINE语言,唯独一点不太方便的就是在TradingView上进行实盘交易。虽然在优宽上已经支持PINE语言,也可以实盘运行。但是也有TradingView的铁粉还是希望以TradingView上的图表发出的信号去下单交易,那么这个需求也可以通过优宽来解决。

大家都知道某些商品是具有外盘的,这些外盘合约对国内期货价格的变动会有显著的影响。比较有名的比如新加坡的铁矿石合约,美国的原油和农产品合约,英国的贵金属合约等。这些合约的交易时间是和国内不同步的,所以国内的交易老手经常会参考外盘行情,获取信息差的利润。然而,这些外盘数据,在国内平台进行获取和信号计算,是比较困难的。然而在TradingView上,涵盖基本所有国家的金融数据,可以实现实时的数据获取和信号计算。所以,我们可以在TradingView上建立一个外盘信息推送机制,可以让国内商品期货的实盘实时接受外盘的信息,进而进行一些策略的运行。

讲解完具体的需求场景,我们来看下具体的解决方案。整个方案中涉及4个主体,简单来说分别是:

编号 主体 描述
1 TradingView TradingView上运行着PINE脚本,可以发出信号,访问优宽的扩展API接口
2 优宽平台 管理实盘、可以在实盘页面发送交互指令、也可以通过扩展API接口让优宽平台发送交互指令给托管者上的实盘策略程序
3 托管者软件上的实盘程序 TradingView信号执行策略实际运行起来的程序
4 交易所 实盘上配置的交易所,托管者上的实盘程序直接发送请求下单的交易所

具体的实现步骤,我们大致介绍下:

  • TradingView上编写脚本进行运行,这个脚本进行交易数据获取和信号计算。
  • 在优宽上配置API KEY,填写到TradingView的webhook接口,用来给优宽发指标信号。需要注意的是,这里需要TradingView账号至少是PRO会员级别。
  • 实盘编写「TradingView信号执行策略」,用来接收TradingView的信号,对接真实的市场进行交易操作。

首先,我们来看怎样通过TradingView向优宽实盘发送消息?我们打开TradingView,这里我们挑选的品种是布伦特原油。布伦特原油是国际上最重要的原油定价基准之一,其价格波动会对全球原油市场产生广泛的影响。当布伦特原油价格变动较大时,国内原油期货市场可能会受到价格传导的影响,导致国内原油期货价格也出现相应的波动。所以这里我们建立一个测试策略,使用布伦特原油的交易信号去进行国内原油的交易操作。这个测试策略很简单,根据k线是否连续上涨或者下跌去判断做多还是做空的信号。这里我们定义两个参数,consecutiveBarsUp 和 consecutiveBarsDown初始值都为3,然后统计连续上涨ups,和连续下跌dns的数值。在非历史bar的情况下,当ups大于consecutiveBarsUp参数,定义action为long,然后使用strategy.order进行下单,这里我们将comment定义为action;对于连续下跌的情况也是一样,当dns小于consecutiveBarsDown,定义action为short。这里我们只是判断上涨还是下跌的趋势,然后将action信息推送到我们国内优宽原油期货交易实盘,根据交易的趋势进行相应的交易操作。我们来看下怎样设置推送?

点击左边的闹钟按钮,点击创建警报,这里我们可以选择很多的指标和策略的警报,这里我们选择刚创建好的策略,到期时间无限制,然后消息这里我们只返回strategy.order.comment,这里的comment就是定义的action信息。

{
    "Action":"{{strategy.order.comment}}",
}

然后第二栏这里设置webhook url,就是当警报触发时,向指定的url发送post的请求。我们来看下这里的webhook url怎样填写?我相信到这里大家一定有疑问,TradingView的信息是怎样传送的,传送的信息实盘怎样接受。关于第一个问题,TradingView其实是把消息发送给优宽的扩展API接口的。API接口我们在前面的课程中介绍过,那次课程我们使用扩展API实现了实盘的定时启停。这次我们要使用的扩展API接口是CommandRobot,来实现消息的接受。所以webhook的设定格式是这样的:

https://www.youquant.com/api/v1?access_key=xxx&secret_key=yyy&method=CommandRobot&args=[实盘ID,""]

我们来看下怎样填写:这里的ApiKey,也就是xxx和yyy,在账号设置Api接口这里获取。这里的CommandRobot是调用的方法,最后的参数args填写实盘的id,最后的空字符代表我们要发送的消息。

但是相信让大家自己拼凑填写有时候总会出现小小的失误,其实这些字符的拼接我们可以在实盘策略里面进行完成。我们来看下这个实盘策略应该怎样编写。

var BaseUrl = "https://www.youquant.com/api/v1"   // 优宽扩展API接口地址 
var RobotId = _G()         // 当前实盘ID
var Success = "#5cb85c"    // 成功颜色
var Danger = "#ff0000"     // 危险颜色
var Warning = "#f0ad4e"    // 警告颜色
var buffSignal = []        // 保存信息
var symbol = 'sc2311'      // 定义合约
var q = $.NewTaskQueue();

function createManager() {
    var self = {}

    self.tasks = []
    
    self.newTask = function(signal) {
        var task = {}
        task.Action = signal["Action"]
        task.error = null 
        task.finished = false 
        Log("创建任务:", task)
        self.tasks.push(task)
    }
    
    self.pollTask = function(task) {
        exchange.SetContractType(symbol)
        var pos = exchange.GetPosition() 

        if (task.Action == "long"){
            Log('当前仓位:', pos)
            if(pos.length == 0) {
                q.pushTask(exchange, symbol, "buy", 1) 
                Log('开多', Success)
            }else if(pos[0].Type == PD_SHORT || pos[0].Type == PD_SHORT_YD){
                q.pushTask(exchange, symbol, "closesell", 1) 
                Log('平空', Success)
            }else if(pos[0].Type == PD_LONG || pos[0].Type == PD_LONG_YD){
                Log('已有多头仓位', Warning)
            }
        }
        
        if (task.Action == "short"){
            if(pos.length == 0) {
                q.pushTask(exchange, symbol, "sell", 1) 
                Log('开空', Success)
            }else if(pos[0].Type == PD_LONG || pos[0].Type == PD_LONG_YD){
                q.pushTask(exchange, symbol, "closebuy", 1) 
                Log('平多', Success)
            }else if(pos[0].Type == PD_SHORT || pos[0].Type == PD_SHORT_YD){
                Log('已有空头仓位', Warning)
            }
        }

        q.poll()
        task.finished = true
    }

    self.process = function() {
        var processed = 0
        if (self.tasks.length > 0) {
            _.each(self.tasks, function(task) {
                if (!task.finished) {
                    processed++
                    self.pollTask(task)
                }
            })
            if (processed == 0) {
                self.tasks = []
            }
        }
    }
    
    return self
}

var manager = createManager()

function HandleCommand(signal) {
    // 检测是否收到交互指令
    if (signal && $.IsTrading(symbol)) { // 交易时间收到交互指令
        Log('是否正在交易:', $.IsTrading(symbol))
        Log("收到交互指令:", signal)     
    } else if (signal && !$.IsTrading(symbol)){  // 非交易时间收到交互指令
        Log("非交易时间") 
        return                            
    } else { // 没有收到时直接返回,不做处理
        return
    }

    objSignal = JSON.parse(signal)
    buffSignal.push(objSignal)
    
    // 创建任务
    manager.newTask(objSignal)
}

function main() {
    Log("WebHook地址:", "https://www.youquant.com/api/v1?access_key=" + 优宽_AccessKey + "&secret_key=" + 优宽_SecretKey + "&method=CommandRobot&args=[" + RobotId + ',+""]', Danger)
    
    while (true) {
        if(exchange.IO('status')){
            LogStatus("已经连接CTP, " + new Date())
            try {
                // 处理交互
                HandleCommand(GetCommand())
                
                // 处理任务
                manager.process()
                
                // 状态栏表格显示
                if (buffSignal.length > maxBuffSignalRowDisplay) {
                    buffSignal.shift()
                }
                var buffSignalTbl = {
                    "type" : "table",
                    "title" : "信号记录",
                    "cols" : ["Time", "Action"],
                    "rows" : []
                }
                for (var i = buffSignal.length - 1 ; i >= 0 ; i--) {
                    buffSignalTbl.rows.push([_D(), buffSignal[i].Action])
                }
                LogStatus(_D(), "\n", "`" + JSON.stringify(buffSignalTbl) + "`")
                Sleep(1000)
            } catch (error) {
                Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
                Sleep(1000)
            }
            
        }else{
            LogStatus('未连接CTP', _D())
        }
        Sleep(1000);
    }
}

在策略开头,我们定义BaseUrl,代表优宽扩展API接口地址,RobotId可以通过_G获取,这在前面恢复策略进度我们有提到过,接着定义三种颜色,分别代表成功,危险和警告的颜色,定义buffSignal保存获取到的信息,symbol是原油期货主力合约,q是交易类库多品种管理对象。

第一部分是我们来定义createManager,用来处理推送过来的signal对应的交易操作。具体来说,createManager函数创建了一个任务管理器对象。函数内部定义了一个self对象,该对象包含以下属性和方法:

  • tasks:任务数组,用于存储待处理的任务。
  • newTask方法:首先定义接收信号,创建任务的方法newTask。在该方法数中,接受一个信号对象作为参数。当参数从TradingView传达以后,创建一个新的task变量,然后定义任务的Action(操作)属性,error(错误信息)属性和finished(是否已完成)属性,并将定义好的task加入任务数组中。
  • pollTask方法:然后对给定的任务对象执行相应的交易操作,定义pollTask方法。根据任务的Action属性判断是买入还是卖出操作,然后根据持仓类型,调用q.pushTask方法添加相应的交易任务到交易队列中。使用pushTask不要忘记q.poll()。最后将任务的finished属性标记为true,表示任务已完成。
  • process方法:最后建立process方法用于处理任务队列中的任务。通过遍历任务数组,对未完成的任务调用pollTask方法进行处理。若所有任务都已完成,则将任务数组清空。

函数最后返回self对象。

这里通过调用createManager函数manager变量,这样可以通过manager变量来管理和执行任务队列。

因为国内外开盘时间的不一致,所以我们接受到的信息是不能直接使用的。HandleCommand函数的作用是接受和处理交互指令。它接受一个参数signal,该参数表示收到的交互指令。函数首先检测是否收到了有效的交互指令,并且当前处于交易时段。如果不处于交易中或者没有收到有效的交互指令,则直接返回,不进行任何处理。如果收到了有效的交互指令并且处于交易状态,函数将JSON解析交互指令,将其转换为对象objSignal。然后将objSignal添加到buffSignal数组中,表示记录交互指令。

接下来,函数调用manager.newTask(objSignal)方法创建一个新的任务。这样可以将交互指令转化为对应的任务,并添加到任务管理器的任务队列中,以便后续处理。

最后的main函数,在该函数中,会进行以下操作:

首先我们输出WebHook地址和交易类型信息,这里就要根据提供的参数信息完成webhook字段的拼接。

信号的接受和处理方法已经设置完成了,在进入主循环之前,我们需要设置几个参数:优宽_AccessKey和优宽_SecretKey,maxBuffSignalRowDisplay表示最大信号显示行数。

接着我们进入主循环:我们首先完成webhook url的拼接。这里的三个参数是优宽 的ApiKey的两部分,和实盘的ID。 在大致的框架列好以后,这里我们使用try和catch结构捕获任何异常错误,打印相关错误信息。 这里首先调用HandleCommand()函数获取并处理交互指令,TradingView传送的信息会发送到交互信息里面,使用GetCommand()就是获取交互命令字符串。

然后调用任务管理器manager的process()方法来处理任务队列中的任务。 当然我们也要记录信号,将收到的信号记录添加到buffSignalTbl状态栏表格进行显示,并根据设定的最大显示行数进行处理。

根据设定的休眠间隔时间,使用Sleep()函数暂停一段时间。

这样呢,我们就简单设置好了使用优宽接收TradingView传递的实时信息进行交易的策略,我们创建实盘运行一下。可以看到,首先返回webhook的信息,我们需要填写的TradingView的栏中。这时候TradingView才可以传递消息。当实盘接受到信号,打印收到交互任务,并创建任务,并根据现有的仓位进行相应的交易操作,如果是在非交易时间段接收到了任务,直接进行返回。这样我们就可以使用TradingView的信息实现跨平台的交易操作。

我们这里的策略设计比较简单,主要是为了教学的目的。其实TradingView上有很多新颖的指标和策略,大家都可以动手尝试下。如果大家有好的想法,想让我们帮忙实现,也欢迎留言评论区,我们将为大家热心解答!

视频参考链接: 《另一种TradingView信号执行策略方案》

10. 浅谈量化交易中的过拟合问题

你有没有听过这样的传闻,传说有一种特殊类型的策略,每年只需要少数的参数修改,就可以实现高达200%+的收益,今天就要为大家带来这个神奇的策略中的一种,EMV(简易波动策略)策略。

简易波动(Ease of Movement Value)反映的是价格、成交量、人气的变化,它是一种将价格与成交量变化相结合的技术,它通过衡量单位成交量的价格变动,形成一个价格波动指标。当市场人气聚集,交易活跃时提示买入信号;当成交量低迷,市场能量即将耗尽时提示卖出信号。

简易波动EMV根据等量图和压缩图的原理设计而成,它的核心理念是:市场价格仅在发生趋势转折或即将转折时,才会消耗大量能量,外在表现就是成交量变大。当价格在上升的过程中,由于推波助澜的作用,不会消耗太多的能量。虽然这个理念与量价同升的观点相悖,但的确有其独特的地方。

如果大家不相信的话,我们来使Javascript语言做一个试试看。

function main(){
    var emvList = []
    var isFirst = true
    var PreBarTime = 0
    var symbol = 'SA888'
    $.CTA(symbol, function(st) {
        var r = st.records
        if(isFirst){
            PreBarTime = r[r.length - 2].Time
            isFirst = false
        }else {
            if(PreBarTime != r[r.length - 2].Time){
                PreBarTime = r[r.length - 2].Time
                //指标计算 交易执行
                var A = (r[r.length - 2].High + r[r.length - 2].Low)/2
                var B = (r[r.length - 3].High + r[r.length - 3].Low)/2
                var C = r[r.length - 2].High - r[r.length - 2].Low
                var EMV = (A - B) * C / r[r.length - 2].Volume * 1000
                emvList.push(EMV)

                var smaEMV = TA.SMA(emvList, period)

                var buySignal = smaEMV[smaEMV.length - 2] < N1 && smaEMV[smaEMV.length - 1] > N1
                var sellSignal = smaEMV[smaEMV.length - 2] > N2 && smaEMV[smaEMV.length - 1] < N2

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

策略的原理基于EMV指标和其简单移动平均线的计算,定义了一个基本的交易策略:当EMV指标突破其买入阈值时,执行买入操作;当EMV指标跌破卖出阈值时,执行卖出操作。

这里面有三个参数,分别是EMV的窗口长度,买入信号EMA阈值和卖出信号EMA阈值。话说ChatGpt由1750亿个参数就可以模拟众生,今天我们就来试试调试这三个参数来实现年化200%+的梦想。

因为该策略是根据突破点位不断地进行开平仓的操作,所以这里我们选择使用CTA框架函数进行交易的操作,该函数接受一个参数 symbol 和一个回调函数,回调函数的参数是一个表示交易状态的对象 st,st包含了期货合约的综合数据,包括k线,账户和仓位等信息。

这里首先,我们定义三个变量,分别是一个空数组 emvList,用于存储计算出的 EMV指标;isFirst,代表是否是策略的起始状态;preBartime,表示先前K线的时间。

将变量 r 赋值为 st.records。因为我们EMV指标使用到了volume成交量这个数据,而只有当一根k线真正的走完,这一k线周期的成交量才算固定。所以这里我们建立了一个类似pine语言的收盘价模型。其实我们前面也用到过,这里我们正式介绍下它的名字。根据倒数第二根k线的时间戳是否更新,我们来确定是否进行交易指标的计算和具体交易操作的执行。

具体的来说,在策略起始状态,isFirst为真,将preBartime定义为倒数第二根k线的时间戳;然后伴随策略进度,判断最新的k线是否更新。在判断时间戳更新后,首先更新preBartime,然后:

第一步计算倒数第二根k线的最高价和最低价的平均值作为 A 值;第二步计算倒数第三根k线的最高价和最低价的平均值作为 B 值;然后使用计算倒数第二根k线的价格范围,也就是最高价减去最低价作为 C 值;最后根据公式计算 EMV 值,并将计算出的 EMV 值添加到 emvList 数组中。

利用技术分析库(TA)中的简单移动平均线内置函数(SMA),计算 emvList 数组的简单移动平均线值,存储在变量 smaEMV 中。这样呢,可以对噪音信号做了一些平滑。

信号计算完毕,接着就要根据策略判断买入和卖出信号:如果倒数第二个 smaEMV值小于 N1 但是最新的smaEMV值大于 N1,表示突破买入阈值,设置 buySignal 为真,发出买入信号;与此相反,如果倒数第二个移动平均线值大于 N2 并且最新的移动平均线值小于 N2,表示跌破卖出的阈值,那么设置 sellSignal 为真,发出卖出信号。

根据买入和卖出信号以及持仓状态进行决策,这里我们很熟悉了,根据CTA框架的特点,使用持仓量判断return的数值:多头信号下,持仓为空,return 1;如果持有空仓,首先平掉空仓,再开多仓,return 2。空头信号出现的时候,相反的逻辑处理就可以了。

这样我们的策略就定义完了,确实不复杂,下面我们来看策略的效果。在回测页面,我们设置原始的参数,均线周期为15,买入阈值和卖出阈值都设置为0,回测时间定为2022年完整的一年,跑一下看下结果。可以看到,使用1小时为策略周期,针对于纯碱主力合约,我们的策略收益为10252,而这仅仅是一手的收益。当然这并不是这个策略的极限,不知道调一下参数是否会有别的惊喜呢。

这里设置均线周期调参最小值5,最大值是30,步长是1;N1最小值是0,最大值是2,步长0.1;N2最小-2,最大0,步长一样。优宽平台支持多个参数同时调参,当然时间会稍微长一点,大家也可以按顺序分别调整参数。最后我们看下我们最优的参数结果,窗口周期为29,买入阈值为0.2,卖出阈值为-1.3,让我们看下效果。

年化收益变为21101,确实很神奇。简简单单三个参数的改变,就可以实现年化率百分之二百+,但是这个策略一直有效吗,让我们把时间换为今年的时间段,2023年1月到2023年9月间,可以看到收益变为了-4500。

人生的大起大落真是刺激,本以为找到了可以一本万利的无风险策略,结果却是黄粱一梦。看来“All models are wrong, but some are useful”是真实的。问题究竟出在哪里呢?

答案是过拟合。过拟合(overfitting)是在,在机器学习模型中,模型过于强调训练数据的细节和噪声,结果导致它在新的、未见过的数据上的泛化性能较差。也就是说,过拟合本质上是对训练数据集过度拟合,使得模型在测试数据上表现不佳。有没有觉得我们在第一次调参后的数据过于绝对了,买入阈值为0.2,卖出阈值为-1.3,这意味着这一年确实是多头主导的年份,小小的跌幅不用怕,只有达到-1.3巨额的跌幅才是真的应该转变空头方向。并且2022年的价格最低点是2162,最高点是3268,价格差距1000点。

在2023年,如果经常做期货的朋友都知道3月15号,纯碱开始大跌,一直从2800点跌到1500点左右,算是价格腰斩。也就是从那时开始,可以看到我们的策略收益开始由正转负,一致持续负收益到5月31号,这也是我们公认的2023年商品期货走势的拐点。从这个时候起,我们的策略收益也开始逐步回血,到9月份已经收回了13000元左右的回撤。

所以呢,过拟合并不是仅仅出现在使用过多参数的模型中,使用少量的参数也可以出现过拟合的现象。在量化交易中,过拟合是一个普遍存在的问题。有些策略即使在历史数据上表现得很好,但在实际市场上表现不佳,因为过度适应了历史数据的特点而失去了泛化能力。我相信这些比较理论性的解决方法,大家都听说过很多次。但是,最重要的还是,我们要去了解这个市场,这个世界唯一不变的就是一直在改变,想要一个永久万能的模型也是不太可能的,即使一个专业工业性的大模型,三到五个月的时间也会进行一次更新和淘汰,所以对于我们来说,需要通过实践和不断地试验,针对于当前市场和当前品种,找出局部的最优解已经很好了,大家加油。

视频参考链接: 《EMV指标策略》

11.商品期货Orderflow订单流策略(上)

在电子交易兴起之前,要想了解成交量是如何在K线上分配的是一件很难的事情。如今科技的发展给带给我们一种前所未有的市场分析方式,大部分软件都已经支持以Order Book方式向我们提供价格和成交量数据,用来了解价格上涨或下跌背后的原因。

在二级交易市场中,影响价格变化的因素是纷繁复杂的,并且每一个因素影响价格变化的权重都不一样,以至于很难从传统技术分析图形中推导出价格行为,因为相对于价格和成交量来说,技术分析图形相对抽象和滞后。而OrderFlow订单流工具的横空出世,使得市场更加通透。

OrderFlow订单流分类

OrderFlow订单流有很多种分类,包括:

成交明细(Sales Details):这个我们最为熟悉,它是期货市场中一种记录每笔交易细节的信息报告。它提供了每个合约的交易价格、交易量以及订单执行的时间戳等详细数据。通过成交明细,我们可以获得更细致的市场交易情绪和活动信息,有助于进一步分析市场的买卖力量、支撑阻力区域以及潜在的价格变动趋势。我们可以观察成交明细来确定市场的流动性、看涨或看跌力量,以及潜在的支撑和阻力水平。此外,成交明细还可以用于验证交易策略、识别潜在的市场信号,并帮助交易者做出更明智的交易决策。

image

市场深度数据:这个我们也比较熟悉。它是用来显示市场上委托买单和卖单的价格和数量信息的一种数据。


更多内容