La stratégie de grille perpétuelle est une stratégie classique populaire sur la plate-forme FMZ. Par rapport à la grille au comptant, il n'est pas nécessaire d'avoir des devises et un effet de levier peut être ajouté, ce qui est beaucoup plus pratique que la grille au comptant. Cependant, comme il n'est pas possible de faire des backtests sur la plate-forme FMZ Quant directement, il n'est pas propice à la sélection des devises et à la détermination de l'optimisation des paramètres.
En règle générale, il suffit d'utiliser des données de ligne K. Pour plus de précision, plus la période de ligne K est petite, mieux c'est. Cependant, pour équilibrer le temps de backtest et le volume des données, dans cet article, nous utilisons 5 minutes de données des deux dernières années pour le backtest. Le volume final des données a dépassé 200 000 lignes. Nous choisissons DYDX comme devise. Bien sûr, la devise spécifique et la période de ligne K 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()
Pour le backtesting, nous continuons à choisir le cadre couramment utilisé qui prend en charge les contrats perpétuels USDT dans plusieurs devises, 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 #Initial assets
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 #Deduction of handling fee
self.account['USDT']['fee'] += price*amount*self.fee
self.account[symbol]['fee'] += price*amount*self.fee
if cover_amount > 0: #Close the position first.
self.account['USDT']['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount #Profits
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): #Updating of assets
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 la grille est très simple. Vendre lorsque le prix augmente et acheter lorsque le prix baisse. Il implique spécifiquement trois paramètres: prix initial, espacement de la grille et valeur de négociation. Le marché de DYDX fluctue considérablement. Il est tombé du plus bas initial de 8,6U à 1U, puis est remonté à 3U dans le récent marché haussier. Le prix initial par défaut de la stratégie est de 8,6U, ce qui est très défavorable pour la stratégie de la grille, mais les paramètres par défaut testés en arrière un profit total de 9200U a été réalisé en deux ans, et une perte de 7500U a été réalisée au cours de la période.
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 storing intermediate results
for row in df.iterrows():
kline = row[1] #To backtest a K-line will only generate one buy order or one sell order, which is not particularly accurate.
buy_price = (value / pct - value) / ((value / pct) / init_price + e.account[symbol]['amount']) #The buy order price, as it is a pending order transaction, is also the final aggregated price
sell_price = (value / pct + value) / ((value / pct) / init_price + e.account[symbol]['amount'])
if kline.low < buy_price: #The lowest price of the K-line is lower than the current pending order price, the buy order is filled
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
La définition du prix initial affecte la position initiale de la stratégie. Le prix initial par défaut pour le backtest à l'instant est le prix initial au démarrage, c'est-à-dire qu'aucune position n'est détenue au démarrage. Et nous savons que la stratégie de grille réalisera tous les profits lorsque le prix reviendra au stade initial, donc si la stratégie peut prédire correctement le marché futur lorsqu'elle est lancée, les revenus seront considérablement améliorés. Ici, nous définissons le prix initial à 3U puis le backtest. À la fin, le tirage maximal était de 9200U, et le profit final était de 13372U. La stratégie finale ne détient pas de positions. Le profit est tous les profits de fluctuation, et la différence entre les profits des paramètres par défaut 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 tiendra un grand nombre de positions courtes.
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 profit d'une seule transaction est faible et plus les frais de traitement sont élevés. Cependant, il convient de noter qu'à mesure que l'espacement de la grille devient plus petit et que la valeur de la grille reste inchangée, lorsque le prix change, les positions totales augmenteront et les risques rencontrés sont complètement différents. Par conséquent, pour vérifier l'effet de l'espacement de la grille, il est nécessaire de convertir la valeur de la grille.
Comme le backtest utilise des données de 5m K-line, et chaque K-line n'est négociée qu'une seule fois, ce qui est évidemment irréaliste, d'autant plus que la volatilité des monnaies numériques est très élevée. Un espacement plus petit manquera de nombreuses transactions dans le backtesting par rapport au trading en direct. Seul un espacement plus grand aura une valeur de référence. Dans ce mécanisme de backtesting, les conclusions tirées ne sont pas exactes.
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 les fluctuations sont les mêmes, plus la valeur de la participation est grande, le risque est proportionnel. Cependant, tant qu'il n'y a pas de baisse rapide, 1% des fonds totaux et 1% de l'espacement de la grille devraient être en mesure de faire face à la plupart des conditions du marché. Dans cet exemple de DYDX, une baisse de près de 90% a également déclenché une liquidation. Cependant, il convient de noter que DYDX tombe principalement. Lorsque la stratégie de grille va long quand elle tombe, elle tombera de 100% au maximum, alors qu'il n'y a pas de limite à la hausse, et le risque est beaucoup plus élevé. Par conséquent, Grid Strategy recommande aux utilisateurs de ne choisir que le mode de position longue pour les devises qu'ils croient potentielles.