Dalam artikel sebelumnyaDiskusi tentang Penerimaan Sinyal Eksternal dari Platform FMZ: API diperluas vs Strategi Layanan HTTP Terbina, kami membandingkan dua cara yang berbeda untuk menerima sinyal eksternal untuk perdagangan programmatic dan menganalisis detailnya. Solusi menggunakan FMZ platform diperluas API untuk menerima sinyal eksternal memiliki strategi lengkap di perpustakaan strategi platform. Dalam artikel ini, mari kita menerapkan solusi lengkap menggunakan strategi built-in layanan Http untuk menerima sinyal.
Mengikuti strategi sebelumnya menggunakan ekstensi FMZ API untuk mengakses sinyal Trading View, kami menggunakan format pesan sebelumnya, metode pemrosesan pesan, dll dan membuat modifikasi sederhana untuk strategi.
Karena layanan bawaan dalam strategi dapat menggunakan Http atau HTTPS, untuk demonstrasi sederhana, kami menggunakan protokol Http, menambahkan verifikasi daftar putih IP, dan menambahkan verifikasi kata sandi.
//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)
}
}
}
Dibandingkan dengan strategi menggunakan API diperluas untuk mengakses sinyal eksternal, strategi tidak banyak berubah.serverFunc
Fungsi pemrosesan layanan Http dan menggunakan metode pengiriman pesan multi-threaded yang baru ditambahkan oleh platform FMZ:postMessage
/ peekMessage
Kode lainnya hampir tidak berubah.
Karena permintaan dari webhook Trading View
52.89.214.238
34.212.75.30
54.218.53.128
52.32.178.7
Oleh karena itu, kita menambahkan parameteripWhiteList
Semua permintaan yang tidak berada dalam daftar putih alamat IP akan diabaikan.
// 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
}
}
Tambahkan parameterpassPhrase
ke strategi untuk mengatur kata sandi verifikasi. Kata sandi ini dikonfigurasi di pengaturan url Webhook di Trading View. Permintaan yang tidak cocok dengan kata sandi verifikasi akan diabaikan.
Sebagai contoh, kita mengatur: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
}
}
Gunakan skrip PINE dari platform Trading View sebagai sumber pemicu sinyal eksternal, dan pilih salah satu skrip PINE yang dirilis secara acak oleh Trading View secara resmi:
//@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 di platform FMZ untuk menjalankan perdagangan langsung, tetapi jika Anda ingin platform Trading View untuk menjalankan skrip PINE untuk mengirim sinyal, Anda hanya dapat menggunakan solusi yang kami bahas.
Untuk menyesuaikan skrip PINE ini dengan pesan dalam permintaan webhook kita, kita perlu memodifikasi fungsi perdagangancomment
, yang akan kita bahas nanti dalam artikel ini.
Pengaturan WebhookUrl dan request body pada dasarnya sama dengan metode API lanjutan sebelumnya untuk mengakses sinyal eksternal. Bagian yang sama tidak akan diulang dalam artikel ini. Anda dapat merujuk ke artikel sebelumnya.
Setelah kami menambahkan skrip PINE ini ke grafik pasar (kami memilih pasar kontrak abadi ETH_USDT Binance untuk pengujian) di Trading View, kami dapat melihat bahwa skrip telah mulai bekerja. Kemudian kami menambahkan peringatan ke skrip seperti yang ditunjukkan pada tangkapan layar.
Pengaturan URL Webhook: Kode stratey telah dirancang untuk menghasilkan URL webhook secara otomatis. kita hanya perlu menyalin dari log pada awal operasi strategi.
http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456
Trading View menetapkan bahwa URL Webhook hanya dapat menggunakan port 80 untuk permintaan Http, jadi kita juga mengatur parameter port menjadi 80 dalam strategi, sehingga kita dapat melihat bahwa port link URL Webhook yang dihasilkan oleh strategi juga 80.
Kemudian kita mengatur pesan request body di tab
{
"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 penempatan order dalam skrip PINE yang baru saja kita bicarakan? mari kita ambil kode pembukaan posisi panjang sebagai contoh:
strategy.entry("MACrossLE", strategy.long, comment="long")
Jadi pengaturan harus konsisten. Di sini kita mengatur
Skrip PINE tidak menentukan jumlah pesanan untuk setiap pesanan, jadi ketika Trading View mengirim pesan peringatan, itu menggunakan jumlah pesanan default untuk mengisi bagian
Ketika skrip PINE yang berjalan di Trading View mengeksekusi fungsi perdagangan, karena kita telah mengatur peringatan URL Webhook, platform Trading View akan mengirim permintaan POST ke layanan Http built-in dari strategi kita.passPhrase
Badan permintaan yang sebenarnya diterima mirip dengan ini:
Kemudian strategi kami mengeksekusi operasi perdagangan yang sesuai berdasarkan pesan dalam tubuh ini.
Hal ini dapat dilihat bahwa strategi melakukan perdagangan sinyal yang disinkronkan dalam lingkungan simulasi OKX sesuai dengan skrip PINE pada Trading View.
Terima kasih atas perhatianmu pada FMZ Quant, dan terima kasih sudah membaca.