[TOC]
Bài trước"Nghiên cứu nhận tín hiệu bên ngoài của nền tảng FMZ: API mở rộng vs chiến lược dịch vụ HTTP tích hợp"Trong bài thảo luận, chúng tôi đã so sánh hai cách khác nhau để nhận tín hiệu bên ngoài để thực hiện giao dịch có tính lập trình và phân tích chi tiết. Các phương pháp để nhận tín hiệu bên ngoài bằng cách sử dụng nền tảng FMZ mở rộng API đã có một chiến lược hoàn chỉnh trong thư viện chính sách của nền tảng.
Mô phỏng chính sách trước đây để truy cập tín hiệu Trading View bằng cách sử dụng FMZ Extension API, chúng tôi đã thay đổi đơn giản theo định dạng tin nhắn trước đó, xử lý tin nhắn, v.v.
Vì các dịch vụ được xây dựng theo chính sách có thể sử dụng HTTP hoặc HTTPS, để minh họa một cách đơ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, thêm xác minh mật khẩu. Nếu cần thêm bảo mật, các dịch vụ được xây dựng theo chính sách có thể được thiết kế như dịch vụ Https.
//信号结构
var Template = {
Flag: "45M103Buy", // 标识,可随意指定
Exchange: 1, // 指定交易所交易对
Currency: "BTC_USDT", // 交易对
ContractType: "spot", // 合约类型,swap,quarter,next_quarter,现货填写spot
Price: "{{close}}", // 开仓或者平仓价格,-1为市价
Action: "buy", // 交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]
Amount: "1", // 交易量
}
var Success = "#5cb85c" // 成功颜色
var Danger = "#ff0000" // 危险颜色
var Warning = "#f0ad4e" // 警告颜色
var buffSignal = []
// Http服务
function serverFunc(ctx, ipWhiteList, passPhrase) {
var path = ctx.path()
if (path == "/CommandRobot") {
// 校验IP地址
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
}
}
// 校验口令
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)
}
}
// 校验信号消息格式
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("交易所最小编号为1,并且为整数", Danger)
return
}
if (Signal.Amount <= 0 || typeof(Signal.Amount) != "number") {
Log("交易量不能小于0,并且为数值类型", typeof(Signal.Amount), Danger)
return
}
if (typeof(Signal.Price) != "number") {
Log("价格必须是数值", Danger)
return
}
if (Signal.ContractType == "spot" && Signal.Action != "buy" && Signal.Action != "sell") {
Log("指令为操作现货,Action错误,Action:", Signal.Action, Danger)
return
}
if (Signal.ContractType != "spot" && Signal.Action != "long" && Signal.Action != "short" && Signal.Action != "closesell" && Signal.Action != "closebuy") {
Log("指令为操作期货,Action错误,Action:", Signal.Action, Danger)
return
}
return true
}
// 信号处理对象
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("创建任务:", 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) {
// 非现货,则设置合约
e.SetContractType(task.ContractType)
} else if (task.ContractType == "spot" && name.indexOf("Futures_") == -1) {
isFutures = false
} else {
task.error = "指令中的ContractType与配置的交易所对象类型不匹配"
return
}
var depth = e.GetDepth()
if (!depth || !depth.Bids || !depth.Asks) {
task.error = "订单薄数据异常"
return
}
if (depth.Bids.length == 0 && depth.Asks.length == 0) {
task.error = "盘口无订单"
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 = "获取精度失败"
return
}
e.SetPrecision(self.pricePrecision, self.amountPrecision)
// buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多
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 = "交易方向错误:" + 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 = "交易方向错误:" + task.Action
return
}
}
var id = tradeFunc(task.Price, task.Amount)
if (!id) {
task.error = "下单失败"
}
task.finished = true
}
return self
}
function main() {
// 重置日志信息
if (isResetLog) {
LogReset(1)
}
Log("交易类型[ buy:现货买入 , sell:现货卖出 , long:期货做多 , short:期货做空 , closesell:期货买入平空 , closebuy:期货卖出平多]", Danger)
Log("指令模板:", 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}`)
}
// 创建Http内置服务
__Serve("http://0.0.0.0:" + port, serverFunc, ipWhiteList, passPhrase)
// 初始化执行的代码
if (initCode && initCode.length > 0) {
try {
Log("执行初始化代码:", initCode)
eval(initCode)
} catch(error) {
Log("e.name:", error.name, "e.stack:", error.stack, "e.message:", error.message)
}
}
// 创建信号管理对象
var manager = createManager()
while (true) {
try {
// 检测交互控件,用于测试
var cmd = GetCommand()
if (cmd) {
// 发送Http请求,模拟测试
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": signal})
Log("测试请求的应答:", ret)
} else {
var ret = HttpQuery(`http://${serverIP}:${port}/CommandRobot?passPhrase=${passPhrase}`, {"method": "POST", "body": signal})
Log("测试请求的应答:", ret)
}
}
}
// 检测内置Http服务收到请求之后通知主线程的消息,写入manager对象的任务队列
var msg = threading.mainThread().peekMessage(-1)
if (msg) {
Log("收到消息 msg:", msg)
var objSignal = JSON.parse(msg)
if (DiffObject(Template, objSignal)) {
Log("接收到交易信号指令:", objSignal)
buffSignal.push(objSignal)
// 检查交易量、交易所编号
if (!CheckSignal(objSignal)) {
continue
}
// 创建任务
if (objSignal["Flag"] == "TestSignal") {
Log("收到测试消息:", JSON.stringify(objSignal))
} else {
manager.newTask(objSignal)
}
} else {
Log("指令无法识别", signal)
}
} else {
Sleep(1000 * SleepInterval)
}
// 处理任务
manager.process()
// 状态栏显示信号
if (buffSignal.length > maxBuffSignalRowDisplay) {
buffSignal.shift()
}
var buffSignalTbl = {
"type" : "table",
"title" : "信号记录",
"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)
}
}
}
Thay đổi chính sách không lớn so với việc sử dụng API mở rộng để truy cập tín hiệu bên ngoài, chỉ thêm mộtserverFunc
Http là một chức năng xử lý dịch vụ, sử dụng các phương thức truyền tin nhiều luồng mới của nền tảng FMZ:postMessage
/peekMessage
Các mã khác hầu như không thay đổi.
Vì Webhook của Trading View chỉ gửi yêu cầu từ địa chỉ IP sau:
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
Vì vậy, chúng ta thêm một tham số vào chiến lược.ipWhiteList
, được sử dụng để thiết lập danh sách trắng IP và bỏ qua các yêu cầu không nằm trong danh sách trắng địa chỉ IP này.
// 校验IP地址
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ố vào chính sáchpassPhrase
, được sử dụng để 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 Webhook url trên Trading View, yêu cầu không khớp mật khẩu xác minh sẽ bị bỏ qua.
Ví dụ, chúng tôi đặt:test123456
。
// 校验口令
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, chọn một trong các kịch bản PINE được công bố chính thức trên Trading View:
//@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ên đĩa thực, nhưng nếu bạn muốn nền tảng Trading View chạy kịch bản PINE để phát 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 phải quan tâm đến các hàm dưới của kịch bản này, để cho kịch bản PINE phù hợp với tin nhắn trong yêu cầu webhook của chúng tôi, chúng ta cần phải tìm hiểu các hàm giao dịch trong thư mục này.comment
Trong khi đó, một số người cho rằng, việc này là một sự thay đổi, và chúng tôi sẽ đề cập đến nó sau bài viết này.
Đối với Webhook Url và request body, thiết lập là tương tự như cách 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 vào một thị trường trên Trading View (chúng tôi đã thử nghiệm thị trường hợp đồng vĩnh cửu ETH_USDT của Binance), bạn có thể thấy kịch bản đã bắt đầu hoạt động. Sau đó, chúng tôi thêm cảnh báo cho kịch bản như trong hình ảnh trên.
Thiết lập URL của Webhook: Trong mã chính sách đã được thiết kế để tự động tạo URL webhook, chúng ta chỉ có thể sao chép trong nhật ký khi chính sách bắt đầu chạy.
http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
Trading View quy định rằng Webhook url chỉ sử dụng 80 cổng cho các yêu cầu HTTP, vì vậy theo chính sách chúng tôi cũng đặt tham số cổng là 80, vì vậy bạn sẽ thấy cổng liên kết của URL Webhook được tạo ra bởi chính sách cũng là 80.
Sau đó, đặt tin nhắn body của yêu cầu trong thẻ "Cài đặt" trong hình ả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ớ đoạn code sau trong kịch bản PINE mà tôi vừa nói không?
strategy.entry("MACrossLE", strategy.long, comment="long")
Trong đó, "MACrossLE" là một trong những cách để kích hoạt báo động trong tương lai.strategy.order.id}}" chứa nội dung.
Trong đó, "long" là một tín hiệu được xác định trong chính sách khi kích hoạt cảnh báo trong tương lai.
Vì vậy, phải được thiết lập phù hợp. Ở đây, chúng tôi đã đặt "long" và "short" cho các hàm đơn giản sau đây, biểu thị nhiều, tín hiệu trống.
Các kịch bản PINE không chỉ định số lượng đơn đặt hàng mỗi lần, vì vậy Trading View sẽ sử dụng mục "Strategy.order.contracts" để gửi thông báo báo động.
Khi kịch bản PINE chạy trên Trading View, thực hiện các chức năng giao dịch, vì chúng tôi đã thiết lập báo động url Webhook, tại thời điểm đó nền tảng Trading View sẽ gửi một yêu cầu POST đến dịch vụ HTTP được xây dựng sẵn trong chính sách của chúng tôi, yêu cầu này chứa một tham số mật khẩu để xác minh trong truy vấn yêu cầu.passPhrase
│ requestbody thực tế được nhận tương tự như sau:
Sau đó, chiến lược của chúng tôi là thực hiện các giao dịch tương ứng dựa trên thông báo trong body này.
Bạn có thể thấy rằng các chiến lược được thực hiện trong môi trường mô phỏng OKX, giao dịch tín hiệu đồng bộ dựa trên kịch bản PINE trên Trading View.
Xin cảm ơn các bạn đã quan tâm đến FMZ Quantity và cảm ơn các bạn đã đọc.