Sumber daya yang dimuat... Pemuatan...

Pembahasan Penerimaan Sinyal Eksternal Platform FMZ: Solusi Lengkap untuk Penerimaan Sinyal dengan Layanan Http Terbina dalam Strategi

Penulis:FMZ~Lydia, Dibuat: 2024-12-18 09:22:33, Diperbarui: 2024-12-18 09:24:49

Pengantar

Dalam artikel sebelumnyaDiskusi tentang Penerimaan Sinyal Eksternal dari Platform FMZ: API diperluas vs Strategi Layanan HTTP Terbina, kami membandingkan dua cara yang berbeda untuk menerima sinyal eksternal untuk perdagangan programmatic dan menganalisis detailnya. Solusi menggunakan FMZ platform diperluas API untuk menerima sinyal eksternal memiliki strategi lengkap di perpustakaan strategi platform. Dalam artikel ini, mari kita menerapkan solusi lengkap menggunakan strategi built-in layanan Http untuk menerima sinyal.

Pelaksanaan Strategi

Mengikuti strategi sebelumnya menggunakan ekstensi FMZ API untuk mengakses sinyal Trading View, kami menggunakan format pesan sebelumnya, metode pemrosesan pesan, dll dan membuat modifikasi sederhana untuk strategi.

Karena layanan bawaan dalam strategi dapat menggunakan Http atau HTTPS, untuk demonstrasi sederhana, kami menggunakan protokol Http, menambahkan verifikasi daftar putih IP, dan menambahkan verifikasi kata sandi.

//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

  • port parameter: Jika Anda menggunakan protokol HTTP, Anda hanya dapat mengatur port 80 pada Trading View.
  • serverIP parameter: Masukkan alamat IP publik server.
  • Parameter initCode: Ini dapat digunakan untuk beralih alamat dasar untuk pengujian di lingkungan pengujian pertukaran.

Dibandingkan dengan strategi menggunakan API diperluas untuk mengakses sinyal eksternal, strategi tidak banyak berubah.serverFuncFungsi pemrosesan layanan Http dan menggunakan metode pengiriman pesan multi-threaded yang baru ditambahkan oleh platform FMZ:postMessage / peekMessageKode lainnya hampir tidak berubah.

Daftar Putih IP

Karena permintaan dari webhook Trading View hanya dikirim dari alamat IP berikut:

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

Oleh karena itu, kita menambahkan parameteripWhiteListSemua permintaan yang tidak berada dalam daftar putih alamat IP akan diabaikan.

        // 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 
            }
        }

Memverifikasi kata sandi

Tambahkan parameterpassPhraseke strategi untuk mengatur kata sandi verifikasi. Kata sandi ini dikonfigurasi di pengaturan url Webhook di Trading View. Permintaan yang tidak cocok dengan kata sandi verifikasi akan diabaikan.

Sebagai contoh, kita mengatur: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 
            }
        }

Sinyal eksternal

Gunakan skrip PINE dari platform Trading View sebagai sumber pemicu sinyal eksternal, dan pilih salah satu skrip PINE yang dirilis secara acak oleh Trading View secara resmi:

//@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")

Tentu saja, Anda juga dapat menjalankan skrip PINE langsung di platform FMZ untuk menjalankan perdagangan langsung, tetapi jika Anda ingin platform Trading View untuk menjalankan skrip PINE untuk mengirim sinyal, Anda hanya dapat menggunakan solusi yang kami bahas.

Untuk menyesuaikan skrip PINE ini dengan pesan dalam permintaan webhook kita, kita perlu memodifikasi fungsi perdagangancomment, yang akan kita bahas nanti dalam artikel ini.

Pengaturan WebhookUrl dan Body Request

Pengaturan WebhookUrl dan request body pada dasarnya sama dengan metode API lanjutan sebelumnya untuk mengakses sinyal eksternal. Bagian yang sama tidak akan diulang dalam artikel ini. Anda dapat merujuk ke artikel sebelumnya.

Url Webhook

img

Setelah kami menambahkan skrip PINE ini ke grafik pasar (kami memilih pasar kontrak abadi ETH_USDT Binance untuk pengujian) di Trading View, kami dapat melihat bahwa skrip telah mulai bekerja. Kemudian kami menambahkan peringatan ke skrip seperti yang ditunjukkan pada tangkapan layar.

img

Pengaturan URL Webhook: Kode stratey telah dirancang untuk menghasilkan URL webhook secara otomatis. kita hanya perlu menyalin dari log pada awal operasi strategi.

img

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

Trading View menetapkan bahwa URL Webhook hanya dapat menggunakan port 80 untuk permintaan Http, jadi kita juga mengatur parameter port menjadi 80 dalam strategi, sehingga kita dapat melihat bahwa port link URL Webhook yang dihasilkan oleh strategi juga 80.

Pesan tubuh

img

Kemudian kita mengatur pesan request body di tab Settings seperti yang ditunjukkan pada screenshot.

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

Apakah Anda ingat kode penempatan order dalam skrip PINE yang baru saja kita bicarakan? mari kita ambil kode pembukaan posisi panjang sebagai contoh:

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

MACrossLE adalah isi yang diisi dengan {{strategy.order.id}} ketika peringatan dipicu di masa depan.

long adalah konten yang diisi dalam {{strategi.order.comment}} ketika peringatan dipicu di masa depan.

img

Jadi pengaturan harus konsisten. Di sini kita mengatur long dan short untuk fungsi order, menunjukkan sinyal pembukaan panjang dan pembukaan pendek.

Skrip PINE tidak menentukan jumlah pesanan untuk setiap pesanan, jadi ketika Trading View mengirim pesan peringatan, itu menggunakan jumlah pesanan default untuk mengisi bagian {{strategi.order.contracts}}.

Tes perdagangan langsung

img

img

Ketika skrip PINE yang berjalan di Trading View mengeksekusi fungsi perdagangan, karena kita telah mengatur peringatan URL Webhook, platform Trading View akan mengirim permintaan POST ke layanan Http built-in dari strategi kita.passPhraseBadan permintaan yang sebenarnya diterima mirip dengan ini:

img

Kemudian strategi kami mengeksekusi operasi perdagangan yang sesuai berdasarkan pesan dalam tubuh ini.

Hal ini dapat dilihat bahwa strategi melakukan perdagangan sinyal yang disinkronkan dalam lingkungan simulasi OKX sesuai dengan skrip PINE pada Trading View.

Alamat Strategi

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

Terima kasih atas perhatianmu pada FMZ Quant, dan terima kasih sudah membaca.


Lebih banyak