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.

Scenarios and Principles

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.

  1. 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.
  2. 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:

  1. The script running on the TradingView is responsible for sending signal requests to the FMZ's extended API interface. The TradingView account must be a PRO member at least.
  2. To deploy a docker program on FMZ, it needs to be the kind that can access the exchange interface (such as servers in Singapore, Japan, Hong Kong, etc.).
  3. Configure the API KEY of the exchange to (place an order) operation when the TradingView signal is sent on FMZ.
  4. You need to have a "TradingView Signal Execution Strategy", which is mainly discussed in this article.

TradingView Signal Execution Strategy

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

Simple test

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.

The test of using TradingView in the actual scene

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)
  1. PINE script can attach some information when the script sends order instructions

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).

  1. Construct messages in combination with the "TradingView Signal Execution Strategy"
{
    "Flag":"{{strategy.order.id}}",
    "Exchange":1,
    "Currency":"BTC_USDT",
    "ContractType":"swap",
    "Price":"-1",
    "Action":"{{strategy.order.comment}}",
    "Amount":"{{strategy.order.contracts}}"
}
  1. Let TradingView send a signal according to the running of the PINE script. You need to set an alert when loading the script on the TradingView

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.

Leave a Reply

Your email address will not be published. Required fields are marked *