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.
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)
}
}
}
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.serverFunc
Chứ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
/ peekMessage
Các mã khác hầu như không thay đổi.
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ốipWhiteList
Tấ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
}
}
Thêm một tham sốpassPhrase
và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
}
}
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.
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.
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.
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.
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.
Sau đó, chúng tôi đặt thông báo request body trong tab
{
"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")
Vì vậy, các thiết lập phải phù hợp. Ở đây chúng tôi đặt
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
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.passPhrase
Cơ quan yêu cầu thực tế nhận được tương tự như sau:
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.
Cảm ơn các bạn đã chú ý đến FMZ Quant, và cảm ơn các bạn đã đọc.