Die Ressourcen sind geladen. Beförderung...

Diskussion über den externen Signalempfang der FMZ-Plattform: Eine Komplettlösung für den Empfang von Signalen mit integriertem Http-Service in der Strategie

Schriftsteller:FMZ~Lydia, Erstellt: 2024-12-18 09:22:33, Aktualisiert: 2024-12-18 09:24:49

Vorwort

In dem vorherigen ArtikelDiskussion zum externen Signalempfang der FMZ-Plattform: Erweiterte API vs. Strategie eingebauter HTTP-Service, haben wir zwei verschiedene Möglichkeiten des Empfangs von externen Signalen für den programmatischen Handel verglichen und die Details analysiert. Die Lösung der Verwendung von FMZ-Plattform erweiterte API, um externe Signale zu empfangen, hat eine vollständige Strategie in der Plattform-Strategie-Bibliothek.

Umsetzung der Strategie

Nach der bisherigen Strategie, die FMZ-Erweiterung API für den Zugriff auf Trading View-Signale zu verwenden, verwenden wir das bisherige Nachrichtenformat, die Nachrichtenverarbeitungsmethode usw. und machen einfache Änderungen an der Strategie.

Da die eingebauten Dienste in der Strategie Http oder HTTPS verwenden können, verwenden wir für eine einfache Demonstration das Http-Protokoll, fügen IP-Whitelist-Verifizierung und Passwortverifizierung hinzu.

//Signal structure
var Template = {
    Flag: "45M103Buy",     // Logo, can be specified at will
    Exchange: 1,           // Designated exchange trading pairs
    Currency: "BTC_USDT",  // Trading pairs
    ContractType: "spot",  // Contract type, swap, quarter, next_quarter, spot fill in spot
    Price: "{{close}}",    // Opening or closing price, -1 is the market price
    Action: "buy",         // Transaction type [buy: spot buy, sell: spot sell, long: futures long, short: futures short, closesell: futures buy to close short, closebuy: futures sell to close long]
    Amount: "1",           // Trading volume
}

var Success = "#5cb85c"    // Success color
var Danger = "#ff0000"     // Danger color
var Warning = "#f0ad4e"    // Warning color
var buffSignal = []

// Http service
function serverFunc(ctx, ipWhiteList, passPhrase) {
    var path = ctx.path()
    if (path == "/CommandRobot") {
        // Verify IP address
        var fromIP = ctx.remoteAddr().split(":")[0]        
        if (ipWhiteList && ipWhiteList.length > 0) {
            var ipList = ipWhiteList.split(",")
            if (!ipList.includes(fromIP)) {
                ctx.setStatus(500)
                ctx.write("IP address not in white list")
                Log("500 Error: IP address not in white list", "#FF0000")
                return 
            }
        }

        // Verify password
        var pass = ctx.rawQuery().length > 0 ? ctx.query("passPhrase") : ""
        if (passPhrase && passPhrase.length > 0) {
            if (pass != passPhrase) {
                ctx.setStatus(500)
                ctx.write("Authentication failed")
                Log("500 Error: Authentication failed", "#FF0000")
                return 
            }
        }

        var body = JSON.parse(ctx.body())
        threading.mainThread().postMessage(JSON.stringify(body))
        ctx.write("OK")
        // 200
    } else {
        ctx.setStatus(404)
    }
}

// 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 exchange number is 1 and is an integer.", Danger)
        return
    }
    if (Signal.Amount <= 0 || typeof(Signal.Amount) != "number") {
        Log("The trading volume cannot be less than 0 and must be a numeric type.", typeof(Signal.Amount), Danger)
        return
    }
    if (typeof(Signal.Price) != "number") {
        Log("Price must be a numeric value", Danger)
        return
    }
    if (Signal.ContractType == "spot" && Signal.Action != "buy" && Signal.Action != "sell") {
        Log("The instruction is to operate spot goods, and the Action is wrong, Action:", Signal.Action, Danger)
        return 
    }
    if (Signal.ContractType != "spot" && Signal.Action != "long" && Signal.Action != "short" && Signal.Action != "closesell" && Signal.Action != "closebuy") {
        Log("The instruction is to operate futures, and the Action is wrong, Action:", Signal.Action, Danger)
        return 
    }
    return true
}

// Signal processing object
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 a 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) {
            // If it is not spot, set up a contract
            e.SetContractType(task.ContractType)
        } else if (task.ContractType == "spot" && name.indexOf("Futures_") == -1) {
            isFutures = false 
        } else {
            task.error = "The ContractType in the instruction does not match the configured exchange object type"
            return 
        }
        
        var depth = e.GetDepth()
        if (!depth || !depth.Bids || !depth.Asks) {
            task.error = "Abnormal order book data"
            return 
        }
        
        if (depth.Bids.length == 0 && depth.Asks.length == 0) {
            task.error = "No orders on the market"
            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 get precision"
            return 
        }
        
        e.SetPrecision(self.pricePrecision, self.amountPrecision)
        
        // buy: spot purchase, sell: spot sell, long: futures long, short: futures short, closesell: futures buy to close short, closebuy: futures sell to close long
        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 = "Order failed"
        }
        
        task.finished = true
    }
    
    return self
}

