
La stratégie de grille perpétuelle est une stratégie classique populaire sur la plateforme. Par rapport à la grille spot, il n’est pas nécessaire de détenir des pièces et un effet de levier peut être ajouté, ce qui est beaucoup plus pratique que la grille spot. Cependant, comme il est impossible d’effectuer des backtests directement sur la plateforme quantitative Inventor, elle n’est pas propice au filtrage des devises et à la détermination de l’optimisation des paramètres. Cet article présentera le processus complet de backtesting Python, y compris la collecte de données, le cadre de backtesting, la fonction de backtesting, l’optimisation des paramètres, etc. Vous pouvez l’essayer vous-même dans le notebook Juypter.
En règle générale, les données de la ligne K sont suffisantes. Pour des raisons de précision, plus la période de la ligne K est courte, mieux c’est. Cependant, le temps de backtest et le volume de données doivent être équilibrés. Cet article utilise des données de 5 minutes des deux dernières années pour le backtesting . Le volume de données final dépasse 200 000 lignes. Choisissez DYDX. Bien entendu, la devise spécifique et la période K-line peuvent être sélectionnées en fonction de vos propres intérêts.
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()
Le backtest continue d’utiliser le cadre couramment utilisé auparavant qui prend en charge plusieurs devises de contrats perpétuels USDT, ce qui est simple et facile à utiliser.
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)
Le principe de la stratégie de grille est très simple : vendre lorsque le prix monte et acheter lorsque le prix baisse. Elle fait intervenir trois paramètres spécifiques : le prix initial, l’espacement de la grille et la valeur de la transaction. Les fluctuations du marché de DYDX sont très importantes. Il est passé de 8,6 U à 1 U, puis est remonté à 3 U lors du récent marché haussier. Le prix initial par défaut de la stratégie est de 8,6 U, ce qui est très défavorable pour le réseau. stratégie, mais le paramètre par défaut backtest Le bénéfice total en deux ans était de 9 200 U, et il y a eu une perte de 7 500 U à un moment donné.

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

Le réglage du prix initial affecte la position initiale de la stratégie. Le prix initial par défaut du backtest actuel est le prix initial au démarrage, c’est-à-dire qu’aucune position n’est détenue au démarrage. Nous savons que la stratégie de grille réalisera tous les bénéfices lorsque le prix reviendra au niveau initial, donc si la stratégie peut prédire correctement le marché futur lors de son lancement, elle augmentera considérablement les bénéfices. Ici, nous fixons le prix initial à 3U, puis effectuons un backtest. Le drawdown maximum final était de 9200U et le bénéfice final était de 13372U. La stratégie finale ne conserve pas de positions. Ce profit est le profit de la volatilité, et la différence entre le profit des paramètres par défaut et ce profit est la perte de position causée par un jugement inexact du prix final.
Cependant, si le prix initial est fixé à 3U, la stratégie sera short au début et détiendra un grand nombre de positions short. Dans cet exemple, une position short de 17 000 U est directement détenue, elle est donc confrontée à des risques plus importants.

L’espacement de la grille détermine la distance entre les ordres en attente. Évidemment, plus l’espacement est petit, plus les transactions sont fréquentes, plus le bénéfice d’une transaction unique est faible et plus les frais de traitement sont élevés. Mais il convient de noter que lorsque l’espacement de la grille devient plus petit et que la valeur de la grille reste inchangée, la position totale augmentera lorsque le prix changera, et les risques encourus seront complètement différents. Par conséquent, pour tester rétrospectivement l’effet de l’espacement de la grille, il est nécessaire de convertir la valeur de la grille.
Étant donné que le backtest utilise des données de ligne de 5 000 mK et qu’une seule transaction est effectuée sur une ligne K. Cela n’est évidemment pas conforme à la réalité, d’autant plus que la volatilité des monnaies numériques est très importante. Un espacement plus petit fera manquer de nombreuses transactions lors des backtests par rapport aux transactions réelles. Seul un espacement plus grand aura une valeur de référence. Dans le cadre de ce mécanisme de backtesting, les conclusions tirées ne sont pas exactes. Grâce aux tests rétrospectifs des données de flux d’ordres au niveau des ticks, l’espacement optimal de la grille devrait être de 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
Comme mentionné précédemment, lorsque la volatilité est la même, plus la valeur détenue est élevée, plus la méthode proportionnelle au risque est élevée. Cependant, tant qu’il ne s’agit pas d’une baisse rapide, 1 % du total des fonds combinés à 1 % de l’espacement de la grille devrait être capable de faire face à la plupart des conditions du marché. Dans cet exemple DYDX, une chute de près de 90 % a également déclenché un appel de marge. Mais attention, le DYDX est principalement en baisse. Lorsqu’il baisse, la stratégie de grille devient longue et la baisse maximale est de 100 %. Cependant, il n’y a pas de limite à la hausse et le risque est beaucoup plus élevé. Par conséquent, la stratégie de grille recommande aux utilisateurs de choisir les devises qui, selon eux, ont du potentiel et de n’investir qu’à long terme.
Le prix de régression est le prix initial. La différence entre le prix actuel et le prix initial ainsi que la taille de la grille déterminent la position à conserver. Si le prix de régression est fixé plus haut que le prix actuel, la stratégie de grille sera longue et vice versa. Le prix de régression par défaut est le prix auquel la stratégie a été démarrée. Afin de réduire le risque, il est recommandé d’utiliser une grille longue uniquement. Une idée naturelle est de savoir s’il est possible de modifier le prix de régression de sorte que même si le prix augmente, vous puissiez toujours conserver des positions longues sans avoir à les ajuster toi-même. Prenons l’exemple du marché du BTC cette année : il est passé de 15 000 au début de l’année à 43 000 à la fin de l’année. Si vous commencez à exécuter la stratégie de grille dès le début de l’année, le prix de retour par défaut est de 15 000, et vous effectuez à la fois des transactions longues et courtes. Les rendements spécifiques sont indiqués dans la figure ci-dessous. Vous perdrez de l’argent tout au long du processus, car le BTC se lève. Même si le mode long only n’est pas configuré correctement, le prix peut rapidement dépasser la limite sans conserver de position.

Tout d’abord, lorsque la stratégie est lancée, le prix de régression est fixé à 1,6 fois le prix au moment du démarrage. De cette façon, la stratégie de grille traitera le prix comme s’il tombait de 1,6 fois le prix actuel et commencera à maintenir le long terme position causée par cette partie de la différence de prix. Si le prix ultérieur dépasse Lorsque le prix de retour est /1,6, le prix initial est réinitialisé, de sorte qu’au moins 60 % de la différence soit toujours disponible pour les positions longues. Les résultats du backtest sont les suivants :

Bien entendu, si vous êtes plus optimiste quant au marché, vous pouvez définir ce ratio sur une valeur plus élevée et le bénéfice final augmentera en conséquence. Bien entendu, si le marché chute, ce paramètre augmentera également le risque de conserver des positions.