另一种TradingView信号执行策略方案

Author: 小小梦, Created: 2022-11-30 10:52:07, Updated: 2023-09-18 20:01:09

[TOC]

img

另一种TradingView信号执行策略方案

经常使用TradingView的交易者都知道,TradingView可以推送消息到其它平台。之前在文库中也公开了一篇TradingView的信号推送策略,推送的消息内容是写死在请求url中的,有些不灵活。本篇我们重新用一种方式来设计一个TradingView信号执行策略。

场景和原理

可能有新手同学看到本篇文章题目和上面的描述有些懵,没关系!我们先把需求场景、原理阐述清楚。让您知道我在讲什么。OK,我们这就切入主题。

1、需求场景: 说了半天是要这个东西做什么工作呢?简单说就是我们在TradingView上有很多可以选择使用的指标、策略、代码等,这些都可以在TradingView上直接运行,可以画线、计算、显示交易信号等。并且TradingView有实时的价格数据、充足的K线数据方便各种指标计算。TradingView上这些脚本代码叫做PINE语言,唯独一点不太方便的就是在TradingView上实盘交易。虽然在FMZ上已经支持PINE语言,也可以实盘运行。但是也有TradingView的铁粉还是希望以TradingView上的图表发出的信号去下单交易,那么这个需求也可以通过FMZ来解决。所以本篇就是讲解这种解决方案的具体细节。

2、原理:

img

整个方案中涉及4个主体,简单来说分别是:

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

所以如果想这么玩就需要这几个准备: 1、TradingView上运行的脚本,负责发送信号请求到FMZ的扩展API接口,需要TradingView账号至少是PRO会员。 2、在FMZ上部署一个托管者程序,需要是可以访问到交易所接口的那种(例如新加坡、日本、香港等地的服务器)。 3、在FMZ上配置当TradingView信号发送过来时,要(下单)操作的交易所的API KEY。 4、你需要有个「TradingView信号执行策略」,这个策略就是本篇主要讲的。

TradingView信号执行策略

上一个版本的「TradingView信号执行策略」设计不太灵活,消息只能写死在TradingView发送的请求的url中。假如我们希望TradingView推送消息时在Body中写一些变量信息,这个时候就无能为力了。例如在TradingView上这样的消息内容:

img

那么TradingView上是可以如图中设置这样,把消息写在请求的Body中发送给FMZ的扩展API接口。那FMZ的这个扩展API接口如何调用呢?

FMZ的一系列扩展API接口中,我们要用到的是CommandRobot这个接口,通常是这样调用这个接口:

https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[186515,"ok12345"]

这个请求url的query中的access_keysecret_key就是FMZ平台的扩展API KEY,这里演示所以设置为xxxyyyy。那这个KEY怎么创建呢?在这个页面:https://www.fmz.com/m/account,创建一个就可以,妥善保管,切勿泄露。

img

回归正题,继续说CommandRobot接口的问题。如果需要访问的是CommandRobot接口,请求中的method就设置为:CommandRobotCommandRobot这个接口的功能就是通过FMZ平台向某个ID的实盘发送一个交互消息,所以参数args中包含的就是实盘ID和消息,上面这个请求url例子就是向ID为186515的实盘程序,发送消息ok12345

之前是用这种方式请求FMZ扩展API的CommandRobot接口,消息只能写死例如上面例子中的ok12345。如果消息在请求的Body中,就需要用另一种方式:

https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[130350,+""]

这样请求就可以通过FMZ平台,发送请求中Body的内容作为交互消息给ID为130350的实盘了。如果TradingView上的消息设置为:{"close": {{close}}, "name": "aaa"},那么ID为130350的实盘就会收到交互指令:{"close": 39773.75, "name": "aaa"}

为了让「TradingView信号执行策略」收到交互指令时能正确理解TradingView发送的这个指令,要提前约定一下消息格式:

{
    Flag: "45M103Buy",     // 标识,可随意指定
    Exchange: 1,           // 指定交易所交易对
    Currency: "BTC_USDT",  // 交易对
    ContractType: "swap",  // 合约类型,swap,quarter,next_quarter,现货填写spot
    Price: "{{close}}",    // 开仓或者平仓价格,-1为市价
    Action: "buy",         // 交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]
    Amount: "0",           // 交易量
}

