在 backtrader 中交易加密货币分数大小
原文: https://www.backtrader.com/blog/posts/2019-08-29-fractional-sizes/fractional-sizes/
首先,让我们用两行文字总结一下反向交易者的工作原理:
-
它就像一个带有基本构造块(
Cerebro
的构造套件,可以插入许多不同的部件 -
基本分布包含许多块,如指标、分析工具、观察者、尺寸仪、过滤器、数据源、经纪人、佣金/资产信息方案、。。。
-
新的构建块可以很容易地从头开始构建,或者基于现有的构建块
-
基本构建块(
Cerebro
)已经进行了一些自动【插入】的操作,以便更轻松地使用框架,而不必担心所有细节。
因此,框架预先配置为提供默认行为,例如:
- 使用单个/主数据源
1-day
时间段/压缩组合- 10000 货币单位
- 股票交易
这可能并不适合所有人,但重要的是:它可以根据每个交易者/程序员的个人需求进行定制
交易股票:整数
如上所述,默认配置为股票交易,当交易股票时,买入/卖出完整的股票,(即:1、2……50……1000,而不是1.5
或1001.7589
股票的金额。
这意味着当用户在默认配置中执行以下操作时:
def next(self):
# Apply 50% of the portfolio to buy the main asset
self.order_target_percent(target=0.5)
发生以下情况:
-
系统计算需要多少资产份额,以便给定资产的投资组合中的价值尽可能接近
50%
-
但由于默认配置是使用共享,因此生成的共享数将是一个整数,即:一个整数
笔记
请注意,默认配置是使用单个/主数据馈送,这就是为什么在对order_percent_target
的调用中未指定实际数据的原因。当使用多个数据源进行操作时,必须指定要获取/出售的数据(除非是指主数据)
交易加密货币:分数
很明显,在交易加密货币时,即使是 20 位小数,也可以买到“半个比特币”。
好的方面是,可以实际更改与资产相关的信息。这是通过CommissionInfo
系列可插拔件实现的。
部分文件:文件-佣金方案-https://www.backtrader.com/docu/commission-schemes/commission-schemes/
笔记
必须承认,这个名称是不幸的,因为这些方案不仅包含有关佣金的信息,还包含其他信息。
在分数场景中,感兴趣的是方案的这个方法:getsize(price, cash)
,它有以下 docstring
Returns the needed size to meet a cash operation at a given price
这些方案与代理密切相关,通过代理 api,可以在系统中添加这些方案。
经纪人单据位于:单据-经纪人-https://www.backtrader.com/docu/broker/
相关方法为:addcommissioninfo(comminfo, name=None)
。其中除了添加适用于所有资产的方案(当name
为None
时),还可以设置仅适用于具有特定名称的资产的方案。
实施分数方案
这可以通过扩展现有的基础方案CommissionInfo
轻松实现。
class CommInfoFractional(bt.CommissionInfo):
def getsize(self, price, cash):
'''Returns fractional size for cash operation @price'''
return self.p.leverage * (cash / price)
同上,完成。子类化CommissionInfo
并编写一行方法,实现了目标。由于原方案定义支持leverage
,因此在计算时会考虑到这一点,以防可以通过杠杆购买加密货币(默认值为1.0
,即:无杠杆)
在代码的后面,将添加这样的方案(通过命令行参数控制)
if args.fractional: # use the fractional scheme if requested
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
...
V
为了胜利。空头交易已经以相同的交叉方式开始,但这一次的分数大小为-16.457437774427774
请注意,图表中的最终投资组合值不同,这是因为实际交易规模不同。
结论
是的,反向交易者可以。使用可插拔/可扩展的构造工具包方法,可以很容易地根据交易者程序员的特定需求定制行为。
剧本
#!/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 - Profit {}', trade.pnlcomm)
else: # trade updated
self.loginfo('Trade Updated - Size {} @Price {}',
trade.size, trade.price)
def notify_order(self, order):
if order.alive():
return
otypetxt = 'Buy ' if order.isbuy() else 'Sell'
if order.status == order.Completed:
self.loginfo(
('{} Order Completed - '
'Size: {} @Price: {} '
'Value: {:.2f} Comm: {:.2f}'),
otypetxt, order.executed.size, order.executed.price,
order.executed.value, order.executed.comm
)
else:
self.loginfo('{} Order rejected', otypetxt)
def loginfo(self, txt, *args):
out = [self.datetime.date().isoformat(), txt.format(*args)]
logging.info(','.join(out))
def logerror(self, txt, *args):
out = [self.datetime.date().isoformat(), txt.format(*args)]
logging.error(','.join(out))
def logdebug(self, txt, *args):
out = [self.datetime.date().isoformat(), txt.format(*args)]
logging.debug(','.join(out))
def logdata(self):
txt = []
txt += ['{:.2f}'.format(self.data.open[0])]
txt += ['{:.2f}'.format(self.data.high[0])]
txt += ['{:.2f}'.format(self.data.low[0])]
txt += ['{:.2f}'.format(self.data.close[0])]
txt += ['{:.2f}'.format(self.data.volume[0])]
self.loginfo(','.join(txt))
def run(args=None):
args = parse_args(args)
cerebro = bt.Cerebro()
data = bt.feeds.BacktraderCSVData(dataname=args.data)
cerebro.adddata(data) # create and add data feed
cerebro.addstrategy(St) # add the strategy
cerebro.broker.set_cash(args.cash) # set broker cash
if args.fractional: # use the fractional scheme if requested
cerebro.broker.addcommissioninfo(CommInfoFractional())
cerebro.run() # execute
if args.plot: # Plot if requested to
cerebro.plot(**eval('dict(' + args.plot + ')'))
def logconfig(pargs):
if pargs.quiet:
verbose_level = logging.ERROR
else:
verbose_level = logging.INFO - pargs.verbose * 10 # -> DEBUG
logger = logging.getLogger()
for h in logger.handlers: # Remove all loggers from root
logger.removeHandler(h)
stream = sys.stdout if not pargs.stderr else sys.stderr # choose stream
logging.basicConfig(
stream=stream,
format="%(message)s", # format="%(levelname)s: %(message)s",
level=verbose_level,
)
def parse_args(pargs=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description='Fractional Sizes with CommInfo',
)
pgroup = parser.add_argument_group('Data Options')
parser.add_argument('--data', default='../../datas/2005-2006-day-001.txt',
help='Data to read in')
pgroup = parser.add_argument_group(title='Broker Arguments')
pgroup.add_argument('--cash', default=100000.0, type=float,
help='Starting cash to use')
pgroup.add_argument('--fractional', action='store_true',
help='Use fractional commission info')
pgroup = parser.add_argument_group(title='Plotting Arguments')
pgroup.add_argument('--plot', default='', nargs='?', const='{}',
metavar='kwargs', help='kwargs: "k1=v1,k2=v2,..."')
pgroup = parser.add_argument_group('Verbosity Options')
pgroup.add_argument('--stderr', action='store_true',
help='Log to stderr, else to stdout')
pgroup = pgroup.add_mutually_exclusive_group()
pgroup.add_argument('--quiet', '-q', action='store_true',
help='Silent (errors will be reported)')
pgroup.add_argument('--verbose', '-v', action='store_true',
help='Increase verbosity level')
# Parse and process some args
pargs = parser.parse_args(pargs)
logconfig(pargs) # config logging
return pargs
if __name__ == '__main__':
run()