Traders who often use TradingView know that TradingView can push messages to other platforms. In our “Digest” of FMZ platform, there was a TradingView signal push strategy published in the library, where the content of the pushed messages was written in the request url, which was somewhat inflexible. In this article, we re-design a TradingView signal execution strategy in a new way.
Some novices may be confused by the title of this article and the description above, it doesn’t matter! Let’s start with a clear description of the demand scenarios and principles. OK, let’s get into the topic here.
Demand scenarios: So what kind of work do we want it to do? To put it simply, we have a lot of indicators, strategies, codes, etc. that we can choose to use on TradingView, which can be run directly on TradingView to draw lines, calculate and display trading signals. In addition, TradingView has real-time price data and sufficient K-line data to facilitate the calculation of various indicators. These script codes on TradingView are called PINE language. The only thing not convenient is that the real bot trading on TradingView. Although the PINE language is supported on FMZ, it can also be used for real bot trading. However, there are some TradingView fans who still want to place orders using the signals from the charts on TradingView, so this need can be solved by FMZ. So in this article, we will explain the details of the solution.
Principle:
There are 4 subjects involved in the whole scheme, which are, in brief, the following:
So if you want to use it these ways, you need these preparations:
The design of the “TradingView Signal Execution Strategy” in the previous version is not very flexible. Messages can only be written to the url of the request sent by the TradingView. If we want TradingView to write some variable information in the Body when pushing messages, we can do nothing at this time. For example, such message content on the TradingView:
Then the TradingView can be set as shown in the figure to write the message in the request Body and send it to the extended API interface of FMZ. How to call the extended API interface of FMZ?
In a series of extended API interfaces of FMZ, we need to use the CommandRobot
interface, which is usually called as follows:
https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[186515,"ok12345"]
The access_key
and secret_key
in the query
of this request url is the extended API KEY
of FMZ platform, here the demo set to xxx
and yyyy
. Then how to create this KEY? In this page: https://www.fmz.com/m/account
, create on it, keep it properly, do not disclose it.
Back to the point, let’s continue to talk about the interface problem of CommandRobot
. If you need to access the CommandRobot
interface, the method
in the request will be set to: CommandRobot
. The function of the CommandRobot
interface is to send an interactive message to a real bot with an ID through the FMZ platform, so the parameter args
contains the real bot ID and message. The above request url example is to send the message ok12345
to a real bot program with an ID of 186515.
Previously, this method was used to request the CommandRobot interface of the FMZ extended API. Messages can only be written in the above example, such as the ok12345
. If the message is in the requested Body, you need to use another method:
https://www.fmz.com/api/v1?access_key=xxx&secret_key=yyyy&method=CommandRobot&args=[130350,+""]
In this way, the request can send the content of the Body in the request as an interactive message to the real bot with ID 130350
through the FMZ platform. If the message on the TradingView is set to: {"close": {{close}}, "name": "aaa"}
, then the real bot with the ID of 130350
will receive interactive instructions: {"close": 39773.75, "name": "aaa"}
In order for the “TradingView Signal Execution Strategy” to correctly understand the command sent by TradingView when receiving the interactive command, the following message formats should be agreed in advance:
{
Flag: "45M103Buy", // Marker, which can be specified at will
Exchange: 1, // Specify exchange trading pairs
Currency: "BTC_USDT", // Trading pair
ContractType: "swap", // Contract type, swap, quarter, next_quarter, fill in spot for spot
Price: "{{close}}", // Opening position or closing position price, -1 is the market price
Action: "buy", // Transaction type [buy: spot buying, sell: spot selling, long: go long futures, short: go short futures, closesell: buy futures and close short positions, close buy: sell futures and close long positions]
Amount: "0", // Transaction amount
}
The strategy is designed as a multi-exchange architecture, so multiple exchange objects can be configured on this strategy, that is, the order placing operation of multiple different accounts can be controlled. Only the Exchange in the signal structure specifies the exchange to be operated. Setting 1 is to enable this signal to operate the exchange account corresponding to the first added exchange object. If the spot ContractType is set to spot, the futures will write specific contracts, such as, swap for perpetual contracts. The market price list can pass in -1. Action settings are different for futures, spot, opening and closing positions, and it cannot be set incorrectly.
Next, you can design the strategy code. Complete strategy code:
//Signal structure
var Template = {
Flag: "45M103Buy", // Marker, which can be specified at will
Exchange: 1, // Specify exchange trading pairs
Currency: "BTC_USDT", // Trading pair
ContractType: "swap", // Contract type, swap, quarter, next_quarter, fill in spot for spot
Price: "{{close}}", // Opening position or closing position price, -1 is the market price
Action: "buy", // Transaction type [buy: spot buying, sell: spot selling, long: go long futures, short: go short futures, closesell: buy futures and close short positions, close buy: sell futures and close long positions]
Amount: "0", // Transaction amount
}
var BaseUrl = "https://www.fmz.com/api/v1" // FMZ extended API interface address
var RobotId = _G() // Current real bot ID
var Success = "#5cb85c" // Color for success
var Danger = "#ff0000" // Color for danger
var Warning = "#f0ad4e" // Color for alert
var buffSignal = []
// Check signal message format
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("The minimum number of the exchange is 1 and it is an integer", Danger)
return
}
if (Signal.Amount <= 0 || typeof(Signal.Amount) != "number") {
Log("The transaction amount cannot be less than 0 and it is numerical type", typeof(Signal.Amount), Danger)
return
}
if (typeof(Signal.Price) != "number") {
Log("Price must be a value", Danger)
return
}
if (Signal.ContractType == "spot" && Signal.Action != "buy" && Signal.Action != "sell") {
Log("The command is to operate spot, Action error, Action:", Signal.Action, Danger)
return
}
if (Signal.ContractType != "spot" && Signal.Action != "long" && Signal.Action != "short" && Signal.Action != "closesell" && Signal.Action != "closebuy") {
Log("The command is to operate future, Action error, 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("Simulate a webhook request from TradingView, sending a POST request for testing purposes:", url, "body:", cmd, "response:", 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("Create task:", 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) {
// Non-spot, then set the contract
e.SetContractType(task.ContractType)
} else if (task.ContractType == "spot" && name.indexOf("Futures_") == -1) {
isFutures = false
} else {
task.error = "The ContractType in the command does not match the configured exchange object type"
return
}
var depth = e.GetDepth()
if (!depth || !depth.Bids || !depth.Asks) {
task.error = "Order book data exception"
return
}
if (depth.Bids.length == 0 && depth.Asks.length == 0) {
task.error = "No orders on the market entry position"
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 = "Failed to obtain precision"
return
}
e.SetPrecision(self.pricePrecision, self.amountPrecision)
// buy: spot buying, sell: spot selling, long: go long futures, short: go short futures, closesell: buy futures and close short positions, close buy: sell futures and close long positions
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 = "Wrong transaction direction:" + 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 = "Wrong transaction direction:" + task.Action
return
}
}
var id = tradeFunc(task.Price, task.Amount)
if (!id) {
task.error = "Failed to place an order"
}
task.finished = true
}
return self
}
var manager = createManager()
function HandleCommand(signal) {
// Detect whether interactive command is received
if (signal) {
Log("Receive interactive command:", signal) // Receive the interactive command, print the interactive command
} else {
return // If it is not received, it will be returned directly without processing
}
// Check whether the interactive command is a test instruction. The test instruction can be sent out by the current strategy interaction control for testing
if (signal.indexOf("TestSignal") != -1) {
signal = signal.replace("TestSignal:", "")
// Call the FMZ extended API interface to simulate the webhook of the TradingView, and the message sent by the interactive button 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("Execute debug code:", js)
eval(js)
} else {
// Process signal command
objSignal = JSON.parse(signal)
if (DiffObject(Template, objSignal)) {
Log("Received transaction signal command:", objSignal)
buffSignal.push(objSignal)
// Check the trading volume and exchange number
if (!CheckSignal(objSignal)) {
return
}
// Create task
manager.newTask(objSignal)
} else {
Log("Command cannot be recognized", signal)
}
}
}
function main() {
Log("WebHook address:", "https://www.fmz.com/api/v1?access_key=" + FMZ_AccessKey + "&secret_key=" + FMZ_SecretKey + "&method=CommandRobot&args=[" + RobotId + ',+""]', Danger)
Log("Transaction type [buy: spot buying, sell: spot selling, long: go long futures, short: go short futures, closesell: buy futures and close short positions, close buy: sell futures and close long positions]", Danger)
Log("Command template:", JSON.stringify(Template), Danger)
while (true) {
try {
// Process interactions
HandleCommand(GetCommand())
// Process tasks
manager.process()
if (buffSignal.length > maxBuffSignalRowDisplay) {
buffSignal.shift()
}
var buffSignalTbl = {
"type" : "table",
"title" : "Signal recording",
"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)
}
}
}
Strategy parameters and interactions:
The complete strategy address of the “Trading View Signal Execution Strategy”: https://www.fmz.com/strategy/392048
Before running the strategy, the exchange object should be configured, and the two parameters “AccessKey on FMZ Platform” and “SecretKey on FMZ Platform” should be set in the strategy parameters. When running, it will show:
It will print out the WebHook address, supported Action commands, and message format that need to be filled in on the TradingView. The important thing is the WebHook address:
https://www.fmz.com/api/v1?access_key=22903bab96b26584dc5a22522984df42&secret_key=73f8ba01014023117cbd30cb9d849bfc&method=CommandRobot&args=[505628,+""]
Just copy and paste it directly to the corresponding location on the TradingView.
If you want to simulate a signal sent by the TradingView, you can click on the TestSignal button on the strategy interaction.
This strategy sends a request of its own (simulating a TradingView sending a signal request), calling FMZ’s extended API interface to send a message to the strategy itself:
{"Flag":"45M103Buy","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"16000","Action":"buy","Amount":"1"}
The current strategy will receive another interactive message and execute, and place an order for transaction.
Using the TradingView test requires that the TradingView account is at the Pro level. Before the test, you need to konw some pre knowledge.
Take a simple PINE script (randomly found and modified on the TradingView) as an example
//@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)
The following are placeholders. For example, if I write {{strategy.order.contracts}}
in the “message” box of the alert, a message will be sent when the order is triggered (according to the settings on the alert, mail push, webhook url request, pop-up, etc.), and the message will contain the number of orders executed this time.
{{strategy.position_size}}
- Return the value of the same keyword in Pine, i.e. the size of the current position.
{{strategy.order.action}}
- Return the string “buy” or “sell” for the executed order.
{{strategy.order.contracts}}
- Return the number of contracts for which orders have been executed.
{{strategy.order.price}}
- Return the price of the executed order.
{{strategy.order.id}}
- Return the ID of the executed order (the string used as the first parameter in one of the function calls that generate the order: strategy.entry, strategy.exit or strategy.order).
{{strategy.order.comment}}
- Return the comment of the executed order (the string used in the comment parameter in one of the function calls that generate the order: strategy.entry, strategy.exit, or strategy.order). If no comment is specified, the value of strategy.order.id will be used.
{{strategy.order.alert_message}}
- Return the value of the alert_message parameter that can be used in the strategy’s Pine code when calling one of the functions used to place an order: strategy.entry, strategy.exit, or strategy.order. This is only supported in Pine v4.
{{strategy.market_position}}
- Return the current position of the strategy as a string: “long”, “flat”, or “short”.
{{strategy.market_position_size}}
- Returns the size of the current position in the form of an absolute value (that is, a non negative number).
{{strategy.prev_market_position}}
- Return the previous position of the strategy as a string: “long”, “flat”, or “short”.
{{strategy.prev_market_position_size}}
- Returns the size of the previous position in the form of an absolute value (that is, a non negative number).
{
"Flag":"{{strategy.order.id}}",
"Exchange":1,
"Currency":"BTC_USDT",
"ContractType":"swap",
"Price":"-1",
"Action":"{{strategy.order.comment}}",
"Amount":"{{strategy.order.contracts}}"
}
When the PINE script on the TradingView triggers a transaction, a webhook url request will be sent.
The FMZ real bot will execute this signal.
The code in this article is for reference only, and it can be adjusted and expanded by yourself in actual use.