策略设计成了多交易所架构,所以可以在这个策略上配置多个交易所对象,也就是可以控制多个不同账户的下单操作。只用在信号结构中Exchange指定要操作的交易所即可,设置1就是要让这个信号操作第一个添加的交易所对象对应的交易所账户。如果要操作的是现货ContractType设置为spot,期货就写具体合约,例如永续合约写swap。市价单价格传-1就可以了。Action设置对于期货、现货、开仓、平仓都是有区别的,不能设置错。

接下来就可以设计策略代码了,完整的策略代码:

//信号结构
var Template = {
    Flag: "45M103Buy",     // 标识,可随意指定
    Exchange: 1,           // 指定交易所交易对
    Currency: "BTC_USDT",  // 交易对
    ContractType: "swap",  // 合约类型,swap,quarter,next_quarter,现货填写spot
    Price: "{{close}}",    // 开仓或者平仓价格,-1为市价
    Action: "buy",         // 交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]
    Amount: "0",           // 交易量
}

var BaseUrl = "https://www.fmz.com/api/v1"   // FMZ扩展API接口地址 
var RobotId = _G()                           // 当前实盘ID
var Success = "#5cb85c"    // 成功颜色
var Danger = "#ff0000"     // 危险颜色
var Warning = "#f0ad4e"    // 警告颜色
var buffSignal = []

// 校验信号消息格式
function DiffObject(object1, object2) {
    const keys1 = Object.keys(object1)
    const keys2 = Object.keys(object2)
    if (keys1.length !== keys2.length) {
        return false
    }
    for (let i = 0; i < keys1.length; i++) {
        if (keys1[i] !== keys2[i]) {
            return false
        }
    }
    return true
}

function CheckSignal(Signal) {
    Signal.Price = parseFloat(Signal.Price)
    Signal.Amount = parseFloat(Signal.Amount)
    if (Signal.Exchange <= 0 || !Number.isInteger(Signal.Exchange)) {
        Log("交易所最小编号为1,并且为整数", Danger)
        return
    }
    if (Signal.Amount <= 0 || typeof(Signal.Amount) != "number") {
        Log("交易量不能小于0,并且为数值类型", typeof(Signal.Amount), Danger)
        return
    }
    if (typeof(Signal.Price) != "number") {
        Log("价格必须是数值", Danger)
        return
    }
    if (Signal.ContractType == "spot" && Signal.Action != "buy" && Signal.Action != "sell") {
        Log("指令为操作现货,Action错误,Action:", Signal.Action, Danger)
        return 
    }
    if (Signal.ContractType != "spot" && Signal.Action != "long" && Signal.Action != "short" && Signal.Action != "closesell" && Signal.Action != "closebuy") {
        Log("指令为操作期货,Action错误,Action:", Signal.Action, Danger)
        return 
    }
    return true
}

function commandRobot(url, accessKey, secretKey, robotId, cmd) {
    // https://www.fmz.com/api/v1?access_key=xxx&secret_key=xxx&method=CommandRobot&args=[xxx,+""]
    url = url + '?access_key=' + accessKey + '&secret_key=' + secretKey + '&method=CommandRobot&args=[' + robotId + ',+""]'
    var postData = {
        method:'POST', 
        data:cmd
    }
    var headers = "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36\nContent-Type: application/json"
    var ret = HttpQuery(url, postData, "", headers)
    Log("模拟TradingView的webhook请求,发送用于测试的POST请求:", url, "body:", cmd, "应答:", ret)
}

