Sumber daya yang dimuat... Pemuatan...

FMZ platform eksplorasi penerimaan sinyal eksternal: strategi built-in https layanan solusi lengkap untuk penerimaan sinyal

Penulis:Penemu Kuantitas - Mimpi Kecil, Dibuat: 2024-12-17 11:44:07, Diperbarui: 2024-12-17 16:08:11

[TOC]

img

Pengantar

Artikel sebelumnya"Menggali Penerimaan Sinyal Eksternal Platform FMZ: Perluas API vs Strategi Layanan HTTP Bawah Tanah"Dalam diskusi ini, kami membandingkan dua cara berbeda untuk menerima sinyal eksternal untuk melakukan transaksi terprogram dan menganalisis detailnya. Sistem untuk menerima sinyal eksternal menggunakan platform FMZ untuk memperluas API sudah memiliki strategi yang lengkap di repositori kebijakan platform.

Implementasi Strategi

Dengan mengimpor strategi sebelumnya untuk mengakses sinyal Trading View menggunakan FMZ Extension API, kami melakukan modifikasi sederhana pada strategi ini dengan menggunakan format pesan sebelumnya, pengolahan pesan, dll.

Karena layanan built-in kebijakan dapat menggunakan HTTP atau HTTPS, untuk demonstrasi sederhana, kami menggunakan protokol HTTP untuk menambahkan verifikasi daftar putih IP, menambahkan verifikasi kata sandi. Jika ada kebutuhan untuk meningkatkan keamanan lebih lanjut, layanan built-in kebijakan 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)
        }        
    }
}

img

  • Parameter port: Jika menggunakan protokol Http, hanya 80 port yang dapat disetel di Trading View.
  • Server IP Parameter: Mengisi alamat IP publik server.
  • Parameter initCode: dapat digunakan untuk menukar alamat basis, untuk pengujian lingkungan pengujian di bursa.

Perbedaan yang signifikan antara kebijakan untuk mengakses sinyal eksternal dengan menggunakan metode API yang diperluas adalah bahwa perubahan kebijakan tidak terlalu besar, hanya menambahkan satuserverFuncFungsi pemrosesan layanan https menggunakan fitur baru dari platform FMZ untuk mengirim pesan multi-threaded:postMessage/peekMessageDi sini, Anda dapat melihat beberapa gambar yang sangat menarik dari situs ini.

Daftar Putih IP

Karena permintaan Webhook dari 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 kita tambahkan parameter ke dalam strategi.ipWhiteListAplikasi ini digunakan untuk membuat daftar putih IP dan mengabaikan setiap permintaan yang tidak termasuk dalam daftar putih alamat IP ini.

        // 校验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 
            }
        }

Password yang dapat diverifikasi

Tambahkan parameter ke kebijakanpassPhrase, yang digunakan untuk mengatur password verifikasi, yang dikonfigurasi dalam pengaturan url Webhook di Trading View, dan permintaan yang tidak cocok dengan password verifikasi diabaikan.

Di sini kita bisa melihat beberapa contoh: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 
            }
        }

Sinyal luar

Dengan menggunakan skrip PINE dari platform Trading View sebagai sumber pemicu sinyal eksternal, pilihlah salah satu dari skrip PINE yang tersedia secara resmi di 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 secara langsung di platform FMZ untuk melakukan transaksi langsung, tetapi jika Anda ingin platform Trading View menjalankan skrip PINE untuk mengirim sinyal, Anda hanya dapat menggunakan program yang kami bahas.

Kita perlu memperhatikan fungsi submenu dari skrip ini, dan untuk membuat skrip PINE sesuai dengan pesan dalam permintaan webhook kita, kita perlu melihat apa yang terjadi dalam fungsi transaksi.commentSetelah itu, kita akan membahas beberapa perubahan yang akan kita bahas di artikel selanjutnya.

Webhook Url dan pengaturan requestbody

Untuk pengaturan Webhook Url dan request body, yang hampir sama dengan cara API ekstensi sebelumnya untuk mengakses sinyal eksternal, tempat yang sama tidak lagi dibahas dalam artikel ini. Anda dapat melihat artikel sebelumnya.

Url Webhook

img

Setelah kita menambahkan skrip PINE ke suatu pasar di Trading View (kita menguji pasar kontrak jangka panjang ETH_USDT yang dipilih oleh Binance), kita bisa melihat bahwa skrip telah mulai berjalan.

img

Setelan URL Webhook: Dalam kode kebijakan sudah dirancang untuk secara otomatis menghasilkan url webhook, yang hanya bisa kita copy di log di mana kebijakan dimulai.

img

http://xxx.xxx.xxx.xxx:80/CommandRobot?passPhrase=test123456

Trading View menetapkan bahwa URL Webhook hanya dapat menggunakan port 80 untuk permintaan HTTP, jadi secara teknis kami juga menetapkan parameter port menjadi 80, jadi Anda melihat bahwa port link URL Webhook yang dihasilkan oleh kebijakan juga 80.

berita body

img

Setelah itu, setel pesan body yang diminta di tab "Setelan" pada layar.

{
    "Flag":"{{strategy.order.id}}",
    "Exchange":1,
    "Currency":"ETH_USDT",
    "ContractType":"swap",
    "Price":"-1",
    "Action":"{{strategy.order.comment}}",
    "Amount":"{{strategy.order.contracts}}"
}

Ingat kode di bawah ini dari skrip PINE yang baru saja saya bicarakan?

strategy.entry("MACrossLE", strategy.long, comment="long")

"MACrossLE" adalah alat yang digunakan untuk memicu alarm di masa depan.strategy.order.id}}" berisi isi.

"Long" adalah sinyal untuk mengisi "strategy.order.comment" pada saat alarm akan dimulai.

img

Jadi harus disesuaikan. Di sini kita memberikan fungsi tunggal untuk "long" dan "short", yang menunjukkan sinyal yang lebih banyak dan terbuka.

Skenario PINE tidak menentukan berapa banyak pesanan berikutnya untuk setiap kali, jadi Trading View menggunakan bagian "strategy.order.contracts" untuk mengirim pesan peringatan.

Pengujian real disk

img

img

Ketika skrip PINE yang berjalan di Trading View, menjalankan fungsi transaksi, karena kami telah mengatur Webhook url alarm, pada saat ini platform Trading View akan mengirim permintaan POST ke layanan HTTP yang dibangun dalam kebijakan kami, yang berisi parameter kata sandi yang digunakan untuk validasi.passPhrase│ Bentuk permintaan yang sebenarnya diterima adalah seperti ini:

img

Kemudian strategi kami adalah melakukan transaksi sesuai dengan pesan dalam body ini.

Seperti yang dapat dilihat, strategi ini dilakukan di lingkungan simulasi OKX, yang melakukan perdagangan sinyal sinkronisasi berdasarkan skrip PINE di Trading View.

Alamat strategis

https://www.fmz.com/strategy/475235

Terima kasih atas perhatian Anda terhadap FMZ Quantity dan terima kasih telah membaca.


Lebih banyak