如何使用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_rate3. 编写策略
策略主要代码如下:
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):
pass4. 运行策略
策略运行之后,结果如下:

5. 总结
本文介绍了如何使用backtrader进行资金费率策略回测, 主要是使用了资金费率的计算方法, 以及如何使用自定义的资金费率类。
从回测结果来看,2023年之后资金费率策略表现并没有前几年那么好, 可能是因为使用资金费率套利策略的人太多了,导致资金费率策略逐渐失效。
数据和全部代码,详见付费文章:https://yunjinqi.blog.csdn.net/article/details/144687400
系统当前共有 481 篇文章