function createManager() {
    var self = {}
    self.tasks = []
    
    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 = []
            }
        }
    }
    
    self.newTask = function(signal) {
        // {"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"10000","Action":"buy","Amount":"0"}
        var task = {}
        task.Flag = signal["Flag"]
        task.Exchange = signal["Exchange"]
        task.Currency = signal["Currency"]
        task.ContractType = signal["ContractType"]
        task.Price = signal["Price"]
        task.Action = signal["Action"]
        task.Amount = signal["Amount"]
        task.exchangeIdx = signal["Exchange"] - 1
        task.pricePrecision = null
        task.amountPrecision = null 
        task.error = null 
        task.exchangeLabel = exchanges[task.exchangeIdx].GetLabel()
        task.finished = false 
        
        Log("创建任务:", task)
        self.tasks.push(task)
    }
    
    self.getPrecision = function(n) {
        var precision = null 
        var arr = n.toString().split(".")
        if (arr.length == 1) {
            precision = 0
        } else if (arr.length == 2) {
            precision = arr[1].length
        } 
        return precision
    }
    
    self.pollTask = function(task) {
        var e = exchanges[task.exchangeIdx]
        var name = e.GetName()
        var isFutures = true
        e.SetCurrency(task.Currency)
        if (task.ContractType != "spot" && name.indexOf("Futures_") != -1) {
            // 非现货,则设置合约
            e.SetContractType(task.ContractType)
        } else if (task.ContractType == "spot" && name.indexOf("Futures_") == -1) {
            isFutures = false 
        } else {
            task.error = "指令中的ContractType与配置的交易所对象类型不匹配"
            return 
        }
        
        var depth = e.GetDepth()
        if (!depth || !depth.Bids || !depth.Asks) {
            task.error = "订单薄数据异常"
            return 
        }
        
        if (depth.Bids.length == 0 && depth.Asks.length == 0) {
            task.error = "盘口无订单"
            return 
        }
        
        _.each([depth.Bids, depth.Asks], function(arr) {
            _.each(arr, function(order) {
                var pricePrecision = self.getPrecision(order.Price)
                var amountPrecision = self.getPrecision(order.Amount)
                if (Number.isInteger(pricePrecision) && !Number.isInteger(self.pricePrecision)) {
                    self.pricePrecision = pricePrecision
                } else if (Number.isInteger(self.pricePrecision) && Number.isInteger(pricePrecision) && pricePrecision > self.pricePrecision) {
                    self.pricePrecision = pricePrecision
                }
                if (Number.isInteger(amountPrecision) && !Number.isInteger(self.amountPrecision)) {
                    self.amountPrecision = amountPrecision
                } else if (Number.isInteger(self.amountPrecision) && Number.isInteger(amountPrecision) && amountPrecision > self.amountPrecision) {
                    self.amountPrecision = amountPrecision
                }
            })
        })

        if (!Number.isInteger(self.pricePrecision) || !Number.isInteger(self.amountPrecision)) {
            task.err = "获取精度失败"
            return 
        }
        
        e.SetPrecision(self.pricePrecision, self.amountPrecision)
        
        // buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多
        var direction = null 
        var tradeFunc = null 
        if (isFutures) {
            switch (task.Action) {
                case "long": 
                    direction = "buy"
                    tradeFunc = e.Buy 
                    break
                case "short": 
                    direction = "sell"
                    tradeFunc = e.Sell
                    break
                case "closesell": 
                    direction = "closesell"
                    tradeFunc = e.Buy 
                    break
                case "closebuy": 
                    direction = "closebuy"
                    tradeFunc = e.Sell
                    break
            }
            if (!direction || !tradeFunc) {
                task.error = "交易方向错误:" + task.Action
                return 
            }
            e.SetDirection(direction)
        } else {
            if (task.Action == "buy") {
                tradeFunc = e.Buy 
            } else if (task.Action == "sell") {
                tradeFunc = e.Sell 
            } else {
                task.error = "交易方向错误:" + task.Action
                return 
            }
        }
        var id = tradeFunc(task.Price, task.Amount)
        if (!id) {
            task.error = "下单失败"
        }
        
        task.finished = true
    }
    
    return self
}

var manager = createManager()
function HandleCommand(signal) {
    // 检测是否收到交互指令
    if (signal) {
        Log("收到交互指令:", signal)     // 收到交互指令,打印交互指令
    } else {
        return                            // 没有收到时直接返回,不做处理
    }
    
    // 检测交互指令是否是测试指令,测试指令可以由当前策略交互控件发出来进行测试
    if (signal.indexOf("TestSignal") != -1) {
        signal = signal.replace("TestSignal:", "")
        // 调用FMZ扩展API接口,模拟Trading View的webhook,交互按钮TestSignal发送的消息:{"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"10000","Action":"buy","Amount":"0"}
        commandRobot(BaseUrl, FMZ_AccessKey, FMZ_SecretKey, RobotId, signal)
    } else if (signal.indexOf("evalCode") != -1) {
        var js = signal.split(':', 2)[1]
        Log("执行调试代码:", js)
        eval(js)
    } else {
        // 处理信号指令
        objSignal = JSON.parse(signal)
        if (DiffObject(Template, objSignal)) {
            Log("接收到交易信号指令:", objSignal)
            buffSignal.push(objSignal)
            
            // 检查交易量、交易所编号
            if (!CheckSignal(objSignal)) {
                return
            }
            
            // 创建任务
            manager.newTask(objSignal)
        } else {
            Log("指令无法识别", signal)
        }
    }
}

