Tài nguyên đang được tải lên... tải...

Thảo luận về tiếp nhận tín hiệu bên ngoài của nền tảng FMZ: Một giải pháp hoàn chỉnh để tiếp nhận tín hiệu với dịch vụ Http tích hợp trong chiến lược

Tác giả:FMZ~Lydia, Tạo: 2024-12-18 09:22:33, Cập nhật: 2024-12-19 00:15:43

img

Lời giới thiệu

Trong bài trướcThảo luận về tiếp nhận tín hiệu bên ngoài của nền tảng FMZ: API mở rộng so với chiến lược Dịch vụ HTTP tích hợp, chúng tôi so sánh hai cách khác nhau để nhận tín hiệu bên ngoài cho giao dịch theo chương trình và phân tích chi tiết. Giải pháp sử dụng FMZ nền tảng mở rộng API để nhận tín hiệu bên ngoài có một chiến lược hoàn chỉnh trong thư viện chiến lược nền tảng. Trong bài viết này, hãy thực hiện một giải pháp hoàn chỉnh sử dụng chiến lược tích hợp dịch vụ Http để nhận tín hiệu.

Thực hiện chiến lược

Theo chiến lược trước đây sử dụng FMZ mở rộng API để truy cập tín hiệu Trading View, chúng tôi sử dụng định dạng tin nhắn trước đó, phương pháp xử lý tin nhắn, vv và thực hiện các sửa đổi đơn giản cho chiến lược.

Bởi vì các dịch vụ tích hợp trong chiến lược có thể sử dụng Http hoặc HTTPS, để chứng minh đơn giản, chúng tôi sử dụng giao thức Http, thêm xác minh danh sách trắng IP và thêm xác minh mật khẩu.

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

  • tham số cổng: Nếu bạn sử dụng giao thức HTTP, bạn chỉ có thể đặt cổng 80 trên Trading View.
  • tham số serverIP: Nhập địa chỉ IP công khai của máy chủ.
  • Parameter initCode: Nó có thể được sử dụng để chuyển địa chỉ cơ sở để thử nghiệm trong môi trường thử nghiệm trao đổi.

So với chiến lược sử dụng API mở rộng để truy cập tín hiệu bên ngoài, chiến lược không thay đổi nhiều.serverFuncChức năng xử lý dịch vụ Http và sử dụng phương pháp truyền tin nhắn đa luồng mới được thêm vào bởi nền tảng FMZ:postMessage / peekMessageCác mã khác hầu như không thay đổi.

IP Whitelist

Vì các yêu cầu từ webhook của Trading View chỉ được gửi từ các địa chỉ IP sau:

52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7

Do đó, chúng ta thêm một tham sốipWhiteListTất cả các yêu cầu không có trong danh sách địa chỉ IP sẽ bị bỏ qua.

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

Xác minh mật khẩu

Thêm một tham sốpassPhrasevào chiến lược để thiết lập mật khẩu xác minh. mật khẩu này được cấu hình trong cài đặt url Webhook trên Trading View. Yêu cầu không phù hợp với mật khẩu xác minh sẽ bị bỏ qua.

Ví dụ, chúng ta đặt: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 
            }
        }

Tín hiệu bên ngoài

Sử dụng kịch bản PINE của nền tảng Trading View làm nguồn kích hoạt tín hiệu bên ngoài và chọn một trong những kịch bản PINE được phát hành ngẫu nhiên bởi Trading View chính thức:

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

Tất nhiên, bạn cũng có thể chạy kịch bản PINE trực tiếp trên nền tảng FMZ để thực hiện giao dịch trực tiếp, nhưng nếu bạn muốn nền tảng Trading View chạy kịch bản PINE để gửi tín hiệu, bạn chỉ có thể sử dụng các giải pháp mà chúng tôi đã thảo luận.

Chúng ta cần tập trung vào chức năng đặt lệnh của kịch bản này. Để điều chỉnh kịch bản PINE này với thông điệp trong yêu cầu webhook của chúng tôi, chúng ta cần sửa đổi chức năng giao dịchcomment, mà chúng ta sẽ đề cập sau trong bài viết.

WebhookUrl và yêu cầu body Settings

Các thiết lập của WebhookUrl và request body về cơ bản giống như phương pháp API mở rộng trước đây để truy cập tín hiệu bên ngoài.

Webhook Url

img

Sau khi chúng tôi thêm kịch bản PINE này vào biểu đồ thị trường (chọn thị trường hợp đồng vĩnh cửu ETH_USDT của Binance để thử nghiệm) trên Trading View, chúng tôi có thể thấy rằng kịch bản đã bắt đầu hoạt động. Sau đó chúng tôi thêm cảnh báo vào kịch bản như được hiển thị trong ảnh chụp màn hình.

img

Cài đặt URL Webhook: Mã stratey được thiết kế để tạo ra URL webhook tự động. Chúng ta chỉ cần sao chép nó từ nhật ký vào đầu hoạt động chiến lược.

img

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

Trading View quy định rằng URL Webhook chỉ có thể sử dụng cổng 80 cho các yêu cầu HTTP, vì vậy chúng tôi cũng đặt tham số cổng thành 80 trong chiến lược, vì vậy chúng tôi có thể thấy rằng cổng liên kết của URL Webhook được tạo ra bởi chiến lược cũng là 80.

Thông điệp cơ thể

img

Sau đó, chúng tôi đặt thông báo request body trong tab Settings như được hiển thị trong ảnh chụp màn hình.

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

Bạn có nhớ mã đặt lệnh trong kịch bản PINE mà chúng tôi vừa nói về không?

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

MACrossLE là nội dung được điền vàostrategy.order.id}} khi cảnh báo được kích hoạt trong tương lai.

long là nội dung được điền vào {{strategy.order.comment}} khi cảnh báo được kích hoạt trong tương lai.

img

Vì vậy, các thiết lập phải phù hợp. Ở đây chúng tôi đặt longshort cho chức năng thứ tự, chỉ ra các tín hiệu mở dài và mở ngắn.

Kịch bản PINE không xác định số lượng lệnh cho mỗi lệnh, vì vậy khi Trading View gửi thông báo cảnh báo, nó sử dụng số lượng lệnh mặc định để điền vào phần {{strategy.order.contracts}}.

Thử nghiệm giao dịch trực tiếp

img

img

Khi kịch bản PINE chạy trên Trading View thực hiện chức năng giao dịch, bởi vì chúng tôi đã thiết lập cảnh báo Webhook URL, nền tảng Trading View sẽ gửi một yêu cầu POST đến dịch vụ Http tích hợp của chiến lược của chúng tôi.passPhraseCơ quan yêu cầu thực tế nhận được tương tự như sau:

img

Sau đó chiến lược của chúng tôi thực hiện các giao dịch tương ứng dựa trên thông điệp trong cơ thể này.

Có thể thấy rằng chiến lược thực hiện giao dịch tín hiệu đồng bộ trong môi trường mô phỏng OKX theo kịch bản PINE trên Trading View.

Địa chỉ chiến lược

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

Cảm ơn các bạn đã chú ý đến FMZ Quant, và cảm ơn các bạn đã đọc.


Thêm nữa