Detaillierte Erläuterung der Optimierung von Perpetual Contract Grid Strategy Parametern

Schriftsteller:Lydia., Erstellt: 2023-12-11 15:04:25, aktualisiert: 2024-01-02 21:24:31

img

Die Perpetual Grid-Strategie ist eine beliebte klassische Strategie auf der FMZ-Plattform. Im Vergleich zum Spot-Grid ist es nicht notwendig, Währungen zu haben, und Hebelwirkung kann hinzugefügt werden, was viel bequemer ist als das Spot-Grid. Da es jedoch nicht möglich ist, auf der FMZ Quant-Plattform direkt zu backtesten, ist es nicht förderlich, Währungen zu screenen und Parameteroptimierung zu bestimmen. In diesem Artikel stellen wir den kompletten Python-Backtesting-Prozess vor, einschließlich Datenerhebung, Backtesting-Framework, Backtesting-Funktionen, Parameteroptimierung usw. Sie können es selbst in juypter Notebook ausprobieren.

Datenerhebung

Im Allgemeinen reicht es aus, K-Liniendaten zu verwenden. Je kleiner der K-Linienzeitraum, desto besser. Um jedoch die Backtestzeit und das Datenvolumen auszugleichen, verwenden wir in diesem Artikel 5min Daten aus den letzten zwei Jahren für das Backtesting. Das endgültige Datenvolumen überstieg 200.000 Zeilen. Wir wählen DYDX als Währung. Natürlich können die spezifische Währung und die K-Linienzeit nach Ihren eigenen Interessen ausgewählt werden.

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()

Backtesting-Rahmen

Für Backtesting wählen wir weiterhin das allgemein verwendete Framework, das USDT-Perpetual Contracts in mehreren Währungen unterstützt, das einfach und einfach zu bedienen ist.

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)

Funktion für die Gitter-Backtestprüfung

Das Prinzip der Gitterstrategie ist sehr einfach. Verkaufen, wenn der Preis steigt, und kaufen, wenn der Preis fällt. Es beinhaltet speziell drei Parameter: Anfangspreis, Gitterabstand und Handelswert. Der Markt von DYDX schwankt stark. Er fiel von dem anfänglichen Tief von 8,6U auf 1U und stieg dann im jüngsten Bullenmarkt wieder auf 3U. Der Standard-Anfängspreis der Strategie beträgt 8,6U, was für die Gitterstrategie sehr ungünstig ist, aber die Standardparameter backtested ein Gesamtgewinn von 9200U wurde in zwei Jahren erzielt, und ein Verlust von 7500U wurde während des Zeitraums gemacht.

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 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

img

Erste Auswirkungen auf die Preise

Die Einstellung des Anfangspreises beeinflusst die Anfangsposition der Strategie. Der Standard-Anfangspreis für den Backtest ist gerade jetzt der Anfangspreis beim Starten, dh keine Position wird beim Starten gehalten. Und wir wissen, dass die Gitterstrategie alle Gewinne erzielt, wenn der Preis in die Anfangsphase zurückkehrt, so dass, wenn die Strategie den zukünftigen Markt richtig vorhersagen kann, wenn sie gestartet wird, das Einkommen erheblich verbessert wird. Hier setzen wir den Anfangspreis auf 3U und dann Backtest. Am Ende war der maximale Drawdown 9200U, und der endgültige Gewinn war 13372U. Die endgültige Strategie hält keine Positionen. Der Gewinn ist der gesamte Schwankungsgewinn, und die Differenz zwischen den Gewinnen der Standardparameter ist der Positionsverlust, der durch ungenaue Beurteilung des endgültigen Preises verursacht wird.

Wenn der Anfangspreis jedoch auf 3 U gesetzt wird, wird die Strategie zu Beginn kurz gehen und eine große Anzahl von Short-Positionen halten.

img

Einstellungen für Rasterabstand

Der Abstand zwischen den ausstehenden Aufträgen wird durch den Raster bestimmt. Je kleiner der Abstand, desto häufiger die Transaktionen, desto geringer der Gewinn einer einzelnen Transaktion und desto höher die Bearbeitungsgebühr. Es ist jedoch erwähnenswert, dass der Rasterabstand kleiner wird und der Rasterwert unverändert bleibt, wenn sich der Preis ändert, die Gesamtpositionen zunehmen und die Risiken völlig unterschiedlich sind. Um den Effekt des Rasterabstands zu überprüfen, ist es notwendig, den Rasterwert umzuwandeln.

Da der Backtest 5m K-Liniendaten verwendet und jede K-Line nur einmal gehandelt wird, was offensichtlich unrealistisch ist, insbesondere da die Volatilität digitaler Währungen sehr hoch ist. Ein kleinerer Abstand wird im Vergleich zum Live-Handel viele Transaktionen beim Backtesting verpassen. Nur ein größerer Abstand hat einen Referenzwert. In diesem Backtesting-Mechanismus sind die gezogenen Schlussfolgerungen nicht genau. Durch das Tick-Level-Orderflow-Daten-Backtesting sollte der optimale Gitterabstand 0,005-0,01 sein.

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

Grid-Transaktionswert

Wie bereits erwähnt, ist das Risiko proportional, je größer der Wert der Beteiligung ist, wenn die Schwankungen gleich sind. Solange es jedoch keinen schnellen Rückgang gibt, sollten 1% der Gesamtfonds und 1% des Netzstandes in der Lage sein, mit den meisten Marktbedingungen fertig zu werden. In diesem DYDX-Beispiel löste ein Rückgang von fast 90% auch eine Liquidation aus. Es sollte jedoch beachtet werden, dass DYDX hauptsächlich fällt. Wenn die Netzstrategie lang geht, wenn sie fällt, fällt sie höchstens um 100%, während es kein Limit für den Anstieg gibt, und das Risiko ist viel höher. Daher empfiehlt die Grid Strategy den Benutzern, nur den Long-Positionsmodus für Währungen zu wählen, für die sie ein Potenzial haben.


Mehr