在 backtrader 中交易加密货币的分数仓位
首先,让我们用两句话总结一下 backtrader 的工作方式:
它像一个构建工具包,包含一个基本构建模块(Cerebro),可以将许多不同的模块插入其中。
基本分发版包含许多模块,如指标、分析器、观察者、仓位大小计算器、过滤器、数据源、经纪商、佣金/资产信息方案等…
可以轻松地从头开始构建新的模块,或者基于现有模块进行构建。
基本模块(Cerebro)已经实现了一些自动“插拔”,使得用户可以更轻松地使用框架,而不需要关注所有细节。
因此,该框架已预配置以提供默认行为,例如:
- 使用单一的主数据源
- 1天的时间框架/压缩组合
- 10,000 单位的货币
- 股票交易
这些设置可能并不适合每个人,但重要的是:它可以根据每个交易者/程序员的需求进行定制。
交易股票:整数
如上所述,默认配置是用于股票交易,当交易股票时,买入/卖出的是完整的股票份额(即:1、2、50、1000 等,而不是像 1.5 或 1001.7589 这样的数量)。
这意味着,当用户在默认配置下执行以下代码时:
def next(self):
# 将投资组合的 50% 用于购买主资产
self.order_target_percent(target=0.5)
发生的情况是:
系统会计算出需要多少股票份额,以便该资产在投资组合中的价值尽可能接近 50%。
但是,由于默认配置是与股票交易配合使用,结果股票的数量将是一个整数。
注意
请注意,默认配置是使用单一的主数据源,因此在调用 order_target_percent
时,实际的数据并未指定。当使用多个数据源时,必须指定获取/卖出哪个数据(除非是主数据源)。
交易加密货币:分数
显然,在交易加密货币时,即使是小数点后有 20 位数字,也可以购买“半个比特币”。
好消息是,你可以更改与资产相关的信息。这是通过 CommissionInfo
家族的可插拔模块实现的。
一些文档: Docs - Commission Schemes
注意
不得不承认,名字不太合适,因为这些方案不仅仅包含佣金信息,还包含其他内容。
在分数方案中,关注的是该方案的 getsize(price, cash)
方法,它有如下文档字符串:
返回在给定价格下执行现金操作所需的仓位大小
这些方案与经纪商密切相关,并且可以通过经纪商 API 将这些方案添加到系统中。
经纪商文档在这里: Docs - Broker
相关方法是:addcommissioninfo(comminfo, name=None)
。当 name
为 None 时,方案会应用到所有资产;如果指定了名称,则方案仅应用于具有特定名称的资产。
实现分数方案
这可以通过扩展现有的基础方案(即 CommissionInfo
)轻松实现。
class CommInfoFractional(bt.CommissionInfo):
def getsize(self, price, cash):
'''返回按价格执行现金操作所需的分数大小'''
return self.p.leverage * (cash / price)
就是这样。通过子类化 CommissionInfo
并编写一行代码,目标就实现了。由于原始方案定义支持杠杆,因此杠杆也会被考虑进计算中,万一加密货币能够使用杠杆购买(默认值为 1.0,即无杠杆)。
稍后的代码中,方案将按如下方式添加(通过命令行参数控制):
if args.fractional: # 如果需要使用分数方案
cerebro.broker.addcommissioninfo(CommInfoFractional())
即:添加一个子类化方案的实例(注意 ()
用于实例化)。如上所述,name
参数未设置,这意味着它将应用于系统中的所有资产。
测试效果
下面是一个实现了简单的移动平均交叉策略(用于多头/空头仓位)的完整脚本,可以直接在 shell 中使用。测试的默认数据源来自 backtrader 仓库中的数据源之一。
整数模式:没有分数 - 没有乐趣
$ ./fractional-sizes.py --plot
2005-02-14,3079.93,3083.38,3065.27,3075.76,0.00
2005-02-15,3075.20,3091.64,3071.08,3086.95,0.00
...
2005-03-21,3052.39,3059.18,3037.80,3038.14,0.00
2005-03-21,Enter Short
2005-03-22,Sell Order Completed - Size: -16 @Price: 3040.55 Value: -48648.80 Comm: 0.00
2005-03-22,Trade Opened - Size -16 @Price 3040.55
2005-03-22,3040.55,3053.18,3021.66,3050.44,0.00
...
一个大小为 16 单位的空头交易已经开仓。整个日志(因显而易见的原因未显示)包含了许多其他操作,所有交易都采用整数大小。
没有分数
分数模式运行
经过子类化和一行代码的修改,分数的目标就实现了…
$ ./fractional-sizes.py --fractional --plot
2005-02-14,3079.93,3083.38,3065.27,3075.76,0.00
2005-02-15,3075.20,3091.64,3071.08,3086.95,0.00
...
2005-03-21,3052.39,3059.18,3037.80,3038.14,0.00
2005-03-21,Enter Short
2005-03-22,Sell Order Completed - Size: -16.457437774427774 @Price: 3040.55 Value: -50039.66 Comm: 0.00
2005-03-22,Trade Opened - Size -16.457437774427774 @Price 3040.55
2005-03-22,3040.55,3053.18,3021.66,3050.44,0.00
...
胜利了!空头交易已经开仓,并且这次使用的是大小为 -16.457437774427774 的分数仓位。
分数
注意,图表中的最终投资组合价值是不同的,因为实际的交易大小不同。
结论
是的,backtrader 完全可以做到。通过可插拔/可扩展的构建工具包方式,用户可以轻松地根据交易者/程序员的具体需求定制行为。
脚本
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
# Copyright (C) 2019 Daniel Rodriguez - MIT License
# - https://opensource.org/licenses/MIT
# - https://en.wikipedia.org/wiki/MIT_License
###############################################################################
import argparse
import logging
import sys
import backtrader as bt
# This defines not only the commission info, but some other aspects
# of a given data asset like the "getsize" information from below
# params = dict(stocklike=True) # No margin, no multiplier
class CommInfoFractional(bt.CommissionInfo):
def getsize(self, price, cash):
'''Returns fractional size for cash operation @price'''
return self.p.leverage * (cash / price)
class St(bt.Strategy):
params = dict(
p1=10, p2=30, # periods for crossover
ma=bt.ind.SMA, # moving average to use
target=0.5, # percentage of value to use
)
def __init__(self):
ma1, ma2 = [self.p.ma(period=p) for p in (self.p.p1, self.p.p2)]
self.cross = bt.ind.CrossOver(ma1, ma2)
def next(self):
self.logdata()
if self.cross > 0:
self.loginfo('Enter Long')
self.order_target_percent(target=self.p.target)
elif self.cross < 0:
self.loginfo('Enter Short')
self.order_target_percent(target=-self.p.target)
def notify_trade(self, trade):
if trade.justopened:
self.loginfo('Trade Opened - Size {} @Price {}',
trade.size, trade.price)
elif trade.isclosed:
self.loginfo('Trade Closed
- Size {} @Price {} Comm: {:.2f}',
trade.size, trade.price, trade.commission)
def logdata(self):
if self.position:
self.loginfo('Pos {} Value {:.2f} Cash {:.2f}',
self.position.size, self.position.value, self.broker.cash)
def loginfo(self, *args):
'''Logging helper'''
msg = f'{self.datas[0].datetime.datetime(0)} '
msg += ' '.join([str(arg) for arg in args])
print(msg)
def run():
'''Main execution code'''
parser = argparse.ArgumentParser(
description="Backtrader with fractional order size"
)
parser.add_argument(
'-p', '--plot', action='store_true', default=False, help="Plot results"
)
parser.add_argument(
'-f', '--fractional', action='store_true', default=False,
help="Enable fractional order sizes"
)
args = parser.parse_args()
# Create a Cerebro engine
cerebro = bt.Cerebro()
# Load data from Yahoo Finance
data = bt.feeds.YahooFinanceData(dataname='AAPL')
# Add the data to the engine
cerebro.adddata(data)
if args.fractional: # Fractional orders mode
cerebro.broker.addcommissioninfo(CommInfoFractional())
cerebro.addstrategy(St)
# Set starting cash (USD)
cerebro.broker.set_cash(100000.0)
# Set commission (a flat fee per order)
cerebro.broker.set_commission(commission=0.005)
# Set slippage model (default to 0.001)
cerebro.broker.set_slippage_perc(0.001)
# Set the portfolio value
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
# Print out the starting portfolio value
print(f"Starting Portfolio Value: {cerebro.broker.getvalue()}")
# Run the strategy
cerebro.run()
# Print out the final portfolio value
print(f"Ending Portfolio Value: {cerebro.broker.getvalue()}")
if args.plot:
cerebro.plot()
if __name__ == '__main__':
run()