Type/to search
8
Follow
1363
Followers
另一种TradingView信号执行策略方案
Discussions
Created 2022-11-30 10:52:07  Updated 2025-05-16 16:18:13
 17
 4887

img

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

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

场景和原理

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

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

2、原理:

img

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

编号主体描述
1TradingView(图中Trading View)TradingView上运行着PINE脚本,可以发出信号,访问FMZ的扩展API接口
2FMZ平台(图中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"],改用以下方式 https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=%5B186515%2C%22ok12345%22%5D

这个请求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,+""],改用以下方式 https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=%5B130350%2C+%22%22%5D

这样请求就可以通过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 + ',+""]' 改用以下方式: url = url + '?access_key=' + accessKey + '&secret_key=' + secretKey + '&method=CommandRobot&args=%5B' + robotId + '%2C+%22%22%5D' 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("WebHook地址:", "https://www.fmz.com/api/v1?access_key=" + FMZ_AccessKey + "&secret_key=" + FMZ_SecretKey + "&method=CommandRobot&args=%5B" + RobotId + '%2C+%22%22%5D', 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,+""],改用以下方式: https://www.fmz.com/api/v1?access_key=22903bab96b26584dc5a22522984df42&secret_key=73f8ba01014023117cbd30cb9d849bfc&method=CommandRobot&args=%5B505628%2C+%22%22%5D

直接复制粘贴写在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.zhihu.com/zvideo/1581722694294487040

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

Comment
All comments (17)

    img

    2 years ago

    删掉交互结构消息中的中文试下。

    2 years ago

    梦哥,这个运行起来有错误

    2 years ago

    把消息中的中文删掉试下。

    2 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

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

    3 years ago

    好的 感谢老板

    3 years ago
  • 1
iPhone Download
Forums
PINE Language
© 2015 - ∞ INVENTOR PTE LTD (SG)