function main() {
    Log("WebHook地址:", "https://www.fmz.com/api/v1?access_key=" + FMZ_AccessKey + "&secret_key=" + FMZ_SecretKey + "&method=CommandRobot&args=[" + RobotId + ',+""]', Danger)
    Log("交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]", Danger)
    Log("指令模板:", JSON.stringify(Template), Danger)
    
    while (true) {
        try {
            // 处理交互
            HandleCommand(GetCommand())
            
            // 处理任务
            manager.process()
            
            if (buffSignal.length > maxBuffSignalRowDisplay) {
                buffSignal.shift()
            }
            var buffSignalTbl = {
                "type" : "table",
                "title" : "信号记录",
                "cols" : ["Flag", "Exchange", "Currency", "ContractType", "Price", "Action", "Amount"],
                "rows" : []
            }
            for (var i = buffSignal.length - 1 ; i >= 0 ; i--) {
                buffSignalTbl.rows.push([buffSignal[i].Flag, buffSignal[i].Exchange, buffSignal[i].Currency, buffSignal[i].ContractType, buffSignal[i].Price, buffSignal[i].Action, buffSignal[i].Amount])
            }
            LogStatus(_D(), "\n", "`" + JSON.stringify(buffSignalTbl) + "`")
            Sleep(1000 * SleepInterval)
        } catch (error) {
            Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
            Sleep(1000 * 10)
        }
    }
}

策略参数和交互:

img

「TradingView信号执行策略」完整策略地址:https://www.fmz.com/strategy/392048

简单测试

策略运行前要配置好交易所对象,在策略参数中设置好「FMZ平台的AccessKey」、「FMZ平台的SecretKey」这两个参数,不要设置错。运行起来显示:

img

会依次打印出:在TradingView上需要填写的WebHook地址、支持的Action指令、消息格式。重要的是WebHook地址:

https://www.fmz.com/api/v1?access_key=22903bab96b26584dc5a22522984df42&secret_key=73f8ba01014023117cbd30cb9d849bfc&method=CommandRobot&args=[505628,+""]

直接复制粘贴写在TradingView上对应位置就可以。

如果想模拟TradingView发送信号,可以点击策略交互上的TestSignal按钮:

img

这个策略会自己发送一个请求(模拟TradingView发送信号请求),调用FMZ的扩展API接口,给策略自己发送一个消息:

