Sumber dimuat naik... memuat...

Perbincangan mengenai Penerimaan Isyarat Luaran Platform FMZ: Penyelesaian Lengkap untuk Menerima Isyarat dengan Perkhidmatan Http Terbina dalam Strategi

Penulis:FMZ~Lydia, Dicipta: 2024-12-18 09:22:33, Dikemas kini: 2024-12-18 09:24:49

Pengantar

Dalam artikel sebelumnyaPerbincangan mengenai Penerimaan Isyarat Luaran Platform FMZ: API Terpanjang vs Strategi Perkhidmatan HTTP Terbina dalam, kami membandingkan dua cara yang berbeza untuk menerima isyarat luaran untuk perdagangan programatik dan menganalisis perinciannya. Penyelesaian menggunakan API lanjutan platform FMZ untuk menerima isyarat luaran mempunyai strategi lengkap dalam perpustakaan strategi platform. Dalam artikel ini, mari kita melaksanakan penyelesaian lengkap menggunakan strategi perkhidmatan Http terbina dalam untuk menerima isyarat.

Pelaksanaan Strategi

Berikutan strategi sebelumnya menggunakan API sambungan FMZ untuk mengakses isyarat Trading View, kami menggunakan format mesej sebelumnya, kaedah pemprosesan mesej, dan lain-lain dan membuat pengubahsuaian mudah kepada strategi.

Kerana perkhidmatan terbina dalam strategi boleh menggunakan Http atau HTTPS, untuk demonstrasi yang mudah, kami menggunakan protokol Http, menambah pengesahan senarai putih IP, dan menambah pengesahan kata laluan.

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

  • parameter port: Jika anda menggunakan protokol Http, anda hanya boleh menetapkan port 80 pada Trading View.
  • Parameter serverIP: Masukkan alamat IP awam pelayan.
  • Parameter initCode: Ia boleh digunakan untuk menukar alamat asas untuk ujian dalam persekitaran ujian pertukaran.

Berbanding dengan strategi menggunakan API yang diperluaskan untuk mengakses isyarat luaran, strategi ini tidak banyak berubah.serverFuncFungsi pemprosesan perkhidmatan Http dan menggunakan kaedah lulus mesej berbilang benang yang baru ditambah oleh platform FMZ:postMessage / peekMessageKod lain hampir tidak berubah.

Senarai Putih IP

Oleh kerana permintaan dari webhook Trading View hanya dihantar dari alamat IP berikut:

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

Oleh itu, kita menambah parameteripWhiteListSemua permintaan yang tidak berada dalam senarai 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 
            }
        }

Memeriksa kata laluan

Tambah parameterpassPhrasekepada strategi untuk menetapkan kata laluan pengesahan. Kata laluan ini dikonfigurasi dalam tetapan url Webhook pada Trading View. Permintaan yang tidak sepadan dengan kata laluan pengesahan akan diabaikan.

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

Isyarat luaran

Gunakan skrip PINE platform Trading View sebagai sumber pencetus isyarat luaran, dan pilih salah satu skrip PINE yang dikeluarkan secara rawak oleh Trading View secara rasmi:

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

Sudah tentu, anda juga boleh menjalankan skrip PINE secara langsung di platform FMZ untuk menjalankan perdagangan langsung, tetapi jika anda mahu platform Trading View menjalankan skrip PINE untuk menghantar isyarat, anda hanya boleh menggunakan penyelesaian yang kami bincangkan.

Untuk menyesuaikan skrip PINE ini dengan mesej dalam permintaan webhook kita, kita perlu mengubah suai fungsi perdagangancomment, yang akan kita sebutkan kemudian dalam artikel ini.

WebhookUrl dan Permohonan badan Tetapan

Tetapan WebhookUrl dan badan permintaan pada dasarnya sama dengan kaedah API lanjutan sebelumnya untuk mengakses isyarat luaran. Bahagian yang sama tidak akan diulangi dalam artikel ini. Anda boleh merujuk kepada artikel sebelumnya.

URL Webhook

img

Selepas kita menambahkan skrip PINE ini ke carta pasaran (kita memilih pasaran kontrak kekal ETH_USDT Binance untuk ujian) di Trading View, kita dapat melihat bahawa skrip telah mula berfungsi. Kemudian kita menambah amaran kepada skrip seperti yang ditunjukkan dalam tangkapan skrin.

img

Tetapan URL Webhook: Kod stratey telah direka untuk menjana URL webhook secara automatik. kita hanya perlu menyalin dari log pada permulaan operasi strategi.

img

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

Trading View menetapkan bahawa URL Webhook hanya boleh menggunakan port 80 untuk permintaan Http, jadi kita juga menetapkan parameter port ke 80 dalam strategi, jadi kita boleh melihat bahawa port pautan URL Webhook yang dihasilkan oleh strategi juga 80.

Mesej badan

img

Kemudian kita menetapkan mesej badan permintaan dalam tab Settings seperti yang ditunjukkan dalam tangkapan skrin.

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

Adakah anda ingat kod penempatan pesanan dalam skrip PINE yang baru kita bincangkan?

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

MACrossLE adalah kandungan yang diisi dengan {{strategy.order.id}} apabila amaran dicetuskan pada masa akan datang.

long adalah kandungan yang diisi dalam {{strategi.order.comment}} apabila amaran dicetuskan pada masa akan datang. Isyarat yang dikenal pasti dalam strategi adalah (screenshot di bawah):

img

Jadi tetapan mesti konsisten. di sini kita menetapkan long dan short untuk fungsi perintah, menunjukkan isyarat pembukaan panjang dan pembukaan pendek.

Skrip PINE tidak menentukan kuantiti pesanan untuk setiap pesanan, jadi apabila Trading View menghantar mesej amaran, ia menggunakan kuantiti pesanan lalai untuk mengisi bahagian {{strategi.order.contracts}}.

Ujian perdagangan langsung

img

img

Apabila skrip PINE yang dijalankan pada Trading View melaksanakan fungsi perdagangan, kerana kita telah menetapkan amaran URL Webhook, platform Trading View akan menghantar permintaan POST kepada perkhidmatan Http terbina dalam strategi kita.passPhraseBadan permintaan sebenar yang diterima adalah serupa dengan ini:

img

Kemudian strategi kami melaksanakan operasi dagangan yang sepadan berdasarkan mesej dalam badan ini.

Ia dapat dilihat bahawa strategi melakukan perdagangan isyarat yang diselaraskan dalam persekitaran simulasi OKX mengikut skrip PINE pada Trading View.

Alamat Strategi

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

Terima kasih kerana memberi perhatian kepada FMZ Quant, dan terima kasih kerana membaca.


Lebih lanjut