如何使用backtrader进行资金费率策略回测
1. 准备工作
需要获取交易所的1小时价格数据,标记价格数据,以及资金费率数据,并按照币对进行合成,每个币对合成一个csv文件。
参考格式如下:
datetime | open | high | low | close | volume | quote_volume | count | taker_buy_volume | taker_buy_quote_volume | mark_price_open | mark_price_close | current_funding_rate |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1/1/2020 0:00 | 7189.43 | 7190.52 | 7170.15 | 7171.55 | 2449.049 | 17576424.44 | 3688 | 996.198 | 7149370.764 | 7195.365819 | 7176.600761 | -0.00012359 |
1/1/2020 1:00 | 7171.43 | 7225 | 7171.1 | 7210.24 | 3865.038 | 27838046.01 | 6635 | 2340.878 | 16860294.05 | 7176.600792 | 7213.459724 | 0 |
1/1/2020 2:00 | 7210.38 | 7239.3 | 7206.46 | 7237.99 | 3228.365 | 23324810.41 | 5120 | 1774.145 | 12818470.64 | 7213.459755 | 7240.39889 | 0 |
1/1/2020 3:00 | 7237.41 | 7239.74 | 7215 | 7221.65 | 2513.307 | 18161821.86 | 4143 | 1245.065 | 8996218.688 | 7240.521639 | 7224.752027 | 0 |
2. 导入backtrader
需要使用我维护的backtrader版本,增加了资金费率的支持。如果用官方版本,需要增加一个计算资金费率的类。
或者自行添加下面的资金费率类到comminfo.py中:
class ComminfoFundingRate(CommInfoBase): # 实现一个数字货币的资金费率类 params = ( ('commission', 0.0), ('mult', 1.0), ('margin', None), ('stocklike', False), ('commtype', CommInfoBase.COMM_PERC), ('percabs', True) ) def __init__(self): super(ComminfoFundingRate, self).__init__() def _getcommission(self, size, price, pseudoexec): total_commission = abs(size) * price * self.p.mult * self.p.commission # print("total_commission", total_commission) return total_commission def get_margin(self, price): return price * self.p.mult * self.p.margin # 计算利息费用,这里面涉及到一些简化 def get_credit_interest(self, data, pos, dt): """计算币安合约的资金费率,先暂时使用价格代替标记价格,后续再优化""" # 仓位及价格 size, price = pos.size, pos.price # 计算资金费率的时候,使用下个bar的开盘价会更精准一些,实际上两者差距应该不大。 try: current_price = data.mark_price_open[1] except IndexError: current_price = data.mark_price_close[0] position_value = size * current_price * self.p.mult # 得到当前的资金费率 try: funding_rate = data.current_funding_rate[1] except IndexError: funding_rate = 0.0 # 如果资金费率为正,则做空的时候会得到资金费率,如果资金费率为负,则做多的时候会得到资金费率 # total_funding_rate = -1 * funding_rate * position_value # 但是broker里面计算的时候是减去这个值,所以需要取相反数 total_funding_rate = funding_rate * position_value # if total_funding_rate != 0: # print(bt.num2date(data.datetime[0]), data._name, "get funding ", total_funding_rate) return total_funding_rate
3. 编写策略
策略主要代码如下:
import backtrader as bt class FundingRateStrategy(bt.Strategy): params = (('period', 30), ('hold_percent', 0.2) ) def log(self, txt, dt=None): """Logging function fot this strategy""" dt = dt or bt.num2date(self.datas[0].datetime[0]) print('{}, {}'.format(dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.bar_num = 0 # 保存现有持仓的币对 self.position_dict = {} # 当前有交易的币对 self.crypto_dict = {} # 计算平均的资金费率 self.funding_rate_dict = {i._name: bt.indicators.SMA(i.current_funding_rate, period=self.p.period) for i in self.datas} def prenext(self): self.next() def next(self): # 记录策略经历了多少个bar self.bar_num += 1 # 总的账户价值 total_value = self.broker.get_value() # 当前时间, 把比特币数据放第一个,读取比特币的时间,币对等数据最好用指数 current_datetime = bt.num2date(self.datas[0].datetime[0]) # 第一个数据是指数,校正时间使用,不能用于交易 # 循环所有的币对,计算币对的数目 for data in self.datas[1:]: # 上市不满一年的币对,忽略不计 if len(data) >= 252: data_datetime = bt.num2date(data.datetime[0]) # 如果两个日期相等,说明当前币对在交易 if current_datetime == data_datetime: crypto_name = data._name if crypto_name not in self.crypto_dict: self.crypto_dict[crypto_name] = 1 # 如果入选的币对小于20支,不使用策略 if len(self.crypto_dict) < 20: return total_target_crypto_num = len(self.crypto_dict) # 现在持仓的币对数目 total_holding_crypto_num = len(self.position_dict) # 计算理论上的手数 now_value = total_value / int(total_target_crypto_num * self.p.hold_percent * 2) # 如果今天是调仓日 if self.bar_num % self.p.period == 0: # 循环币对,平掉所有的币对,计算现在可以交易的币对的累计收益率 result = [] for crypto_name in self.crypto_dict: data = self.getdatabyname(crypto_name) data_datetime = bt.num2date(data.datetime[0]) size = self.getposition(data).size # 如果有仓位 if size != 0: self.close(data) if data._name in self.position_dict: self.position_dict.pop(data._name) # 已经下单,但是订单没有成交 if data._name in self.position_dict and size == 0: order = self.position_dict[data._name] self.cancel(order) self.position_dict.pop(data._name) # 如果两个日期相等,说明币对在交易,就计算收益率,进行排序 if current_datetime == data_datetime: # 获取币对的资金费率 funding_rate = self.funding_rate_dict[data._name][0] result.append([data, funding_rate]) # 根据计算出来的累计收益率进行排序,选出资金费率靠前的币对做空,靠后的币对做多 new_result = sorted(result, key=lambda x: x[1]) num = int(self.p.hold_percent * total_target_crypto_num) buy_list = new_result[:num] sell_list = new_result[-num:] # 根据计算出来的信号,买卖相应的币对 for data, _ in buy_list: lots = now_value / data.close[0] order = self.buy(data, size=lots) self.position_dict[data._name] = order for data, _ in sell_list: lots = now_value / data.close[0] order = self.sell(data, size=lots) self.position_dict[data._name] = order def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # order被提交和接受 return if order.status == order.Rejected: self.log(f"order is rejected : order_ref:{order.ref} order_info:{order.info}") if order.status == order.Margin: self.log(f"order need more margin : order_ref:{order.ref} order_info:{order.info}") if order.status == order.Cancelled: self.log(f"order is cancelled : order_ref:{order.ref} order_info:{order.info}") if order.status == order.Partial: self.log(f"order is partial : order_ref:{order.ref} order_info:{order.info}") # Check if an order has been completed # Attention: broker could reject order if not enougth cash if order.status == order.Completed: if order.isbuy(): self.log(f"{order.data._name} buy order : " f"price : {round(order.executed.price, 6)} , " f"size : {round(order.executed.size, 6)} , " f"margin : {round(order.executed.value, 6)} , " f"cost : {round(order.executed.comm, 6)}") else: # Sell self.log(f"{order.data._name} sell order : " f"price : {round(order.executed.price, 6)} , " f"size : {round(order.executed.size, 6)} , " f"margin : {round(order.executed.value, 6)} , " f"cost : {round(order.executed.comm, 6)}") def notify_trade(self, trade): # 一个trade结束的时候输出信息 if trade.isclosed: self.log(f'closed symbol is : {trade.getdataname()} , ' f'total_profit : {round(trade.pnl, 6)} , ' f'net_profit : {round(trade.pnlcomm, 6)}') if trade.isopen: self.log(f'open symbol is : {trade.getdataname()} , price : {trade.price} ') def stop(self): pass
4. 运行策略
策略运行之后,结果如下:
5. 总结
本文介绍了如何使用backtrader进行资金费率策略回测, 主要是使用了资金费率的计算方法, 以及如何使用自定义的资金费率类。
从回测结果来看,2023年之后资金费率策略表现并没有前几年那么好, 可能是因为使用资金费率套利策略的人太多了,导致资金费率策略逐渐失效。
数据和全部代码,详见付费文章:https://yunjinqi.blog.csdn.net/article/details/144687400