永续合约网格策略参数优化详解

Author: 小草, Created: 2023-12-08 17:00:38, Updated: 2023-12-14 17:07:42

img

永续网格策略是平台一个很受欢迎的经典策略。与现货网格相比不用持币、可加杠杆,比现货网格方便不少。但由于无法在发明者量化平台直接回测,不利于筛选币种和确定参数优化,本文将介绍下完整的Python回测流程,包含数据收集、回测框架、回测函数、参数优化等各个方面的内容,可以自己在juypter notebook中自行尝试。

数据收集

一般用K线数据就够了,为了精度,K线周期越小越好,但平衡回测时间和数据量,本文使用5min最近两年的数据进行回测,最终数据量超过了20W行,币种选择DYDX。当然具体的币种和K线周期可以根据自己的兴趣选择。

import requests
from datetime import date,datetime
import time
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import requests, zipfile, io
%matplotlib inline

def GetKlines(symbol='BTC',start='2020-8-10',end='2021-8-10',period='1h'):
    Klines = []
    start_time = int(time.mktime(datetime.strptime(start, "%Y-%m-%d").timetuple()))*1000
    end_time = int(time.mktime(datetime.strptime(end, "%Y-%m-%d").timetuple()))*1000
    while start_time < end_time:
        res = requests.get('https://fapi.binance.com/fapi/v1/klines?symbol=%sUSDT&interval=%s&startTime=%s&limit=1000'%(symbol,period,start_time))
        res_list = res.json()
        Klines += res_list
        start_time = res_list[-1][0]
    return pd.DataFrame(Klines,columns=['time','open','high','low','close','amount','end_time','volume','count','buy_amount','buy_volume','null']).astype('float')

df = GetKlines(symbol='DYDX',start='2022-1-1',end='2023-12-7',period='5m')
df = df.drop_duplicates()

回测框架

回测继续选择以前常用的支持USDT永续合约多币种的框架,简单好用。

