[TOC]

Pada artikel sebelumnya“Diskusi tentang penerimaan sinyal eksternal pada platform FMZ: API yang diperluas vs strategi layanan HTTP bawaan”Dalam diskusi ini, kami membandingkan dua cara berbeda dalam menerima sinyal eksternal untuk perdagangan terprogram dan menganalisis detailnya. Solusi penggunaan API ekstensi platform FMZ untuk menerima sinyal eksternal sudah memiliki strategi lengkap di pustaka strategi platform. Dalam artikel ini, kami akan menerapkan solusi lengkap penggunaan strategi layanan Http bawaan untuk menerima sinyal.
Mengikuti strategi sebelumnya menggunakan API ekstensi FMZ untuk mengakses sinyal Trading View, kami menggunakan format pesan sebelumnya, metode pemrosesan pesan, dll. dan membuat modifikasi sederhana pada strategi tersebut.
Karena layanan bawaan dalam kebijakan dapat menggunakan Http atau HTTPS, untuk demonstrasi sederhana, kami menggunakan protokol Http, menambahkan verifikasi daftar putih IP, dan menambahkan verifikasi kata sandi. Jika ada kebutuhan untuk meningkatkan keamanan lebih lanjut, layanan kebijakan bawaan dapat dirancang sebagai layanan 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)
}
}
}

Dibandingkan dengan strategi menggunakan API yang diperluas untuk mengakses sinyal eksternal, perubahan strategi tidak besar, hanya menambahkanserverFuncFungsi pemrosesan layanan Http menggunakan metode penyampaian pesan multi-utas yang baru ditambahkan oleh platform FMZ:postMessage/peekMessage, sisa kodenya hampir tidak berubah.
Karena permintaan dari webhook Trading View hanya dikirim dari alamat IP berikut:
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
Jadi kami menambahkan parameter ke strategiipWhiteList, digunakan untuk menetapkan daftar putih IP. Semua permintaan yang tidak ada dalam daftar putih alamat IP ini akan diabaikan.
// 校验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
}
}
Tambahkan parameter ke strategipassPhrase, digunakan untuk mengatur kata sandi verifikasi. Kata sandi ini dikonfigurasi dalam pengaturan url Webhook di Trading View. Permintaan yang tidak sesuai dengan kata sandi verifikasi akan diabaikan.
Misalnya, kita menetapkan: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
}
}
Gunakan skrip PINE dari platform Trading View sebagai sumber pemicu sinyal eksternal, dan pilih secara acak salah satu skrip PINE yang dirilis secara resmi oleh 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")
Tentu saja, Anda juga dapat menjalankan skrip PINE langsung pada platform FMZ untuk mengeksekusi transaksi nyata, tetapi jika Anda ingin platform Trading View menjalankan skrip PINE untuk mengirim sinyal, Anda hanya dapat menggunakan solusi yang telah kita bahas.
Kita perlu fokus pada fungsi order dari skrip ini. Untuk mengadaptasi skrip PINE ini ke pesan dalam permintaan webhook kita, kita perlu mengubah fungsi order dalam fungsi transaksi.commentKami akan menyebutkannya nanti di artikel.
Pengaturan WebhookUrl dan isi permintaan pada dasarnya sama dengan metode API yang diperluas sebelumnya untuk mengakses sinyal eksternal. Bagian yang sama tidak akan diulang dalam artikel ini. Anda dapat merujuk ke artikel sebelumnya.

Saat kami menambahkan skrip PINE ini ke grafik pasar di Trading View (kami memilih pasar kontrak berjangka ETH_USDT Binance untuk pengujian kami), kami dapat melihat bahwa skrip tersebut telah mulai berfungsi. Lalu kita tambahkan alarm ke skrip seperti yang ditunjukkan pada gambar tangkapan layar.

Pengaturan URL Webhook: Kode kebijakan telah dirancang untuk secara otomatis menghasilkan URL webhook. Kita hanya perlu menyalinnya dari log di awal penerapan kebijakan.

http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
Trading View menetapkan bahwa URL Webhook hanya dapat menggunakan port 80 untuk permintaan HTTP, jadi kami juga menetapkan parameter port ke 80 dalam strategi, sehingga Anda dapat melihat bahwa port tautan URL Webhook yang dihasilkan oleh strategi tersebut juga 80.

Lalu atur isi pesan permintaan di tab “Pengaturan” seperti yang ditunjukkan pada gambar tangkapan layar.
{
"Flag":"{{strategy.order.id}}",
"Exchange":1,
"Currency":"ETH_USDT",
"ContractType":"swap",
"Price":"-1",
"Action":"{{strategy.order.comment}}",
"Amount":"{{strategy.order.contracts}}"
}
Apakah Anda ingat kode pesanan dalam skrip PINE yang baru saja disebutkan? Mari kita ambil contoh pembukaan posisi long dalam kode berikut:
strategy.entry("MACrossLE", strategy.long, comment="long")
“MACrossLE” adalah konten yang diisi untuk “{{strategy.order.id}}” saat alarm dipicu di masa mendatang.
“long” adalah konten yang diisi di “{{strategy.order.comment}}” saat alarm dipicu di masa mendatang. Sinyal-sinyal yang diidentifikasi dalam strategi tersebut adalah (tangkapan layar di bawah):

Jadi pengaturannya harus konsisten. Di sini kami menetapkan “long” dan “short” untuk fungsi order, yang menunjukkan sinyal untuk membuka posisi long atau short.
Skrip PINE tidak menentukan jumlah pesanan untuk setiap pesanan, jadi saat Trading View mengirimkan pesan peringatan, ia menggunakan jumlah pesanan default untuk mengisi bagian “{{strategy.order.contracts}}”.


Ketika skrip PINE yang berjalan pada Trading View menjalankan fungsi perdagangan, karena kita telah menyiapkan alarm URL Webhook, platform Trading View akan mengirimkan permintaan POST ke layanan HTTP yang dibangun dalam strategi kita. Permintaan ini berisi parameter Kata Sandi untuk autentikasipassPhrase. Isi permintaan aktual yang diterima mirip dengan ini:

Kemudian strategi kami mengeksekusi operasi transaksi yang sesuai berdasarkan pesan dalam badan ini.
Dapat dilihat bahwa strategi tersebut melakukan perdagangan sinyal tersinkronisasi dalam lingkungan simulasi OKX sesuai dengan skrip PINE pada Trading View.
Terima kasih atas perhatian Anda pada FMZ Quantitative, dan terima kasih telah membaca.