幣圈量化交易萌新看過來--帶你走近幣圈量化(六)


上篇我們一起動手做了一個簡單的網格策略,本期文章我們把這個策略升級擴展,擴展成一個多品種現貨網格策略,並讓這個策略進行實戰測試。目的並非要找到一個"聖杯",而是要從策略設計中探討設計策略時的各種問題思考、解決思路。本篇會講解我在設計這個策略時的一些經驗、本篇內容略微復雜,需要對程序編寫有一定基礎。

基於策略需求的設計思考

本篇和上篇文章一樣,依然基於發明者量化(FMZ.COM)來探討設計。

多品種
說白了就是我想這個網格策略不僅做BTC_USDT,還能做LTC_USDT/EOS_USDT/DOGE_USDT/ETC_USDT/ETH_USDT。反正現貨的交易對,想跑的品種同時都做網格交易。嗯~~捕捉多個品種的震盪行情,感覺還不錯。需求聽着很簡單,設計的時候問題就來了。
1、首先多個品種的行情獲取。這個是第一個要解決的問題。在查閱了交易所的API文檔后,我發現一般交易所都提供聚合行情接口。OK,就用聚合行情接口獲取數據。

2、第二個遇到的問題就是賬戶資產。因為是要做多品種的策略,所以就要考慮各個交易對資產分別管理。並且要一次獲取所有資產的數據,記錄。為什么要獲取賬戶資產數據呢?還要分開各個交易對記錄?因為你需要在下單時判斷可用資產嘛,是不是要獲取一下再判斷呢?還有你需要計算收益呀,是不是也要先記錄一個最初的賬戶資產數據,然后獲取當前的賬戶資產數據並且和最初的對比算出盈虧?好在交易所的資產賬戶接口通常也是返回所有幣種資產數據的,我們只用獲取一次,然后處理數據即可。

3、策略參數設計。多品種的參數設計和單品種的參數設計差別較大,因為多品種的各個品種交易邏輯雖然是相同的,但是有可能交易時的參數是不同的。比如網格策略,可能做BTC_USDT交易對時希望每次交易0.01個BTC,但是做DOGE_USDT的時候如果還是這個參數(交易0.01個幣)顯然是不合適的,當然你也可以按USDT金額去處理。但是依然還是會有問題,萬一就是想BTC_USDT交易1000U,DOGE_USDT交易10U呢?需求始終無法滿足。
可能還有同學會思考這個問題,然后提出:“我可以多設置幾組參數,分開控制要做的不同交易對的參數。”這依然是不能靈活滿足需求,設置幾組參數為好呢?設置了三組參數,假如我要做4個品種呢?難不成還要修改策略,增加參數...所以設計多品種策略的參數時要充分考慮到這種差異化參數的需求,一個解決辦法就是把參數設計成普通字符串或者JSON字符串。例如:

ETHUSDT:100:0.002|LTCUSDT:20:0.1

其中“|”分割的是每個品種的數據,意思就是ETHUSDT:100:0.002是控制ETH_USDT交易對的,LTCUSDT:20:0.1是控制LTC_USDT交易對的。中間"|"是起到分割作用。ETHUSDT:100:0.002,其中ETHUSDT表示你要做的交易對是什么,100是網格間距,0.002是每個網格交易的ETH幣數,“:”號是分割這些數據的(當然,這些參數規則是策略設計者制定的,你根據你的需求設計成什么樣都行)。這些字符串里面就包含了你要做的各個品種的參數信息了,在策略中解析這些字符串,具體給策略的變量賦值,用來控制各個品種的交易邏輯。那如何解析呢?還是用上面的例子。

function main() {
    var net = []  // 記錄的網格參數,具體運行到網格交易邏輯時,使用這里面的數據
    var params = "ETHUSDT:100:0.002|LTCUSDT:20:0.1"
    var arrPair = params.split("|")
    _.each(arrPair, function(pair) {
        var arr = pair.split(":")
        var symbol = arr[0]              // 交易對名稱
        var diff = parseFloat(arr[1])    // 網格間距
        var amount = parseFloat(arr[2])  // 網格下單量
        net.push({symbol : symbol, diff : diff, amount : amount})
    })
    Log("網格參數數據:", net)
}

