经常使用TradingView的交易者都知道,TradingView可以推送消息到其它平台。之前在文库中也公开了一篇TradingView的信号推送策略,推送的消息内容是写死在请求url中的,有些不灵活。本篇我们重新用一种方式来设计一个TradingView信号执行策略。
可能有新手同学看到本篇文章题目和上面的描述有些懵,没关系!我们先把需求场景、原理阐述清楚。让您知道我在讲什么。OK,我们这就切入主题。
1、需求场景:
说了半天是要这个东西做什么工作呢?简单说就是我们在TradingView上有很多可以选择使用的指标、策略、代码等,这些都可以在TradingView上直接运行,可以画线、计算、显示交易信号等。并且TradingView有实时的价格数据、充足的K线数据方便各种指标计算。TradingView上这些脚本代码叫做PINE语言,唯独一点不太方便的就是在TradingView上实盘交易。虽然在FMZ上已经支持PINE语言,也可以实盘运行。但是也有TradingView的铁粉还是希望以TradingView上的图表发出的信号去下单交易,那么这个需求也可以通过FMZ来解决。所以本篇就是讲解这种解决方案的具体细节。
2、原理:

简单的流程
整个方案中涉及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上这样的消息内容:

trading view上设置消息的界面
那么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_key和secret_key就是FMZ平台的扩展API KEY,这里演示所以设置为xxx和yyyy。那这个KEY怎么创建呢?在这个页面:
https://www.fmz.com/m/account,创建一个就可以,妥善保管,切勿泄露。

创建FMZ平台的API KEY
回归正题,继续说CommandRobot接口的问题。如果需要访问的是CommandRobot接口,请求中的method就设置为:CommandRobot。CommandRobot这个接口的功能就是通过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)
}
}
}
策略参数和交互:

参数和交互设计
「TradingView信号执行策略」完整策略可以在FMZ平台策略广场搜索到。
简单测试
策略运行前要配置好交易所对象,在策略参数中设置好「FMZ平台的AccessKey」、「FMZ平台的SecretKey」这两个参数,不要设置错。运行起来显示:

初始运行时输出的日志信息
会依次打印出:在TradingView上需要填写的WebHook地址、支持的Action指令、消息格式。重要的是WebHook地址:
https://www.fmz.com/api/v1?access_key=22903bab96b26584dc5a22522984df42&secret_key=73f8ba01014023117cbd30cb9d849bfc&method=CommandRobot&args=[505628,+""]
直接复制粘贴写在TradingView上对应位置就可以。
如果想模拟TradingView发送信号,可以点击策略交互上的TestSignal按钮:

交互按钮
这个策略会自己发送一个请求(模拟TradingView发送信号请求),调用FMZ的扩展API接口,给策略自己发送一个消息:
{"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"16000","Action":"buy","Amount":"1"}
当前策略就会收到另一个交互消息,并且执行:

交易操作
并且下单交易。
实际场景中使用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上加载这个脚本时设置报警

trading view上设置webhook url警报
当TradingView上的PINE脚本触发交易动作,就会发送webhook url请求。

trading view图表上信号触发

trading view就会发出警报消息
FMZ的实盘就会执行这个信号。

执行对应操作

某cex上的订单记录
视频地址
西瓜视频:
https://www.ixigua.com/7172134169580372513?utm_source=xiguastudio
发布者:股市刺客,转载请注明出处:https://www.95sca.cn/archives/77166
站内所有文章皆来自网络转载或读者投稿,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!