{"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"16000","Action":"buy","Amount":"1"}

当前策略就会收到另一个交互消息,并且执行:

img

并且下单交易。

实际场景中使用TradingView的测试

使用TradingView测试需要TradingView账号是Pro级别,测试之前有一些前置小知识需要简单讲解一下。

以一个简单的PINE脚本(TradingView上随便找的修改了一下)为例子

//@version=5
strategy("Consecutive Up/Down Strategy", overlay=true)
consecutiveBarsUp = input(3)
consecutiveBarsDown = input(3)
price = close
ups = 0.0
ups := price > price[1] ? nz(ups[1]) + 1 : 0
dns = 0.0
dns := price < price[1] ? nz(dns[1]) + 1 : 0
if (not barstate.ishistory and ups >= consecutiveBarsUp and strategy.position_size <= 0)
    action = strategy.position_size < 0 ? "closesell" : "long"
    strategy.order("ConsUpLE", strategy.long, 1, comment=action)
if (not barstate.ishistory and dns >= consecutiveBarsDown and strategy.position_size >= 0)
    action = strategy.position_size > 0 ? "closebuy" : "short"
    strategy.order("ConsDnSE", strategy.short, 1, comment=action)

1、PINE脚本可以在脚本发出下单指令时附带一些信息

以下这些是占位符,例如我在报警中「消息」框中写入{{strategy.order.contracts}},那么在触发下单时就会发送消息(根据报警上的设置,邮件推送、webhook url请求、弹窗等),消息中就会包含这次执行订单的数量。

{{strategy.position_size}} - 返回Pine中相同关键字的值,即当前仓位的大小。 {{strategy.order.action}} - 为执行的订单返回字符串“buy”或“sell”。 {{strategy.order.contracts}} - 返回已执行订单的合约数量。 {{strategy.order.price}} - 返回执行订单的价格。 {{strategy.order.id}} - 返回已执行订单的ID(在生成订单的函数调用之一中用作第一个参数的字符串:strategy.entry,strategy.exit或strategy.order)。 {{strategy.order.comment}} - 返回已执行订单的注释(在生成订单的函数调用之一中的comment参数中使用的字符串:strategy.entry、strategy.exit、或strategy.order)。如果未指定注释,则将使用strategy.order.id的值。 {{strategy.order.alert_message}} - 返回alert_message参数的值,该参数可以在调用用于下订单的函数之一时在策略的Pine代码中使用:strategy.entry、strategy.exit、或strategy.order。仅在Pine v4中支持此功能。 {{strategy.market_position}} - 以字符串形式返回策略的当前持仓:“long”、“flat”、或 “short”。 {{strategy.market_position_size}} - 以绝对值(即非负数)的形式返回当前仓位的大小。 {{strategy.prev_market_position}} - 以字符串形式返回策略的上一个持仓:“long”、“flat”、或 “short”。 {{strategy.prev_market_position_size}} - 以绝对值(即非负数)的形式返回前一个仓位的大小。

2、结合「TradingView信号执行策略」构造消息

{
    "Flag":"{{strategy.order.id}}",
    "Exchange":1,
    "Currency":"BTC_USDT",
    "ContractType":"swap",
    "Price":"-1",
    "Action":"{{strategy.order.comment}}",
    "Amount":"{{strategy.order.contracts}}"
}

3、让TradingView根据这个PINE脚本运行时发出信号,需要在TradingView上加载这个脚本时设置报警

img

当TradingView上的PINE脚本触发交易动作,就会发送webhook url请求。

img

img

FMZ的实盘就会执行这个信号。

img

img

视频地址

西瓜视频:https://www.ixigua.com/7172134169580372513?utm_source=xiguastudio B站:https://www.bilibili.com/video/BV1BY411d7c6/ 知乎:https://www.zhihu.com/zvideo/1581722694294487040

文章中的代码仅供参考,实际使用可以自行调整、扩展。


Related

More

wbe3-小薯条🍟 梦哥,如何运行模拟盘环境操作呢?想先测试一下信号准确率

guohwa 请教一个问题,tradingview的警报消息能包含上一个订单的消息吗? 我想获取上一个订单是盈利还是亏损,如果上一个订单是亏损则机器人不执行下单操作,直到获取的上一个订单是盈利状态才执行下单操作 请问能做到吗?感谢!

13811047519 /upload/asset/2a5a9fa2b97561c42c027.jpg请问大神,这个报错是什么意思呢,怎么消除

佳境 梦大,我加了6 7个账号用这个做信号交易,但是暂时挺大,一个交易所账号信号完成才会进行下个交易账号的信号,是串行的执行,有办法让同时并行执行交易信号吗? 我看里面有个间隔时间的配置,不知道改成0秒,是不是就能实现并行交易了?

wbe3-小薯条🍟 接收信号的策略里,好像没有打印收益,公开好像也不会生成,所以想请教一下有相关的账户信息表格模版添加查看策略表现吗

小小梦 那个是公开策略围观,页面自动增加的。

wbe3-小薯条🍟 谢谢梦哥,已经测试好了,但是交易后没有策略评分概览,是不是需要自主添加

小小梦 OKX接口,可以切换至OKX的模拟盘测试环境,使用exchange.IO("simulate", true),即可切换为模拟盘环境。

guohwa 感谢回复,我有两个问题需要请教: 1、我有点没明白的是fmz自己本身就能编写pine脚本,为什么本文还要通过TradingView发送警报到fmz然后再处理然后交易? 2、我现在找到一个本身就很不错的策略,不过没有源码有使用权,我想通过我上面说的方法规避连错,您说的在推送消息里增加{{strategy.order.price}} 我也添加了,但是这个推送的貌似是下单时的价格,后面在fmz里面如何通过这个价钱来判断上一单是盈利还是亏损,我有点不明白。您这边要是愿意帮忙调试,我可以付费,我的邮箱是guohwa@qq.com

小小梦 应该可以实现,你可以在推送消息的时候推送{{strategy.order.price}} 内容,然后FMZ上的策略处理这个信息,根据当前价格对比,是否决定下单。

小小梦 现在测试正常么?我这里测试正常的。

佳境 好的 感谢老板

小小梦 FMZ新增了并发功能,应该是可以改成并发的,不过策略代码可能改动会比较大。最近如果有时间,升级一个并发的例子。