看這樣就把參數解析了,當然你還可以直接用JSON字符串,更加簡單。

function main() {        
    var params = '[{"symbol":"ETHUSDT","diff":100,"amount":0.002},{"symbol":"LTCUSDT","diff":20,"amount":0.1}]'
    var net = JSON.parse(params)  // 記錄的網格參數,具體運行到網格交易邏輯時,使用這里面的數據        
    _.each(net, function(pair) {
        Log("交易對:", pair.symbol, pair)
    })
}

4、數據持久化
可以實戰的策略和教學策略差別也較大,上篇的教學策略僅僅是初步測試策略邏輯、設計,實戰的時候考慮的問題就更多了。在實盤時,可能開啟、停止實盤。這時,實盤運行時的數據會全部丟掉。那么如何讓實盤停止后,重啟可以繼續之前的狀態運行呢?這里就需要做實盤運行時的關鍵數據的持久化保存,以供再次啟動時讀取這些數據,繼續運行。在發明者量化交易平台上可以使用_G()函數,或者使用數據庫操作函數DBExec(),具體可以查詢FMZ API文檔。例如我們設計一個掃尾函數,使用_G()函數,保存網格數據。

var net = null 
function main() {  // 策略主函數
    // 首先讀取儲存的net
    net = _G("net")
    
    // ...
}

function onExit() {
    _G("net", net)
    Log("執行掃尾處理,保存數據", "#FF0000")
}

function onexit() {    // 平台系統定義的退出掃尾函數,在點擊實盤停止時觸發執行
    onExit()
}

function onerror() {   // 平台系統定義的異常退出函數,在程序發生異常時觸發執行
    onExit()
}

5、下單量精度、下單價格精度、最小下單量、最小下單金額等限制
回測系統中並未對下單量、下單精度等做那么嚴苛的限制,但是實盤的時候各個交易所對於報單時價格、下單量可以有嚴格標准的,並且各個交易對的這些限制並不相同。所以經常有萌新在回測系統測試OK,一上實盤,觸發交易時就有各種問題,然后也沒看報錯信息內容,出現各種抓狂的現象【手動狗頭】。對於多品種的情況,這個需求更加復雜。單品種策略,你可以設計一個參數用來指定精度等信息,但是多品種策略設計時,顯然這些信息寫到參數里會顯得參數十分臃腫。這個時候就需要查看交易所API文檔,看交易所文檔中有沒有交易對相關信息的接口。如果有這些接口,就可以在策略中設計自動訪問接口獲取精度等信息,配置到參與交易的交易對信息中(簡單說就是精度什么的自動向交易所請求獲取,然后適配到策略參數相關的變量上)。

6、不同交易所的適配
為什么把這個問題放在最后說呢?因為以上我們講的這些問題處理辦法會帶來這最后一個問題,因為我們策略計划使用聚合行情接口,訪問交易所交易對精度等數據自適應,訪問賬戶信息分別處理各個交易對等這些方案會因交易所不同帶來很大差別。有接口調用上的差別、有機制上的差別。對於現貨交易所差別還小一點,如果這個網格策略擴展成期貨的版本。各個交易所機制上的差別更大。一個處理辦法就是設計一個FMZ模板類庫。把這些差異化的實現在類庫中編寫設計。減小策略本身和交易所的耦合。這么做的缺點就是需要編寫一個模板類庫,並且在這個模板中針對每個交易所差異具體實現。

設計一個模板類庫

基於以上的分析,設計一個模板類庫用來降低策略與交易所機制、接口之間的耦合性。我們可以這樣設計這個模板類庫(部分代碼省略):

