上篇文章我们讲解到了一个简单网格策略的交易逻辑分析,本篇我们继续来完成这个教学策略的设计。
交易逻辑分析 上篇文章我们说到,只要遍历网格每个网格线,判断当前价格上穿下穿网格线即可触发交易动作。但是实际上逻辑细节还是有不少的,往往不了解策略编写的萌新们容易形成一个错误认知就是“逻辑非常简单,代码应该也就几行而已,实际编写起来发现细节还是很多的。”
首先我们要考虑的第一个细节就是,无限网格这方面的设计。还记得上篇文章我们一起设计了一个生成初始网格数据结构的函数createNet
么?这个函数是生成了一个网格线是有限个数的网格数据结构。那么如果在策略运行时,价格超出了这个网格数据结构的边界(超过最上边即价格最高、最下边即价格最低的网格线)呢?
所以我们首先要给网格数据结构增加延伸机制。
开始编写策略main函数,main函数就是策略开始执行的代码
var diff = 50 // 全局变量,网格间距,可以设计成参数,方便讲解,我们把这个参数写死在代码里。
function main() {
// 实盘开始运行后,从这里开始执行策略代码
var ticker = _C(exchange.GetTicker) // 获取市场最新的行情数据ticker,ticker这个数据的结构参看FMZ API文档:https://www.fmz.com/api#ticker
var net = createNet(ticker.Last, diff) // 我们上篇设计的初始构造网格数据结构的函数,这里构造一个网格数据结构net
while (true) { // 然后程序逻辑就进入了这个while死循环,策略执行到此将不停的循环执行这里{}符号之内的代码
ticker = _C(exchange.GetTicker) // 死循环代码部分的第一行,获取最新的行情数据,更新给ticker变量
// 检查网格范围
while (ticker.Last >= net[net.length - 1].price) {
net.push({
buy : false,
sell : false,
price : net[net.length - 1].price + diff,
})
}
while (ticker.Last <= net[0].price) {
var price = net[0].price - diff
if (price <= 0) {
break
}
net.unshift({
buy : false,
sell : false,
price : price,
})
}
// 还有其它代码...
}
}
让网格数据结构可以延伸就是这段代码(从上边代码中节选):
// 检查网格范围
while (ticker.Last >= net[net.length - 1].price) { // 如果价格超过网格最高价格的网格线
net.push({ // 就在网格最高价格的网格线之后加入一个新的网格线
buy : false, // 初始化卖出标记
sell : false, // 初始化买入标记
price : net[net.length - 1].price + diff, // 在之前最高价格的基础上再加一个网格间距
})
}
while (ticker.Last <= net[0].price) { // 如果价格低于网格最低价格的网格线
var price = net[0].price - diff // 区别于向上添加,要注意向下添加新网格线的价格不能小于等于0,所以这里要判断
if (price <= 0) { // 小于等于0就不添加了,跳出这层循环
break
}
net.unshift({ // 就在网格最低价格的网格线之前添加一个新的网格线
buy : false,
sell : false,
price : price,
})
}
接下来就要考虑如何具体实现交易触发。
var diff = 50
var amount = 0.002 // 增加一个全局变量,也可以设计成参数,当然为了简便讲解,我们也写死在策略代码,
// 这个参数控制每次网格线上触发交易时的交易量
function main() {
var ticker = _C(exchange.GetTicker)
var net = createNet(ticker.Last, diff)
var preTicker = ticker // 在主循环(死循环)开始前,设置一个变量,记录上一次的行情数据
while (true) {
ticker = _C(exchange.GetTicker)
// 检查网格范围
while (ticker.Last >= net[net.length - 1].price) {
net.push({
buy : false,
sell : false,
price : net[net.length - 1].price + diff,
})
}
while (ticker.Last <= net[0].price) {
var price = net[0].price - diff
if (price <= 0) {
break
}
net.unshift({
buy : false,
sell : false,
price : price,
})
}
// 检索网格
for (var i = 0 ; i < net.length ; i++) { // 遍历网格数据结构中的所有网格线
var p = net[i]
if (preTicker.Last < p.price && ticker.Last > p.price) { // 上穿,卖出,当前节点已经交易过不论SELL BUY ,都不再交易
if (i != 0) {
var downP = net[i - 1]
if (downP.buy) {
exchange.Sell(-1, amount, ticker)
downP.buy = false
p.sell = false
continue
}
}
if (!p.sell && !p.buy) {
exchange.Sell(-1, amount, ticker)
p.sell = true
}
} else if (preTicker.Last > p.price && ticker.Last < p.price) { // 下穿,买入
if (i != net.length - 1) {
var upP = net[i + 1]
if (upP.sell) {
exchange.Buy(-1, amount * ticker.Last, ticker)
upP.sell = false
p.buy = false
continue
}
}
if (!p.buy && !p.sell) {
exchange.Buy(-1, amount * ticker.Last, ticker)
p.buy = true
}
}
}
preTicker = ticker // 把当前的行情数据记录在preTicker中,在下一次循环中,作为“上一次”行情数据和最新的对比,判断上穿下穿
Sleep(500)
}
}
可以看到:
preTicker.Last < p.price && ticker.Last > p.price
preTicker.Last > p.price && ticker.Last < p.price
就是我们上篇所讲的:
上穿下穿只是判断可否下单交易的第一步,其中还需要判断网格线数据中的标记。
如果是上穿,就判断价格低于当前网格线并且最近的网格线上的buy标记,如果buy标记的值为true,则说明上一根网格线买入过,就重置上一根的buy标记为false,重置当前网格线sell标记为false。
判断完刚才的条件,如果没有触发则继续判断,如果当前网格线上buy/sell标记均为false,则说明当前网格线可以交易,由于是上穿,我们这里执行卖出操作,执行之后标记当前网格线sell标记true。
下穿处理逻辑相同(这里留给萌新们思考思考)。
为了可以看到一些回测时的数据,编写了一个函数showTbl
显示数据。
function showTbl(arr) {
var tbl = {
type : "table",
title : "网格",
cols : ["网格信息"],
rows : []
}
var arrReverse = arr.slice(0).reverse()
_.each(arrReverse, function(ele) {
var color = ""
if (ele.buy) {
color = "#FF0000"
} else if (ele.sell) {
color = "#00FF00"
}
tbl.rows.push([JSON.stringify(ele) + color])
})
LogStatus(_D(), "\n`" + JSON.stringify(tbl) + "`", "\n 账户信息:", exchange.GetAccount())
}
完整策略代码:
/*backtest
start: 2021-04-01 22:00:00
end: 2021-05-22 00:00:00
period: 1d
basePeriod: 1m
exchanges: [{"eid":"OKEX","currency":"ETH_USDT","balance":100000}]
*/
var diff = 50
var amount = 0.002
function createNet(begin, diff) {
var oneSideNums = 10
var up = []
var down = []
for (var i = 0 ; i < oneSideNums ; i++) {
var upObj = {
buy : false,
sell : false,
price : begin + diff / 2 + i * diff,
}
up.push(upObj)
var j = (oneSideNums - 1) - i
var downObj = {
buy : false,
sell : false,
price : begin - diff / 2 - j * diff,
}
if (downObj.price <= 0) { // 价格不能小于等于0
continue
}
down.push(downObj)
}
return down.concat(up)
}
function showTbl(arr) {
var tbl = {
type : "table",
title : "网格",
cols : ["网格信息"],
rows : []
}
var arrReverse = arr.slice(0).reverse()
_.each(arrReverse, function(ele) {
var color = ""
if (ele.buy) {
color = "#FF0000"
} else if (ele.sell) {
color = "#00FF00"
}
tbl.rows.push([JSON.stringify(ele) + color])
})
LogStatus(_D(), "\n`" + JSON.stringify(tbl) + "`", "\n 账户信息:", exchange.GetAccount())
}
function main() {
var ticker = _C(exchange.GetTicker)
var net = createNet(ticker.Last, diff)
var preTicker = ticker
while (true) {
ticker = _C(exchange.GetTicker)
// 检查网格范围
while (ticker.Last >= net[net.length - 1].price) {
net.push({
buy : false,
sell : false,
price : net[net.length - 1].price + diff,
})
}
while (ticker.Last <= net[0].price) {
var price = net[0].price - diff
if (price <= 0) {
break
}
net.unshift({
buy : false,
sell : false,
price : price,
})
}
// 检索网格
for (var i = 0 ; i < net.length ; i++) {
var p = net[i]
if (preTicker.Last < p.price && ticker.Last > p.price) { // 上穿,卖出,当前节点已经交易过不论SELL BUY ,都不再交易
if (i != 0) {
var downP = net[i - 1]
if (downP.buy) {
exchange.Sell(-1, amount, ticker)
downP.buy = false
p.sell = false
continue
}
}
if (!p.sell && !p.buy) {
exchange.Sell(-1, amount, ticker)
p.sell = true
}
} else if (preTicker.Last > p.price && ticker.Last < p.price) { // 下穿,买入
if (i != net.length - 1) {
var upP = net[i + 1]
if (upP.sell) {
exchange.Buy(-1, amount * ticker.Last, ticker)
upP.sell = false
p.buy = false
continue
}
}
if (!p.buy && !p.sell) {
exchange.Buy(-1, amount * ticker.Last, ticker)
p.buy = true
}
}
}
showTbl(net)
preTicker = ticker
Sleep(500)
}
}
策略回测:
可以看到网格策略的特点,遇到有趋势行情的时候会有较大浮亏,震荡行情下收益才会回升。 所以网格策略并非无风险,现货策略尚可“躺平”硬撑,期货合约网格策略则风险更大,需要对于网格参数偏保守设置。
husr12345 这是c++语言吧
tony233 为啥判断上穿下穿条件的时候,每根网格线都要判断啊,这里面感觉有个逻辑漏洞啊,不应该是上穿卖出的时候只要遍历高于目前价格的网格线吗? 还有exchange.Sell(-1, amount, ticker)这个函数怎么和api文档里的不一样啊,我看api文档里写的是exchange.Sell(Price, Amount),为啥你有三个参数啊,搞不懂啊,好复杂啊,我人都晕了~
tony233 好难啊
hawl 上穿和下跌时,exchange.Buy(-1, amount * ticker.Last, ticker),amount*ticker.Last是啥意思,为啥sell没有呢?
CYZWX https://www.fmz.com/strategy/291160 last_tick = [] line = [] grid_buy_list = [] def net(now_price): global line print(now_price) line = [now_price*(1+0.003*i) for i in range(-1000,1000)] Log(line) def ontick(): global last_tick global line global grid_buy_list account = exchange.GetAccount() ticker = exchange.GetTicker() last_tick.append(ticker['Last']) if len(last_tick) == 1:return elif len(last_tick) == 100:del last_tick[0] for i in range(len(line)): if last_tick[-1] > line[i] and last_tick[-2] < line[i] and len(grid_buy_list)!= 0 and i > min(grid_buy_list) and account['Stocks'] >= 0.001: exchange.Sell(last_tick[-1],0.01) del grid_buy_list[grid_buy_list.index(min(grid_buy_list))] Log(exchange.GetAccount()) elif last_tick[-1] < line[i] and last_tick[-2] > line[i] and i not in grid_buy_list: exchange.Buy(last_tick[-1],0.01) grid_buy_list.append(i) Log(exchange.GetAccount()) def main(): net(exchange.GetTicker()['Last']) Log(exchange.GetAccount()) while(True): ontick() Sleep(1000)
CYZWX 感谢梦神,讲的好详细,去重买入都解释了,仿着写了个py版
发明者量化-小小梦 策略是```JavaScript```语言。
tony233 文中的永续合约不算期货吗?
发明者量化-小小梦 期货都是合约张数, 现货市价单买单才是金额。现货卖单都是币数。
tony233 大佬,再问一个问题,这个啥意思啊 。注意:需要交易所的下单接口支持市价单(下单类型为买单时,下单量参数为计价币为单位的金额)。数字货币期货市价单方式下单,下单量参数的单位为合约张数。 我看你这篇帖子回测的是okex的eth的usdt永续合约,这不算期货市价单的下单方式吗?为啥买单的下单参数不是合约张数呢?
tony233 哦,我明白了
发明者量化-小小梦 FMZ的API函数中可以产生日志输出的函数例如:Log(...)、exchange.Buy(Price, Amount)、exchange.CancelOrder(Id)等都可以在必要参数后跟一些附带输出参数。https://www.fmz.com/api#exchange.cancelorderid