Chiến lược lưới vĩnh cửu là một chiến lược cổ điển phổ biến trên nền tảng FMZ. So với lưới giao ngay, không cần phải có tiền tệ và có thể thêm đòn bẩy, thuận tiện hơn nhiều so với lưới giao ngay. Tuy nhiên, vì không thể kiểm tra lại trên nền tảng FMZ Quant trực tiếp, nó không thuận lợi cho việc sàng lọc tiền tệ và xác định tối ưu hóa tham số.
Thông thường, chỉ cần sử dụng dữ liệu K-line là đủ. Để chính xác, thời gian K-line càng nhỏ thì càng tốt. Tuy nhiên, để cân bằng thời gian backtest và khối lượng dữ liệu, trong bài viết này, chúng tôi sẽ sử dụng 5min dữ liệu từ hai năm qua để backtest. khối lượng dữ liệu cuối cùng vượt quá 200.000 dòng. Chúng tôi chọn DYDX làm tiền tệ. Tất nhiên, tiền tệ cụ thể và thời gian K-line có thể được chọn theo sở thích của bạn.
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()
Đối với backtesting, chúng tôi tiếp tục chọn khung thường được sử dụng hỗ trợ các hợp đồng vĩnh viễn USDT trong nhiều loại tiền tệ, đơn giản và dễ sử dụng.
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)
Nguyên tắc của chiến lược lưới là rất đơn giản. Bán khi giá tăng và mua khi giá giảm. Nó đặc biệt liên quan đến ba thông số: giá ban đầu, khoảng cách lưới và giá trị giao dịch. Thị trường của DYDX dao động rất nhiều. Nó giảm từ mức thấp ban đầu là 8,6U xuống 1U, và sau đó tăng trở lại 3U trong thị trường tăng gần đây. Giá ban đầu mặc định của chiến lược là 8,6U, rất không thuận lợi cho chiến lược lưới, nhưng các thông số mặc định được kiểm tra lại tổng lợi nhuận là 9200U được thực hiện trong hai năm, và lỗ 7500U được thực hiện trong giai đoạn này.
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
Việc thiết lập giá ban đầu ảnh hưởng đến vị trí ban đầu của chiến lược. Giá ban đầu mặc định cho backtest ngay bây giờ là giá ban đầu khi khởi động, nghĩa là không có vị trí nào được giữ tại thời điểm khởi động. Và chúng ta biết rằng chiến lược lưới sẽ nhận ra tất cả lợi nhuận khi giá trở lại giai đoạn ban đầu, vì vậy nếu chiến lược có thể dự đoán đúng thị trường tương lai khi nó được ra mắt, thu nhập sẽ được cải thiện đáng kể. Ở đây, chúng ta đặt giá ban đầu là 3U và sau đó backtest. Cuối cùng, mức rút tối đa là 9200U, và lợi nhuận cuối cùng là 13372U. Chiến lược cuối cùng không giữ vị trí. Lợi nhuận là tất cả lợi nhuận biến động, và sự khác biệt giữa lợi nhuận của các thông số mặc định là lỗ vị trí do đánh giá không chính xác về giá cuối cùng.
Tuy nhiên, nếu giá ban đầu được thiết lập là 3U, chiến lược sẽ đi ngắn ở đầu và giữ một số lượng lớn các vị trí ngắn.
Khoảng cách lưới xác định khoảng cách giữa các lệnh đang chờ. Rõ ràng, khoảng cách càng nhỏ, giao dịch càng thường xuyên, lợi nhuận của một giao dịch càng thấp và phí xử lý càng cao. Tuy nhiên, cần lưu ý rằng khi khoảng cách lưới trở nên nhỏ hơn và giá trị lưới không thay đổi, khi giá thay đổi, tổng các vị trí sẽ tăng lên, và rủi ro đối mặt hoàn toàn khác nhau. Do đó, để kiểm tra lại hiệu ứng của khoảng cách lưới, cần phải chuyển đổi giá trị lưới.
Vì backtest sử dụng dữ liệu K-line 5m, và mỗi K-line chỉ được giao dịch một lần, điều này rõ ràng là không thực tế, đặc biệt là do sự biến động của tiền kỹ thuật số rất cao. Khoảng cách nhỏ hơn sẽ bỏ lỡ nhiều giao dịch trong backtest so với giao dịch trực tiếp. Chỉ có khoảng cách lớn hơn sẽ có giá trị tham chiếu. Trong cơ chế backtest này, các kết luận được rút ra không chính xác. Thông qua backtest dữ liệu dòng lệnh cấp tick, khoảng cách lưới tối ưu nên là 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
Như đã đề cập trước đây, khi biến động là như nhau, giá trị cổ phần càng lớn, rủi ro sẽ tương xứng. Tuy nhiên, miễn là không có sự sụt giảm nhanh chóng, 1% tổng số tiền và 1% khoảng cách lưới nên có thể đối phó với hầu hết các điều kiện thị trường. Trong ví dụ DYDX này, giảm gần 90% cũng gây ra thanh lý. Tuy nhiên, cần lưu ý rằng DYDX chủ yếu giảm. Khi chiến lược lưới đi dài khi giảm, nó sẽ giảm tối đa 100%, trong khi không có giới hạn tăng, và rủi ro cao hơn nhiều. Do đó, Grid Strategy khuyên người dùng chỉ nên chọn chế độ vị trí dài cho các loại tiền tệ mà họ tin rằng có tiềm năng.