function createBaseEx(e, funcConfigure) {
    var self = {}
    self.e = e 
    
    self.funcConfigure = funcConfigure
    self.name = e.GetName()
    self.type = self.name.includes("Futures_") ? "Futures" : "Spot"
    self.label = e.GetLabel()
    
    // 需要實現的接口
    self.interfaceGetTickers = null   // 創建異步獲取聚合行情數據線程的函數
    self.interfaceGetAcc = null       // 創建異步獲取賬戶數據線程的函數
    self.interfaceGetPos = null       // 獲取持倉
    self.interfaceTrade = null        // 創建並發下單
    self.waitTickers = null           // 等待並發行情數據 
    self.waitAcc = null               // 等待賬戶並發數據
    self.waitTrade = null             // 等待下單並發數據
    self.calcAmount = null            // 根據交易對精度等數據計算下單量
    self.init = null                  // 初始化工作,獲取精度等數據
    
    // 執行配置函數,給對象配置
    funcConfigure(self)

    // 檢測configList約定的接口是否都實現
    _.each(configList, function(funcName) {
        if (!self[funcName]) {
            throw "接口" + funcName + "未實現"
        }
    })
    
    return self
}

$.createBaseEx = createBaseEx
$.getConfigureFunc = function(exName) {
    dicRegister = {
        "Futures_OKCoin" : funcConfigure_Futures_OKCoin,    // OK期貨的實現
        "Huobi" : funcConfigure_Huobi,
        "Futures_Binance" : funcConfigure_Futures_Binance,
        "Binance" : funcConfigure_Binance,
        "WexApp" : funcConfigure_WexApp,                    // wexApp的實現
    }
    return dicRegister
}

在模板中針對具體交易所實現編寫,例如以FMZ的模擬盤WexApp為例:

function funcConfigure_WexApp(self) {
    var formatSymbol = function(originalSymbol) {
        // BTC_USDT
        var arr = originalSymbol.split("_")
        var baseCurrency = arr[0]
        var quoteCurrency = arr[1]
        return [originalSymbol, baseCurrency, quoteCurrency]
    }

    self.interfaceGetTickers = function interfaceGetTickers() {
        self.routineGetTicker = HttpQuery_Go("https://api.wex.app/api/v1/public/tickers")
    }

    self.waitTickers = function waitTickers() {
        var ret = []
        var arr = JSON.parse(self.routineGetTicker.wait()).data
        _.each(arr, function(ele) {
            ret.push({
                bid1: parseFloat(ele.buy), 
                bid1Vol: parseFloat(-1),
                ask1: parseFloat(ele.sell), 
                ask1Vol: parseFloat(-1),
                symbol: formatSymbol(ele.market)[0],
                type: "Spot", 
                originalSymbol: ele.market
            })
        })
        return ret 
    }

    self.interfaceGetAcc = function interfaceGetAcc(symbol, updateTS) {
        if (self.updateAccsTS != updateTS) {
            self.routineGetAcc = self.e.Go("GetAccount")
        }
    }

    self.waitAcc = function waitAcc(symbol, updateTS) {
        var arr = formatSymbol(symbol)
        var ret = null 
        if (self.updateAccsTS != updateTS) {
            ret = self.routineGetAcc.wait().Info
            self.bufferGetAccRet = ret 
        } else {
            ret = self.bufferGetAccRet
        }
        if (!ret) {
            return null 
        }        
        var acc = {symbol: symbol, Stocks: 0, FrozenStocks: 0, Balance: 0, FrozenBalance: 0, originalInfo: ret}
        _.each(ret.exchange, function(ele) {
            if (ele.currency == arr[1]) {
                // baseCurrency
                acc.Stocks = parseFloat(ele.free)
                acc.FrozenStocks = parseFloat(ele.frozen)
            } else if (ele.currency == arr[2]) {
                // quoteCurrency
                acc.Balance = parseFloat(ele.free)
                acc.FrozenBalance = parseFloat(ele.frozen)
            }
        })
        return acc
    }

    self.interfaceGetPos = function interfaceGetPos(symbol, price, initSpAcc, nowSpAcc) {
        var symbolInfo = self.getSymbolInfo(symbol)
        var sumInitStocks = initSpAcc.Stocks + initSpAcc.FrozenStocks
        var sumNowStocks = nowSpAcc.Stocks + nowSpAcc.FrozenStocks
        var diffStocks = _N(sumNowStocks - sumInitStocks, symbolInfo.amountPrecision)
        if (Math.abs(diffStocks) < symbolInfo.min / price) {
            return []
        }
        return [{symbol: symbol, amount: diffStocks, price: null, originalInfo: {}}]
    }

    self.interfaceTrade = function interfaceTrade(symbol, type, price, amount) {
        var tradeType = ""
        if (type == self.OPEN_LONG || type == self.COVER_SHORT) {
            tradeType = "bid"
        } else {
            tradeType = "ask"
        }
        var params = {
            "market": symbol,
            "side": tradeType,
            "amount": String(amount),
            "price" : String(-1),
            "type" : "market"
        }
        self.routineTrade = self.e.Go("IO", "api", "POST", "/api/v1/private/order", self.encodeParams(params))
    }

    self.waitTrade = function waitTrade() {
        return self.routineTrade.wait()
    }

    self.calcAmount = function calcAmount(symbol, type, price, amount) {
        // 獲取交易對信息
        var symbolInfo = self.getSymbolInfo(symbol)
        if (!symbol) {
            throw symbol + ",交易對信息查詢不到"
        }
        var tradeAmount = null 
        var equalAmount = null  // 記錄幣數
        if (type == self.OPEN_LONG || type == self.COVER_SHORT) {
            tradeAmount = _N(amount * price, parseFloat(symbolInfo.pricePrecision))
            // 檢查最小交易量
            if (tradeAmount < symbolInfo.min) {
                Log(self.name, " tradeAmount:", tradeAmount, "小於", symbolInfo.min)
                return false 
            }
            equalAmount = tradeAmount / price
        } else {
            tradeAmount = _N(amount, parseFloat(symbolInfo.amountPrecision))
            // 檢查最小交易量
            if (tradeAmount < symbolInfo.min / price) {
                Log(self.name, " tradeAmount:", tradeAmount, "小於", symbolInfo.min / price)
                return false 
            }
            equalAmount = tradeAmount
        }
        return [tradeAmount, equalAmount]
    }

    self.init = function init() {   // 自動處理精度等條件的函數
        var ret = JSON.parse(HttpQuery("https://api.wex.app/api/v1/public/markets"))
        _.each(ret.data, function(symbolInfo) {
            self.symbolsInfo.push({
                symbol: symbolInfo.pair,
                amountPrecision: parseFloat(symbolInfo.basePrecision),
                pricePrecision: parseFloat(symbolInfo.quotePrecision),
                multiplier: 1,
                min: parseFloat(symbolInfo.minQty),
                originalInfo: symbolInfo
            })
        })        
    }
}

