使用數字貨幣交易所聚合行情接口構建多品種策略


FMZ量化交易平台策略圍觀板塊里面經常見到一些多品種策略,同時檢測幾十個甚至一個交易所全市場的行情。是如何做到的呢?並且需要如何設計呢?本篇文章帶你一起來探討如何使用交易所聚合行情接口構建多品種策略。列舉幣安和火幣這兩個交易所,查看交易所API文檔,發現都有聚合行情接口:

行情接口

  • 幣安合約:
    https://fapi.binance.com/fapi/v1/ticker/bookTicker
    接口返回數據

    [
        {
            "symbol": "BTCUSDT", // 交易對
            "bidPrice": "4.00000000", //最優買單價
            "bidQty": "431.00000000", //掛單量
            "askPrice": "4.00000200", //最優賣單價
            "askQty": "9.00000000", //掛單量
            "time": 1589437530011   // 撮合引擎時間
        }
        ...
    ]
    
  • 火幣現貨幣幣:
    https://api.huobi.pro/market/tickers
    接口返回數據

    [  
        {  
            "open":0.044297,      // 開盤價
            "close":0.042178,     // 收盤價
            "low":0.040110,       // 最低價
            "high":0.045255,      // 最高價
            "amount":12880.8510,  
            "count":12838,
            "vol":563.0388715740,
            "symbol":"ethbtc",
            "bid":0.007545,
            "bidSize":0.008,
            "ask":0.008088,
            "askSize":0.009
        }, 
        ...
    ]
    

    但是,實際不是這樣的,火幣接口實際返回的結構是:

    {
        "status": "ok",
        "ts": 1616032188422,
        "data": [{
      	  "symbol": "hbcbtc",
      	  "open": 0.00024813,
      	  "high": 0.00024927,
      	  "low": 0.00022871,
      	  "close": 0.00023495,
      	  "amount": 2124.32,
      	  "vol": 0.517656218,
      	  "count": 1715,
      	  "bid": 0.00023427,
      	  "bidSize": 2.3,
      	  "ask": 0.00023665,
      	  "askSize": 2.93
        }, ...]
    }
    

    在處理接口返回的數據時需要注意。

構建策略程序框架

如何在策略中封裝這兩個接口,又如何處理數據呢?一起慢慢來看。先來寫一個構造函數,用於構造控制對象。

// 參數e用於傳入exchange交易所對象,參數subscribeList是需要處理的交易對列表,例如["BTCUSDT", "ETHUSDT", "EOSUSDT", "LTCUSDT", "ETCUSDT", "XRPUSDT"]
function createManager(e, subscribeList) {           
    var self = {}
    self.supportList = ["Futures_Binance", "Huobi"]  // 支持的交易所的

    // 對象屬性
    self.e = e
    self.name = e.GetName()
    self.type = self.name.includes("Futures_") ? "Futures" : "Spot"
    self.label = e.GetLabel()
    self.quoteCurrency = ""  
    self.subscribeList = subscribeList   // subscribeList : [strSymbol1, strSymbol2, ...]
    self.tickers = []                    // 接口獲取的所有行情數據,定義數據格式:{bid1: 123, ask1: 123, symbol: "xxx"}}
    self.subscribeTickers = []           // 需要的行情數據,定義數據格式:{bid1: 123, ask1: 123, symbol: "xxx"}}
    self.accData = null                  // 用於記錄賬戶資產數據

    // 初始化函數
    self.init = function() { 
    	// 判斷是否支持該交易所
        if (!_.contains(self.supportList, self.name)) {        	
        	throw "not support"
        }
    }

    // 判斷數據精度
    self.judgePrecision = function (p) {
        var arr = p.toString().split(".")
        if (arr.length != 2) {
            if (arr.length == 1) {
                return 0
            }
            throw "judgePrecision error, p:" + String(p)
        }
        
        return arr[1].length
    }

    // 更新資產
    self.updateAcc = function(callBackFuncGetAcc) {
        var ret = callBackFuncGetAcc(self)
        if (!ret) {
        	return false 
        }
        self.accData = ret 
        return true 
    }

    // 更新行情數據
    self.updateTicker = function(url, callBackFuncGetArr, callBackFuncGetTicker) {
    	var tickers = []
    	var subscribeTickers = []
    	var ret = self.httpQuery(url)
    	if (!ret) {
    		return false 
    	}
    	try {
            _.each(callBackFuncGetArr(ret), function(ele) {
            	var ticker = callBackFuncGetTicker(ele)
            	tickers.push(ticker)
            	for (var i = 0 ; i < self.subscribeList.length ; i++) {
            		if (self.subscribeList[i] == ele.symbol) {
            			subscribeTickers.push(ticker)
            		}
            	}
            })
        } catch(err) {
        	Log("錯誤:", err)
        	return false 
        }

        self.tickers = tickers
        self.subscribeTickers = subscribeTickers
        return true 
    }

    self.httpQuery = function(url) {
    	var ret = null
        try {
            var retHttpQuery = HttpQuery(url)
            ret = JSON.parse(retHttpQuery)
        } catch (err) {
            // Log("錯誤:", err)
            ret = null
        }
        return ret 
    }

    self.returnTickersTbl = function() {
        var tickersTbl = {
        	type : "table", 
        	title : "tickers",
        	cols : ["symbol", "ask1", "bid1"], 
        	rows : []
        }
        _.each(self.subscribeTickers, function(ticker) {        
        	tickersTbl.rows.push([ticker.symbol, ticker.ask1, ticker.bid1])
        })
        return tickersTbl
    }

    // 初始化
    self.init()
	return self 
}