function main() {
    // Reset log information
    if (isResetLog) {
        LogReset(1)
    }

    Log("Transaction type [buy: spot buy, sell: spot sell, long: futures long, short: futures short, closesell: futures buy to close short, closebuy: futures sell to close long]", Danger)
    Log("Instruction templates:", JSON.stringify(Template), Danger)    
    if (!passPhrase || passPhrase.length == 0) {
        Log("webhook url:", `http://${serverIP}:${port}/CommandRobot`)
    } else {
        Log("webhook url:", `http://${serverIP}:${port}/CommandRobot?passPhrase=${passPhrase}`)
    }

    // Creating an Http built-in service
    __Serve("http://0.0.0.0:" + port, serverFunc, ipWhiteList, passPhrase)

    // Initialize the code to execute
    if (initCode && initCode.length > 0) {
        try {
            Log("Execute the initialization code:", initCode)
            eval(initCode)
        } catch(error) {
            Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
        }
    }    

    // Create a signal management object
    var manager = createManager()
    
    while (true) {
        try {
            // Detect interactive controls for testing
            var cmd = GetCommand()
            if (cmd) {
                // Send Http request, simulate test
                var arrCmd = cmd.split(":", 2)
                if (arrCmd[0] == "TestSignal") {
                    // {"Flag":"TestSignal","Exchange":1,"Currency":"BTC_USDT","ContractType":"swap","Price":"10000","Action":"long","Amount":"1"}
                    var signal = cmd.replace("TestSignal:", "")
                    if (!passPhrase || passPhrase.length == 0) {
                        var ret = HttpQuery(`http://${serverIP}:${port}/CommandRobot`, {"method": "POST", "body": JSON.stringify(signal)})
                        Log("Test request response:", ret)
                    } else {
                        var ret = HttpQuery(`http://${serverIP}:${port}/CommandRobot?passPhrase=${passPhrase}`, {"method": "POST", "body": JSON.stringify(signal)})
                        Log("Test request response:", ret)
                    }                    
                }
            }

            // Detect the message that the built-in Http service notifies the main thread after receiving the request, and writes it to the task queue of the manager object
            var msg = threading.mainThread().peekMessage(-1)
            if (msg) {
                Log("Receive message msg:", msg)
                var objSignal = JSON.parse(msg)
                if (DiffObject(Template, objSignal)) {
                    Log("Receive trading signal instructions:", objSignal)
                    buffSignal.push(objSignal)
                    
                    // Check trading volume, exchange ID
                    if (!CheckSignal(objSignal)) {
                        continue
                    }
                    
                    // Create a task
                    if (objSignal["Flag"] == "TestSignal") {
                        Log("Received test message:", JSON.stringify(objSignal))
                    } else {
                        manager.newTask(objSignal)
                    }                    
                } else {
                    Log("Command not recognized", signal)
                }
            } else {
                Sleep(1000 * SleepInterval)
            }

            // Processing tasks
            manager.process()
            
            // Status bar displays signal
            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) + "`")            
        } catch (error) {
            Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
        }        
    }
}

img

  • Portparameter: Wenn Sie das HTTP-Protokoll verwenden, können Sie nur Port 80 auf Trading View setzen.
  • ServerIP-Parameter: Geben Sie die öffentliche IP-Adresse des Servers ein.
  • Parameter initCode: Er kann verwendet werden, um die Basisadresse für Tests in der Austausch-Testumgebung zu wechseln.

Im Vergleich zur Strategie der Nutzung der erweiterten API für den Zugriff auf externe Signale ändert sich die Strategie nicht viel.serverFuncHttp-Dienstverarbeitungsfunktion und verwendet die neu von der FMZ-Plattform hinzugefügte Mehrthread-Nachrichtenübertragungsmethode:postMessage / peekMessageDie anderen Codes sind fast unverändert.

IP-Weißliste

Da die Anfragen von Trading Views Webhook nur von folgenden IP-Adressen gesendet werden:

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

Daher fügen wir einen Parameter hinzuipWhiteListAlle Anfragen, die nicht in der IP-Adressen-Whitelist enthalten sind, werden ignoriert.

        // Verify IP address
        var fromIP = ctx.remoteAddr().split(":")[0]        
        if (ipWhiteList && ipWhiteList.length > 0) {
            var ipList = ipWhiteList.split(",")
            if (!ipList.includes(fromIP)) {
                ctx.setStatus(500)
                ctx.write("IP address not in white list")
                Log("500 Error: IP address not in white list", "#FF0000")
                return 
            }
        }

Passwort überprüfen

Ein Parameter hinzufügenpassPhraseDas Passwort wird in den Webhook-URL-Einstellungen in der Trading-Ansicht konfiguriert. Anfragen, die nicht mit dem Verifizierungspasswort übereinstimmen, werden ignoriert.

Zum Beispiel:test123456.

        // Verify password
        var pass = ctx.rawQuery().length > 0 ? ctx.query("passPhrase") : ""
        if (passPhrase && passPhrase.length > 0) {
            if (pass != passPhrase) {
                ctx.setStatus(500)
                ctx.write("Authentication failed")
                Log("500 Error: Authentication failed", "#FF0000")
                return 
            }
        }

Außensignal

Verwenden Sie das PINE-Skript der Trading View-Plattform als externe Signal-Triggerquelle und wählen Sie eines der von Trading View offiziell zufällig veröffentlichten PINE-Skripte aus:

//@version=6
strategy("MovingAvg Cross", overlay=true)
length = input(9)
confirmBars = input(1)
price = close
ma = ta.sma(price, length)
bcond = price > ma
bcount = 0
bcount := bcond ? nz(bcount[1]) + 1 : 0
if (bcount == confirmBars)
	strategy.entry("MACrossLE", strategy.long, comment="long")
scond = price < ma
scount = 0
scount := scond ? nz(scount[1]) + 1 : 0
if (scount == confirmBars)
	strategy.entry("MACrossSE", strategy.short, comment="short")

Natürlich können Sie auch PINE-Skripte direkt auf der FMZ-Plattform ausführen, um Live-Tradings auszuführen, aber wenn Sie möchten, dass die Trading View-Plattform PINE-Skripte ausführt, um Signale zu senden, können Sie nur die von uns besprochenen Lösungen verwenden.

Um dieses PINE-Skript an die Nachricht in unserer Webhook-Anfrage anzupassen, müssen wir die Handelsfunktion änderncomment, die wir später im Artikel erwähnen werden.

WebhookUrl und Anforderung Körper Einstellungen

Die Einstellungen von WebhookUrl und Request Body sind im Grunde die gleichen wie bei der vorherigen erweiterten API-Methode zum Zugriff auf externe Signale.

Webhook-Adresse

img

Nachdem wir dieses PINE-Skript zu einem Diagramm eines Marktes auf der Trading View hinzugefügt haben (wir wählen den ETH_USDT-Perpetual Contract-Markt von Binance zum Testen) können wir sehen, dass das Skript funktioniert. Dann fügen wir dem Skript eine Warnung hinzu, wie im Screenshot gezeigt.

img

Webhook-URL-Einstellungen: Der Stratey-Code wurde entwickelt, um die Webhook-URL automatisch zu generieren.

img

http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456

Trading View schreibt vor, dass die Webhook-URL nur Port 80 für HTTP-Anfragen verwenden kann, also setzen wir auch den Portparameter auf 80 in der Strategie, so dass wir sehen können, dass der Linkport der Webhook-URL, die von der Strategie generiert wird, auch 80 ist.

Körpernachricht

img

Anschließend setzen wir die Anforderungsbodenmeldung auf der Registerkarte Einstellungen, wie im Screenshot gezeigt.

{
    "Flag":"{{strategy.order.id}}",
    "Exchange":1,
    "Currency":"ETH_USDT",
    "ContractType":"swap",
    "Price":"-1",
    "Action":"{{strategy.order.comment}}",
    "Amount":"{{strategy.order.contracts}}"
}

Erinnern Sie sich an den Code für die Bestellung im PINE-Skript, von dem wir gerade gesprochen haben?

strategy.entry("MACrossLE", strategy.long, comment="long")

MACrossLE ist der in {{ gefüllte Inhaltstrategy.order.id}} wenn die Warnung in Zukunft ausgelöst wird.

long ist der Inhalt, der in {{strategy.order.comment}} ausgefüllt wird, wenn die Warnung in Zukunft ausgelöst wird.

img

Hier setzen wir long und short für die Reihenfolgefunktion, die die Signale für die Eröffnung von Long und Short anzeigt.

Das PINE-Skript gibt die Ordermenge für jede Order nicht an, also verwendet es bei der Trading View eine Warnmeldung mit der Standardordermenge, um den Teil {{strategy.order.contracts}} zu füllen.

Live-Handelstest

img

img

Wenn das PINE-Skript, das in der Trading View ausgeführt wird, die Handelsfunktion ausführt, weil wir die Webhook-URL-Warnung eingestellt haben, sendet die Trading View-Plattform eine POST-Anfrage an den integrierten Http-Dienst unserer Strategie. Diese Anfrage enthält einen PasswortparameterpassPhraseDie tatsächliche erhaltene Antragsstelle sieht folgendermaßen aus:

img

Dann führt unsere Strategie die entsprechenden Handelsoperationen auf der Grundlage der Nachricht in diesem Körper aus.

Es ist zu sehen, dass die Strategie synchronisierten Signalhandel in der OKX-Simulationsumgebung nach dem PINE-Skript in der Trading View durchführt.

Strategieadresse

https://www.fmz.com/strategy/475235

Vielen Dank für Ihre Aufmerksamkeit für FMZ Quant, und vielen Dank fürs Lesen.


Mehr