La estrategia de red perpetua es una estrategia clásica popular en la plataforma FMZ. En comparación con la red spot, no es necesario tener monedas, y se puede agregar apalancamiento, lo que es mucho más conveniente que la red spot. Sin embargo, ya que no es posible realizar pruebas de retroceso en la plataforma FMZ Quant directamente, no es propicio para seleccionar monedas y determinar la optimización de parámetros. En este artículo, presentaremos el proceso completo de backtesting de Python, incluida la recopilación de datos, el marco de backtesting, las funciones de backtesting, la optimización de parámetros, etc. Puede probarlo usted mismo en el cuaderno juypter.
Generalmente, es suficiente usar datos de línea K. Para mayor precisión, cuanto menor sea el período de línea K, mejor. Sin embargo, para equilibrar el tiempo de backtest y el volumen de datos, en este artículo, usamos 5min de datos de los últimos dos años para backtesting. El volumen de datos final superó las 200,000 líneas. Elegimos DYDX como moneda. Por supuesto, la moneda específica y el período de línea K se pueden seleccionar de acuerdo con sus propios intereses.
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()
Para backtesting, seguimos eligiendo el marco comúnmente utilizado que admite contratos perpetuos USDT en múltiples monedas, que es simple y fácil de usar.
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)
El principio de la estrategia de la red es muy simple. Vender cuando el precio sube y comprar cuando el precio cae. En concreto, implica tres parámetros: precio inicial, espaciamiento de la red y valor comercial. El mercado de DYDX fluctúa mucho.
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 configuración del precio inicial afecta a la posición inicial de la estrategia. El precio inicial predeterminado para la prueba de retroceso ahora es el precio inicial en el inicio, es decir, no se mantiene ninguna posición en el inicio. Y sabemos que la estrategia de red realizará todas las ganancias cuando el precio regrese a la etapa inicial, por lo que si la estrategia puede predecir correctamente el mercado futuro cuando se lanza, los ingresos mejorarán significativamente. Aquí, establecemos el precio inicial a 3U y luego la prueba de retroceso. Al final, el descenso máximo fue de 9200U, y la ganancia final fue de 13372U. La estrategia final no mantiene posiciones. La ganancia es todas las ganancias de fluctuación, y la diferencia entre las ganancias de los parámetros predeterminados es la pérdida de posición causada por un juicio inexacto del precio final.
Sin embargo, si el precio inicial se establece en 3U, la estrategia será corta al principio y mantendrá un gran número de posiciones cortas.
El intervalo de la cuadrícula determina la distancia entre las órdenes pendientes. Obviamente, cuanto menor sea el intervalo, más frecuentes sean las transacciones, menor sea la ganancia de una sola transacción y mayor sea la tarifa de manejo. Sin embargo, vale la pena señalar que a medida que el intervalo de la cuadrícula se vuelve más pequeño y el valor de la cuadrícula permanece sin cambios, cuando el precio cambia, las posiciones totales aumentarán, y los riesgos enfrentados son completamente diferentes. Por lo tanto, para probar el efecto del intervalo de la cuadrícula, es necesario convertir el valor de la cuadrícula.
Como el backtest utiliza datos de 5 millones de líneas K, y cada línea K solo se negocia una vez, lo que obviamente no es realista, especialmente porque la volatilidad de las monedas digitales es muy alta. Un espaciamiento más pequeño perderá muchas transacciones en el backtesting en comparación con el comercio en vivo. Solo un espaciamiento más grande tendrá valor de referencia. En este mecanismo de backtesting, las conclusiones extraídas no son precisas. A través del backtesting de datos de flujo de pedidos a nivel de tick, el espaciamiento óptimo de la cuadrícula debe ser 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
Como se mencionó anteriormente, cuando las fluctuaciones son las mismas, cuanto mayor sea el valor de la tenencia, el riesgo es proporcional. Sin embargo, siempre que no haya una disminución rápida, el 1% de los fondos totales y el 1% del espaciamiento de la red deberían poder hacer frente a la mayoría de las condiciones del mercado. En este ejemplo de DYDX, una caída de casi el 90% también desencadenó una liquidación. Sin embargo, debe tenerse en cuenta que DYDX cae principalmente. Cuando la estrategia de red va largo cuando cae, caerá en un 100% como máximo, mientras que no hay límite en el aumento, y el riesgo es mucho mayor. Por lo tanto, Grid Strategy recomienda a los usuarios que elijan solo el modo de posición larga para las monedas que creen potencial.