使用FMZ的API函數HttpQuery函數發出請求,訪問交易所接口。使用HttpQuery時需要使用異常處理try...catch處理接口返回失敗等異常情況。
看到這里有的同學可能會問:“交易所接口返回的數據結構各不相同,要怎么處理呢?用同樣的處理方式肯定不行吧。”
確實如此,不僅交易所接口返回的數據結構不同,就連返回的數據字段命名也不同。同樣的一個意思可能是不同的命名。例如以上我們列舉的接口。同樣表達的意思是買一價格,在幣安稱為:bidPrice,在火幣稱為bid

我們這里使用回調函數解決,把這些特殊處理的部分獨立出來。
所以上面這個對象初始化后,具體使用時就成了這樣:
(以下代碼省略了構造函數createManager
以幣安期貨監控這些合約:["BTCUSDT", "ETHUSDT", "EOSUSDT", "LTCUSDT", "ETCUSDT", "XRPUSDT"]
火幣現貨監控這些幣幣交易對:["btcusdt", "ethusdt", "eosusdt", "etcusdt", "ltcusdt", "xrpusdt"]為例子。

function main() {
    var manager1 = createManager(exchanges[0], ["BTCUSDT", "ETHUSDT", "EOSUSDT", "LTCUSDT", "ETCUSDT", "XRPUSDT"])
    var manager2 = createManager(exchanges[1], ["btcusdt", "ethusdt", "eosusdt", "etcusdt", "ltcusdt", "xrpusdt"])   

    while (true) {
    	// 更新行情數據
    	var ticker1GetSucc = manager1.updateTicker("https://fapi.binance.com/fapi/v1/ticker/bookTicker", 
    		function(data) {return data}, 
    		function (ele) {return {bid1: ele.bidPrice, ask1: ele.askPrice, symbol: ele.symbol}})
    	var ticker2GetSucc = manager2.updateTicker("https://api.huobi.pro/market/tickers", 
    		function(data) {return data.data}, 
    		function(ele) {return {bid1: ele.bid, ask1: ele.ask, symbol: ele.symbol}})
        if (!ticker1GetSucc || !ticker2GetSucc) {
        	Sleep(1000)
        	continue
        }
        
        var tbl1 = {
        	type : "table", 
        	title : "期貨行情數據",
        	cols : ["期貨合約", "期貨買一", "期貨賣一"], 
        	rows : []
        }
        _.each(manager1.subscribeTickers, function(ticker) {
        	tbl1.rows.push([ticker.symbol, ticker.bid1, ticker.ask1])
        })
        var tbl2 = {
        	type : "table", 
        	title : "現貨行情數據",
        	cols : ["現貨合約", "現貨買一", "現貨賣一"], 
        	rows : []
        }
        _.each(manager2.subscribeTickers, function(ticker) {
        	tbl2.rows.push([ticker.symbol, ticker.bid1, ticker.ask1])
        })
        LogStatus(_D(), "\n`" + JSON.stringify(tbl1) + "`", "\n`" + JSON.stringify(tbl2) + "`")
        Sleep(10000)
    }
}

運行測試:
第一個交易所對象添加幣安期貨,第二個交易所對象添加火幣現貨

可以看到,這里把如何取接口返回的數據等操作使用回調函數進行不同交易所的特異化處理。

    	var ticker1GetSucc = manager1.updateTicker("https://fapi.binance.com/fapi/v1/ticker/bookTicker", 
    		function(data) {return data}, 
    		function (ele) {return {bid1: ele.bidPrice, ask1: ele.askPrice, symbol: ele.symbol}})
    	var ticker2GetSucc = manager2.updateTicker("https://api.huobi.pro/market/tickers", 
    		function(data) {return data.data}, 
    		function(ele) {return {bid1: ele.bid, ask1: ele.ask, symbol: ele.symbol}})

制定了行情獲取,接下來可以制定賬戶資產獲取,因為多品種策略,賬戶資產數據同樣也要是多個的。好在交易所賬戶資產接口一般都是返回全資產數據。

在構造函數createManager中添加獲取資產的方法

    // 更新資產
    self.updateAcc = function(callBackFuncGetAcc) {
        var ret = callBackFuncGetAcc(self)
        if (!ret) {
        	return false 
        }
        self.accData = ret 
        return true 
    }

同樣由於交易所接口返回的格式,字段命名各不相同,這里也需要使用回調函數特異化處理。

以火幣現貨,幣安期貨為例子,可以這樣寫回調函數:

    // 獲取賬戶資產的回調函數
    var callBackFuncGetHuobiAcc = function(self) {
        var account = self.e.GetAccount()
        var ret = []
        if (!account) {
        	return false 
        }
        // 構造資產的數組結構
        var list = account.Info.data.list
        _.each(self.subscribeList, function(symbol) {
            var coinName = symbol.split("usdt")[0]
            var acc = {symbol: symbol}
            for (var i = 0 ; i < list.length ; i++) {
            	if (coinName == list[i].currency) {
                    if (list[i].type == "trade") {
                        acc.Stocks = parseFloat(list[i].balance)
                    } else if (list[i].type == "frozen") {
                    	acc.FrozenStocks = parseFloat(list[i].balance)
                    }
                } else if (list[i].currency == "usdt") {
                	if (list[i].type == "trade") {
                		acc.Balance = parseFloat(list[i].balance)
                	} else if (list[i].type == "frozen") {
                		acc.FrozenBalance = parseFloat(list[i].balance)
                	}
                }
            }
            ret.push(acc)
        })
        return ret 
    }

    var callBackFuncGetFutures_BinanceAcc = function(self) {
    	self.e.SetCurrency("BTC_USDT")   // 設置為U本位合約的交易對
        self.e.SetContractType("swap")   // 合約都是永續合約
        var account = self.e.GetAccount() 
        var ret = []
        if (!account) {
        	return false 
        }
        var balance = account.Balance
        var frozenBalance = account.FrozenBalance
        // 構造資產數據結構
        _.each(self.subscribeList, function(symbol) {
            var acc = {symbol: symbol}
            acc.Balance = balance
            acc.FrozenBalance = frozenBalance
            ret.push(acc)
        })
        return ret 
    }

運行具有獲取行情、資產功能的策略框架

行情:

資產:

可以看到獲取到行情數據后,可以處理數據計算各個品種的差價,監控多個交易對的期現差價。
進而可以設計一個多品種的期現對沖策略。

根據這樣的設計方式,還可以擴展其它的交易所,有興趣的同學可以動手試下~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM