上篇我们一起动手做了一个简单的网格策略,本期文章我们把这个策略升级扩展,扩展成一个多品种现货网格策略,并让这个策略进行实战测试。目的并非要找到一个"圣杯",而是要从策略设计中探讨设计策略时的各种问题思考、解决思路。本篇会讲解我在设计这个策略时的一些经验、本篇内容略微复杂,需要对程序编写有一定基础。
基于策略需求的设计思考
本篇和上篇文章一样,依然基于发明者量化(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日到现在没动过它,期货网格暂时还不敢尝试。