然后策略中使用這個模板就很簡單了:

function main() {
    var fuExName = exchange.GetName()
    var fuConfigureFunc = $.getConfigureFunc()[fuExName]
    var ex = $.createBaseEx(exchange, fuConfigureFunc)

    var arrTestSymbol = ["LTC_USDT", "ETH_USDT", "EOS_USDT"]
    var ts = new Date().getTime()
    
    // 測試獲取行情
    ex.goGetTickers()
    var tickers = ex.getTickers()
    Log("tickers:", tickers)
    
    // 測試獲取賬戶信息
    ex.goGetAcc(symbol, ts)
    
    _.each(arrTestSymbol, function(symbol) {        
        _.each(tickers, function(ticker) {
            if (symbol == ticker.originalSymbol) {
                // 打印行情數據
                Log(symbol, ticker)
            }
        })

        // 打印資產數據
        var acc = ex.getAcc(symbol, ts)
        Log("acc:", acc.symbol, acc)
    })
}

策略實盤

基於以上模板設計編寫策略就很簡單了,整個策略大約300+行,實現了一個數字貨幣現貨多品種網格策略。

目前虧錢T_T,源碼就暫時不發了。發幾個注冊碼,有興趣的可以掛wexApp玩下:

購買地址: https://www.fmz.com/m/s/284507
注冊碼: 
adc7a2e0a2cfde542e3ace405d216731
f5db29d05f57266165ce92dc18fd0a30
1735dca92794943ddaf277828ee04c27
0281ea107935015491cda2b372a0997d
1d0d8ef1ea0ea1415eeee40404ed09cc

就200多U,剛跑起來,就遇到一個大單邊行情,慢慢回血。現貨網格的最大優點就是:“能睡着覺!”。穩定性還湊合,從5月27日到現在沒動過它,期貨網格暫時還不敢嘗試。


免責聲明!

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



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