Strategi grid kekal adalah strategi klasik yang popular di platform FMZ. Berbanding dengan grid spot, tidak perlu mempunyai mata wang, dan leverage boleh ditambah, yang jauh lebih mudah daripada grid spot. Walau bagaimanapun, kerana tidak mungkin untuk backtest di Platform FMZ Quant secara langsung, ia tidak kondusif untuk menyaring mata wang dan menentukan pengoptimuman parameter. Dalam artikel ini, kami akan memperkenalkan proses backtesting Python lengkap, termasuk pengumpulan data, kerangka kerja backtesting, fungsi backtesting, pengoptimuman parameter, dll.
Secara amnya, cukup untuk menggunakan data K-line. Untuk ketepatan, semakin kecil tempoh K-line, semakin baik. Walau bagaimanapun, untuk mengimbangi masa backtest dan jumlah data, dalam artikel ini, kami menggunakan 5min data dari dua tahun yang lalu untuk backtesting. Jumlah data akhir melebihi 200,000 baris. Kami memilih DYDX sebagai mata wang. Sudah tentu, mata wang tertentu dan tempoh K-line boleh dipilih mengikut minat anda sendiri.
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()
Untuk backtesting, kami terus memilih kerangka yang biasa digunakan yang menyokong kontrak kekal USDT dalam pelbagai mata wang, yang mudah dan mudah digunakan.
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)
Prinsip strategi grid adalah sangat mudah. Jual apabila harga naik dan beli apabila harga turun. Ia secara khusus melibatkan tiga parameter: harga awal, jarak grid, dan nilai dagangan. Pasaran DYDX turun naik sangat. Ia jatuh dari tahap rendah awal 8.6U ke 1U, dan kemudian naik kembali ke 3U dalam pasaran bull baru-baru ini. Harga awal lalai strategi adalah 8.6U, yang sangat tidak menguntungkan untuk strategi grid, tetapi parameter lalai yang diuji semula, keuntungan keseluruhan 9200U dibuat dalam dua tahun, dan kerugian 7500U dibuat dalam tempoh itu.
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
Tetapan harga awal mempengaruhi kedudukan awal strategi. Harga awal lalai untuk backtest sekarang adalah harga awal pada permulaan, iaitu tidak ada kedudukan yang dipegang pada permulaan. Dan kita tahu bahawa strategi grid akan merealisasikan semua keuntungan apabila harga kembali ke peringkat awal, jadi jika strategi dapat meramalkan pasaran masa depan dengan betul apabila ia dilancarkan, pendapatan akan meningkat dengan ketara. Di sini, kita menetapkan harga awal kepada 3U dan kemudian backtest. Pada akhirnya, penarikan maksimum adalah 9200U, dan keuntungan akhir adalah 13372U. Strategi akhir tidak memegang kedudukan. Keuntungan adalah semua keuntungan turun naik, dan perbezaan antara keuntungan parameter lalai adalah kerugian kedudukan yang disebabkan oleh penghakiman harga akhir yang tidak tepat.
Walau bagaimanapun, jika harga awal ditetapkan kepada 3U, strategi akan pergi pendek pada mulanya dan memegang sebilangan besar kedudukan pendek.
Jarak grid menentukan jarak antara pesanan yang menunggu. Jelas, semakin kecil jarak, semakin kerap transaksi, semakin rendah keuntungan satu transaksi, dan semakin tinggi yuran pengendalian. Walau bagaimanapun, perlu diperhatikan bahawa apabila jarak grid menjadi lebih kecil dan nilai grid tetap tidak berubah, apabila harga berubah, jumlah kedudukan akan meningkat, dan risiko yang dihadapi sama sekali berbeza. Oleh itu, untuk menguji semula kesan jarak grid, perlu menukar nilai grid.
Oleh kerana backtest menggunakan data 5m K-line, dan setiap K-line hanya diperdagangkan sekali, yang jelas tidak realistik, terutamanya kerana turun naik mata wang digital sangat tinggi. Jarak yang lebih kecil akan terlepas banyak transaksi dalam backtesting berbanding dengan perdagangan langsung. Hanya jarak yang lebih besar yang akan mempunyai nilai rujukan. Dalam mekanisme backtesting ini, kesimpulan yang diambil tidak tepat. Melalui backtesting aliran data pesanan tahap tik, jarak grid optimum harus 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
Seperti yang disebutkan sebelum ini, apabila turun naik adalah sama, semakin besar nilai pegangan, risiko adalah sebanding. Walau bagaimanapun, selagi tidak ada penurunan pesat, 1% daripada jumlah dana dan 1% dari jarak grid harus dapat mengatasi kebanyakan keadaan pasaran. Dalam contoh DYDX ini, penurunan hampir 90% juga mencetuskan pembubaran. Walau bagaimanapun, perlu diperhatikan bahawa DYDX terutamanya jatuh. Apabila strategi grid pergi lama apabila jatuh, ia akan jatuh sebanyak 100% paling banyak, sementara tidak ada had untuk kenaikan, dan risiko jauh lebih tinggi. Oleh itu, Strategi Grid mengesyorkan pengguna untuk memilih hanya mod kedudukan panjang untuk mata wang yang mereka percayai berpotensi.