[TOC]

Trong bài viết trước“Thảo luận về việc tiếp nhận tín hiệu bên ngoài trên nền tảng FMZ: API mở rộng so với dịch vụ HTTP tích hợp chiến lược”Trong cuộc 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 cho giao dịch theo chương trình và phân tích chi tiết. Giải pháp sử dụng API mở rộng nền tảng FMZ để nhận tín hiệu bên ngoài đã có 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, chúng tôi sẽ triển khai giải pháp hoàn chỉnh sử dụng dịch vụ Http tích hợp chiến lược để nhận tín hiệu.
Tiếp theo chiến lược trước đó là sử dụng API mở rộng FMZ để truy cập tín hiệu Trading View, chúng tôi sử dụng định dạng tin nhắn, phương pháp xử lý tin nhắn, v.v. trước đó và thực hiện những sửa đổi đơn giản cho chiến lược.
Vì các dịch vụ tích hợp trong chính sách có thể sử dụng Http hoặc HTTPS, để minh họa đơ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. Nếu cần tăng cường bảo mật hơn nữa, dịch vụ tích hợp chính sách có thể được thiết kế dưới dạng 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)
}
}
}

So với chiến lược sử dụng API mở rộng để truy cập các tín hiệu bên ngoài, chiến lược thay đổi không lớn, chỉ cần thêm mộtserverFuncChức năng xử lý dịch vụ Http sử dụng phương thức truyền tin đa luồng mới được nền tảng FMZ bổ sung:postMessage/peekMessage, phần còn lại của mã vẫn gần 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
Vì vậy, chúng tôi thêm một tham số vào chiến lượcipWhiteList, được sử dụng để thiết lập danh sách trắng IP. Tất cả các yêu cầu không có trong danh sách trắng địa chỉ IP này sẽ bị bỏ qua.
// 校验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 chiến lượcpassPhrase, được sử dụng để đặt 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. Các yêu cầu không khớp với mật khẩu xác minh sẽ bị bỏ qua.
Ví dụ, chúng ta đặ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 tập lệnh 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 ngẫu nhiên một trong các tập lệnh PINE do Trading View phát hành 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 các tập lệnh PINE trực tiếp trên nền tảng FMZ để thực hiện các giao dịch thực, nhưng nếu bạn muốn nền tảng Trading View chạy các tập lệnh 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 hàm order của tập lệnh này. Để điều chỉnh tập lệnh PINE này cho phù hợp với thông báo trong yêu cầu webhook của chúng ta, chúng ta cần sửa đổi hàm order trong hàm transaction.commentChúng tôi sẽ đề cập đến vấn đề này ở phần sau của bài viết.
Thiết lập WebhookUrl và nội dung yêu cầu về cơ bản giống với phương pháp API mở rộng trước đó để truy cập tín hiệu bên ngoài. Các phần tương tự sẽ không được lặp lại trong bài viết này. Bạn có thể tham khảo bài viết trước.

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

Cài đặt URL Webhook: Mã chính sách được thiết kế để tự động tạo URL webhook. Chúng ta chỉ cần sao chép nó từ nhật ký khi bắt đầu chạy chính sách.

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, do đó bạn có thể thấy rằng cổng liên kết của URL Webhook do chiến lược tạo ra cũng là 80.

Sau đó, đặt nội dung tin nhắn yêu cầu trong tab “Cài đặt” như 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ã lệnh trong tập lệnh PINE vừa đề cập không? Hãy lấy việc mở một vị thế mua trong mã này làm ví dụ:
strategy.entry("MACrossLE", strategy.long, comment="long")
“MACrossLE” là nội dung được điền vào cho “{{strategy.order.id}}” khi cảnh báo được kích hoạt trong tương lai.
“dài” 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. Các tín hiệu được xác định trong chiến lược là (ảnh chụp màn hình bên dưới):

Vì vậy, các thiết lập phải nhất quán. Ở đây chúng ta thiết lập “dài” và “ngắn” cho chức năng đặt lệnh, chỉ ra các tín hiệu để mở vị thế dài hoặc ngắn.
Tập lệnh PINE không chỉ định số lượng lệnh cho mỗi lệnh, vì vậy khi Trading View gửi tin nhắn cảnh báo, nó sẽ sử dụng số lượng lệnh mặc định để điền vào phần “{{strategy.order.contracts}}”.


Khi tập lệnh PINE chạy trên Trading View thực thi chức năng giao dịch, vì chúng tôi đã thiết lập cảnh báo URL Webhook, nền tảng Trading View sẽ gửi yêu cầu POST đến dịch vụ HTTP được tích hợp trong chiến lược của chúng tôi. Truy vấn yêu cầu này chứa tham số Mật khẩu để xác thựcpassPhrase. Nội dung 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 sẽ thực hiện các hoạt động giao dịch tương ứng dựa trên thông điệp trong nội dung này.
Có thể thấy rằng chiến lược này thực hiện giao dịch tín hiệu đồng bộ trong môi trường mô phỏng OKX theo tập lệnh PINE trên Trading View.
Cảm ơn bạn đã quan tâm đến FMZ Quantitative và cảm ơn bạn đã đọc.