[TOC]
经常使用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发送的请求的url中。假如我们希望TradingView推送消息时在Body中写一些变量信息,这个时候就无能为力了。例如在TradingView上这样的消息内容:
那么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
,创建一个就可以,妥善保管,切勿泄露。
回归正题,继续说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信号执行策略」完整策略地址:https://www.fmz.com/strategy/392048
策略运行前要配置好交易所对象,在策略参数中设置好「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账号是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上加载这个脚本时设置报警
当TradingView上的PINE脚本触发交易动作,就会发送webhook url请求。
FMZ的实盘就会执行这个信号。
西瓜视频:https://www.ixigua.com/7172134169580372513?utm_source=xiguastudio B站:https://www.bilibili.com/video/BV1BY411d7c6/ 知乎:https://www.zhihu.com/zvideo/1581722694294487040
文章中的代码仅供参考,实际使用可以自行调整、扩展。
q102133 /upload/asset/1979ae34cdbe7673f64b5.jpg
q102133 梦哥,这个运行起来有错误
N95 梦哥,如何运行模拟盘环境操作呢?想先测试一下信号准确率
guohwa 请教一个问题,tradingview的警报消息能包含上一个订单的消息吗? 我想获取上一个订单是盈利还是亏损,如果上一个订单是亏损则机器人不执行下单操作,直到获取的上一个订单是盈利状态才执行下单操作 请问能做到吗?感谢!
13811047519 /upload/asset/2a5a9fa2b97561c42c027.jpg请问大神,这个报错是什么意思呢,怎么消除
佳境 梦大,我加了6 7个账号用这个做信号交易,但是暂时挺大,一个交易所账号信号完成才会进行下个交易账号的信号,是串行的执行,有办法让同时并行执行交易信号吗? 我看里面有个间隔时间的配置,不知道改成0秒,是不是就能实现并行交易了?
发明者量化-小小梦 删掉交互结构消息中的中文试下。
发明者量化-小小梦 把消息中的中文删掉试下。
N95 接收信号的策略里,好像没有打印收益,公开好像也不会生成,所以想请教一下有相关的账户信息表格模版添加查看策略表现吗
发明者量化-小小梦 那个是公开策略围观,页面自动增加的。
N95 谢谢梦哥,已经测试好了,但是交易后没有策略评分概览,是不是需要自主添加
发明者量化-小小梦 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新增了并发功能,应该是可以改成并发的,不过策略代码可能改动会比较大。最近如果有时间,升级一个并发的例子。