class Exchange:
    
    def __init__(self, trade_symbols, fee=0.0004, initial_balance=10000):
        self.initial_balance = initial_balance #初始的资产
        self.fee = fee
        self.trade_symbols = trade_symbols
        self.account = {'USDT':{'realised_profit':0, 'unrealised_profit':0, 'total':initial_balance, 'fee':0}}
        for symbol in trade_symbols:
            self.account[symbol] = {'amount':0, 'hold_price':0, 'value':0, 'price':0, 'realised_profit':0,'unrealised_profit':0,'fee':0}
            
    def Trade(self, symbol, direction, price, amount):
        
        cover_amount = 0 if direction*self.account[symbol]['amount'] >=0 else min(abs(self.account[symbol]['amount']), amount)
        open_amount = amount - cover_amount
        self.account['USDT']['realised_profit'] -= price*amount*self.fee #扣除手续费
        self.account['USDT']['fee'] += price*amount*self.fee
        self.account[symbol]['fee'] += price*amount*self.fee

        if cover_amount > 0: #先平仓
            self.account['USDT']['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount  #利润
            self.account[symbol]['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount
            
            self.account[symbol]['amount'] -= -direction*cover_amount
            self.account[symbol]['hold_price'] = 0 if self.account[symbol]['amount'] == 0 else self.account[symbol]['hold_price']
            
        if open_amount > 0:
            total_cost = self.account[symbol]['hold_price']*direction*self.account[symbol]['amount'] + price*open_amount
            total_amount = direction*self.account[symbol]['amount']+open_amount
            
            self.account[symbol]['hold_price'] = total_cost/total_amount
            self.account[symbol]['amount'] += direction*open_amount
                    
    
    def Buy(self, symbol, price, amount):
        self.Trade(symbol, 1, price, amount)
        
    def Sell(self, symbol, price, amount):
        self.Trade(symbol, -1, price, amount)
        
    def Update(self, close_price): #对资产进行更新
        self.account['USDT']['unrealised_profit'] = 0
        for symbol in self.trade_symbols:
            self.account[symbol]['unrealised_profit'] = (close_price[symbol] - self.account[symbol]['hold_price'])*self.account[symbol]['amount']
            self.account[symbol]['price'] = close_price[symbol]
            self.account[symbol]['value'] = abs(self.account[symbol]['amount'])*close_price[symbol]
            self.account['USDT']['unrealised_profit'] += self.account[symbol]['unrealised_profit']
        self.account['USDT']['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + self.account['USDT']['unrealised_profit'],6)

网格回测函数

网格策略的原理很简单,涨了卖出,跌了买入,具体涉及三个参数:初始价格,网格间距,交易价值。DYDX的行情波动非常大,从最初的8.6U最低跌倒过1U,最近的牛市又涨回了3U,策略默认的初始价是8.6U,这对于网格策略是非常不利的,但默认参数回测两年共盈利9200U,期间一度亏了7500U。 img

symbol = 'DYDX'
value = 100
pct = 0.01

def Grid(fee=0.0002, value=100, pct=0.01, init = df.close[0]):
    e = Exchange([symbol], fee=0.0002, initial_balance=10000)
    init_price = init
    res_list = [] #用于储存中间结果
    for row in df.iterrows():
        kline = row[1] #这样会测一根K线只会产生一个买单或一个卖单,不是特别精确
        buy_price = (value / pct - value) / ((value / pct) / init_price + e.account[symbol]['amount']) #买单价格,由于是挂单成交,也是最终的撮合价格
        sell_price = (value / pct + value) / ((value / pct) / init_price + e.account[symbol]['amount'])
        if kline.low < buy_price: #K线最低价低于当前挂单价,买单成交
            e.Buy(symbol,buy_price,value/buy_price)
        if kline.high > sell_price:
            e.Sell(symbol,sell_price,value/sell_price)
        e.Update({symbol:kline.close})
        res_list.append([kline.time, kline.close, e.account[symbol]['amount'], e.account['USDT']['total']-e.initial_balance,e.account['USDT']['fee'] ])
    res = pd.DataFrame(data=res_list, columns=['time','price','amount','profit', 'fee'])
    res.index = pd.to_datetime(res.time,unit='ms')
    return res

img

初始价格的影响

初始价格的设置影响策略的初始持仓,刚才回测的默认初始价格是启动时的初始价格,即启动时不持仓。而我们知道网格策略会在价格返回初始时兑现所有利润,所以如果策略启动时能够对未来的行情有正确的预判,将会显著提高收益。这里设定初始价格为3U再回测下。最终最大回撤9200U, 最终收益13372U。最终策略不持仓,这个收益是所有的波动收益,而默认参数的收益与之相差的部分就是对最终价格判断不准带来的持仓亏损。

但初始价格设定为3U, 策略会在一开始就会做空而持有大量的空仓,这个例子中直接持有了1.7万U的空单,因此也面临者更大的风险。

img

网格间距设置

网格间距决定了挂单的距离,显然间距越小成交越频繁,单笔的利润越低,手续费也就越高。但值得注意,网格间距变小而网格价值不变,当价格发生变化时,总的持仓会增加,面临的风险完全不同。所以要回测出网格间距的作用,需要换算下网格价值。

由于回测采用的是5mK线数据,并且一根K线上只交易一次。这显然是不符合现实的,特别是数字货币波动率非常大,较小的间距在回测中和实盘相比会漏掉很多交易,只有加大的间距才有参考价值。在这种回测机制下,得出的结论并不准确。通过tick级订单流数据回测,最佳的网格间距应该在0.005-0.01。

for p in [0.0005, 0.001 ,0.002 ,0.005, 0.01, 0.02, 0.05]:
    res = Grid( fee=0.0002, value=value*p/0.01, pct=p, init =3)
    print(p, round(min(res['profit']),0), round(res['profit'][-1],0), round(res['fee'][-1],0))
    
0.0005 -8378.0 144.0 237.0
0.001 -9323.0 1031.0 465.0
0.002 -9306.0 3606.0 738.0
0.005 -9267.0 9457.0 781.0
0.01 -9228.0 13375.0 550.0
0.02 -9183.0 15212.0 309.0
0.05 -9037.0 16263.0 131.0

网格交易价值

前面讲过,当波动相同时,持有的价值越大,风险等比例方法,但只要不是急速的下跌,1%的总资金配合1%的网格间距应该能应付绝大多数行情。在本次的DYDX例子中,下跌了几乎90%也触发爆仓。但是注意的是DYDX主要是下跌,下跌时网格策略做多,最多也就跌100%,而上涨则是没有限制,风险高很多。因此网格策略推荐用户选择认为有潜力的币种只做多模式。

可变回归价格

回归价格也就是初始价格,当前价格和初始价格的差和网格大小决定了应该持有多少仓位,如果回归价格设置为高于当前价,网格策略会做多,反过来会做空。默认的回归价格是策略启动时的价格。为了降低风险,推荐只做多的网格,一个自然的想法是,能不能改变回归价格,使得即使价格上涨,仍然不断持有多仓,不用自行调节。以BTC今年的行情为例,从年初的15000上涨到年末的43000。如果从年初开始跑网格策略,回归价格默认15000,多空都做,具体收益如下图,随着BTC的上涨一路亏。即使只做多模式如果设置的不准确也会很快超出价格而没有持仓。 img

首先策略启动时,将回归价格设为启动时价格的1.6倍,这样网格策略会当作价格从1.6倍跌到当前价而开始持有这部分差价造成的多仓,如果后来的价格超过了回归价格/1.6 ,就重新设置下初始价格,这样始终保持最少60%的差价用于做多。回测结果如下: img

当然如果你更加看好市场,可以将这个比例设置的更大,最终的收益也会相应的提高,当然如果行情出现下跌,这种设置方式也增加了持仓风险。


More

EEEE fmz为什么不能直接回测网格策略呢?