原文:
www.backtrader.com/blog/posts/2016-12-28-btfd-bites/btfd-bites/
之前的帖子设法复制了BTFD
策略,发现真正的收益是16x
而不是31x
。
但正如在复制过程中指出的那样:
2x
杠杆不收取利息
这引起了一个显而易见的问题:
幸运的是,之前的样本足够灵活,可以进行实验。 为了得到一些视觉反馈和验证,以下代码将添加到策略中
def start(self):
print(','.join(['TRADE', 'STATUS', 'Value', 'PNL', 'COMMISSION']))
def notify_order(self, order):
if order.status in [order.Margin]:
print('ORDER FAILED with status:', order.getstatusname())
def notify_trade(self, trade):
if trade.isclosed:
print(','.join(map(str, [
'TRADE', 'CLOSE',
self.data.num2date(trade.dtclose).date().isoformat(),
trade.value,
trade.pnl,
trade.commission,
]
)))
elif trade.justopened:
print(','.join(map(str, [
'TRADE', 'OPEN',
self.data.num2date(trade.dtopen).date().isoformat(),
trade.value,
trade.pnl,
trade.commission,
]
)))
关键在于以下内容:
Margin
注意
因为将有一个调整要投资的金额,以留出佣金的空间,所以一些订单可能无法被经纪人接受。 这种视觉反馈可以帮助识别情况
首先快速测试以查看某些订单是否被接受。
$ ./btfd.py --comminfo commission=0.001,leverage=2.0 --strat target=1.0
TRADE,STATUS,Value,PNL,COMMISSION
ORDER FAILED with status: Margin
ORDER FAILED with status: Margin
TRADE,OPEN,1990-01-08,199345.2,0.0,199.3452
TRADE,CLOSE,1990-01-10,0.0,-1460.28,397.23012
注意:
target=1.0
,意味着:尝试投资 100%的资本。 这是默认值,但它在这里作为参考。
commission=0.001
或0.1%
,以确保我们有时会满足保证金
Margin
。
100%
的资本,但资产有一个价格,这是用来计算股份大小的。 大小是从实际可用的现金计算出的潜在大小的结果进行向下舍入。 这次向下舍入为这个第三个订单留下了足够的空间。
OPEN
和CLOSE
)显示了开仓手续费和最终总手续费,接近200k
的价值,展示了2x
杠杆的实际效果。
开仓手续费为199.3452
,这是杠杆价值的0.1%
,即199,345.2
剩余的测试将使用target=0.99x
进行,其中x
将确保有足够的空间用于选定的手续费。
让我们举一些真实的例子
./btfd.py --comminfo commission=0.001,leverage=2.0 --strat target=0.998 --plot
霹雳般的! 不仅BTFD
策略远未接近16x
的收益:它损失了大部分资金。
100,000
下降到大约4,027
注意
下降到值是非杠杆值,因为这是在平仓时将返回系统的大约值
很可能是佣金过于激进。 让我们去一半
./btfd.py --comminfo commission=0.0005,leverage=2.0 --strat target=0.999 --plot
NO, NO。佣金并非如此激进,因为系统仍然亏损,从100,000
下降到约69,000
(非杠杆价值)
佣金再次被除以二
./btfd.py --comminfo commission=0.00025,leverage=2.0 --strat target=0.9995 --plot
最终系统赚钱了:
100,000
被用于3x
增益,增加到331,459
。
600k
注意
该示例接受--fromdate YYYY-MM-DD
和--todate YYYY-MM-DD
来选择应用策略的时间段。这将允许对不同日期范围进行类似的场景测试。
当面对佣金时,16x
的收益无法持续。对于一些经纪人提供的佣金(无上限且按百分比计算),需要非常好的交易才能确保系统赚钱。
在这种情况下,策略应用于标普 500
,BTFD
策略与指数的表现不相匹配。
没有应用利率。使用佣金已足以看出16x
距离任何潜在利润有多远。无论如何,以2%
的利率运行将执行如下
./btfd.py --comminfo commission=0.00025,leverage=2.0,interest=0.02,interest_long=True --strat target=0.9995 --plot
interest_long=True
是必须的,因为默认情况下收取利息的行为只针对多头仓位进行。
$ ./btfd.py --help
usage: btfd.py [-h] [--offline] [--data TICKER]
[--fromdate YYYY-MM-DD[THH:MM:SS]]
[--todate YYYY-MM-DD[THH:MM:SS]] [--cerebro kwargs]
[--broker kwargs] [--valobserver kwargs] [--strat kwargs]
[--comminfo kwargs] [--plot [kwargs]]
BTFD - http://dark-bid.com/BTFD-only-strategy-that-matters.html - https://www.
reddit.com/r/algotrading/comments/5jez2b/can_anyone_replicate_this_strategy/
optional arguments:
-h, --help show this help message and exit
--offline Use offline file with ticker name (default: False)
--data TICKER Yahoo ticker to download (default: ^GSPC)
--fromdate YYYY-MM-DD[THH:MM:SS]
Starting date[time] (default: 1990-01-01)
--todate YYYY-MM-DD[THH:MM:SS]
Ending date[time] (default: 2016-10-01)
--cerebro kwargs kwargs in key=value format (default: stdstats=False)
--broker kwargs kwargs in key=value format (default: cash=100000.0,
coc=True)
--valobserver kwargs kwargs in key=value format (default:
assetstart=100000.0)
--strat kwargs kwargs in key=value format (default:
approach="highlow")
--comminfo kwargs kwargs in key=value format (default: leverage=2.0)
--plot [kwargs] kwargs in key=value format (default: )
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# References:
# - https://www.reddit.com/r/algotrading/comments/5jez2b/can_anyone_replicate_this_strategy/
# - http://dark-bid.com/BTFD-only-strategy-that-matters.html
import argparse
import datetime
import backtrader as bt
class ValueUnlever(bt.observers.Value):
'''Extension of regular Value observer to add leveraged view'''
lines = ('value_lever', 'asset')
params = (('assetstart', 100000.0), ('lever', True),)
def next(self):
super(ValueUnlever, self).next()
if self.p.lever:
self.lines.value_lever[0] = self._owner.broker._valuelever
if len(self) == 1:
self.lines.asset[0] = self.p.assetstart
else:
change = self.data[0] / self.data[-1]
self.lines.asset[0] = change * self.lines.asset[-1]
class St(bt.Strategy):
params = (
('fall', -0.01),
('hold', 2),
('approach', 'highlow'),
('target', 1.0)
)
def __init__(self):
if self.p.approach == 'closeclose':
self.pctdown = self.data.close / self.data.close(-1) - 1.0
elif self.p.approach == 'openclose':
self.pctdown = self.data.close / self.data.open - 1.0
elif self.p.approach == 'highclose':
self.pctdown = self.data.close / self.data.high - 1.0
elif self.p.approach == 'highlow':
self.pctdown = self.data.low / self.data.high - 1.0
def next(self):
if self.position:
if len(self) == self.barexit:
self.close()
else:
if self.pctdown <= self.p.fall:
self.order_target_percent(target=self.p.target)
self.barexit = len(self) + self.p.hold
def start(self):
print(','.join(['TRADE', 'STATUS', 'Value', 'PNL', 'COMMISSION']))
def notify_order(self, order):
if order.status in [order.Margin, order.Rejected, order.Canceled]:
print('ORDER FAILED with status:', order.getstatusname())
def notify_trade(self, trade):
if trade.isclosed:
print(','.join(map(str, [
'TRADE', 'CLOSE',
self.data.num2date(trade.dtclose).date().isoformat(),
trade.value,
trade.pnl,
trade.commission,
]
)))
elif trade.justopened:
print(','.join(map(str, [
'TRADE', 'OPEN',
self.data.num2date(trade.dtopen).date().isoformat(),
trade.value,
trade.pnl,
trade.commission,
]
)))
def runstrat(args=None):
args = parse_args(args)
cerebro = bt.Cerebro()
# Data feed kwargs
kwargs = dict()
# Parse from/to-date
dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S'
for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']):
kwargs[d] = datetime.datetime.strptime(a, dtfmt + tmfmt * ('T' in a))
if not args.offline:
YahooData = bt.feeds.YahooFinanceData
else:
YahooData = bt.feeds.YahooFinanceCSVData
# Data feed - no plot - observer will do the job
data = YahooData(dataname=args.data, plot=False, **kwargs)
cerebro.adddata(data)
# Broker
cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')'))
# Add a commission
cerebro.broker.setcommission(**eval('dict(' + args.comminfo + ')'))
# Strategy
cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))
# Add specific observer
cerebro.addobserver(ValueUnlever, **eval('dict(' + args.valobserver + ')'))
# Execute
cerebro.run(**eval('dict(' + args.cerebro + ')'))
if args.plot: # Plot if requested to
cerebro.plot(**eval('dict(' + args.plot + ')'))
def parse_args(pargs=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description=(' - '.join([
'BTFD',
'http://dark-bid.com/BTFD-only-strategy-that-matters.html',
('https://www.reddit.com/r/algotrading/comments/5jez2b/'
'can_anyone_replicate_this_strategy/')]))
)
parser.add_argument('--offline', required=False, action='store_true',
help='Use offline file with ticker name')
parser.add_argument('--data', required=False, default='^GSPC',
metavar='TICKER', help='Yahoo ticker to download')
parser.add_argument('--fromdate', required=False, default='1990-01-01',
metavar='YYYY-MM-DD[THH:MM:SS]',
help='Starting date[time]')
parser.add_argument('--todate', required=False, default='2016-10-01',
metavar='YYYY-MM-DD[THH:MM:SS]',
help='Ending date[time]')
parser.add_argument('--cerebro', required=False, default='stdstats=False',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--broker', required=False,
default='cash=100000.0, coc=True',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--valobserver', required=False,
default='assetstart=100000.0',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--strat', required=False,
default='approach="highlow"',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--comminfo', required=False, default='leverage=2.0',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--plot', required=False, default='',
nargs='?', const='volume=False',
metavar='kwargs', help='kwargs in key=value format')
return parser.parse_args(pargs)
if __name__ == '__main__':
runstrat()
注意
为CloseClose
方法添加了操作日志(下面的示例也已更新)
Reddit上的一篇帖子呼吁复制BTFD
策略,结果证明这是添加另一个功能到backtrader所需的小推动:杠杆
这些链接:
在Reddit的帖子中最终没有对Dark Bid的结果进行完全复制。但是这是可以做到的。尽管从一开始就必须指出一些事情:
让我们直接在这里查看图表(请记住,图表来自Dark Bid BTFD帖子)
大多数人可能会做出以下解释:
1%
附加信息:
1990-01-01
运行到2016-09-30
100,000
开始
注意
请注意,没有收取任何佣金或信用利息。
下面的示例已配置为获胜设置,但第一次执行将遵守解释的规则。
$ ./btfd.py --plot --strat approach="'closeclose'"
注意
该示例接受一个--offline
参数,以使用之前从Yahoo下载的文件来加速测试。如果不使用,将每次都从Yahoo下载默认日期和默认股票代码为^GSPC
的数据。
休斯顿,我们有问题!尽管没有预期的那么大。观察结果:
100,000
增加到602,816
,这似乎与上面的BTFD
图表完全吻合
100,000
现金只增加到357,277
,这似乎低于预期值的10x
,预期值为3,000,000
(或3M
)
value
和value_unlever
相等,这意味着策略不在市场中
但是最有趣的是:红色线显示的时间内的价值明显不同
2x
杠杆允许购买两倍的资产。
但是当策略处于现金状态时,现金的价值并不是其价值的两倍。
BTFD
行没有展现出这种行为,因为它始终始终是杠杆的
好的。BTFD
谜题的第一部分… 已解决!但我们仍然有一个问题,这是匹配Dark Bid实现的性能。毫不犹豫,使用默认的获胜设置运行样本。
$ ./btfd.py --plot --strat approach="'highlow'"
注意
如果在策略中没有approach参数运行样本,则approach=highlow
是默认设置
在这种情况下:
high
和low
之间的差异。
当然,这并不是*“下跌至少 1%,如BTFD
图表所示。这是不同的情况,因为low
可能也发生在high
之前,许多人会认为这是上涨而不是下跌*。
注意
该策略允许这些方法:closeclose
、highlow
、highclose
和openclose
。
但是:
value
为3,184,118
。这是一个赢家,因为对BTFD
图表的视觉检查显示最终值超过了3M
。
value_unlever
)是:1,592,608
,这不是上述情况。这只是告诉我们策略正在市场上
显然,在大约 1 或 2 天内,该策略将出售头寸,实际现金价值将约为1,592,608
,绝不接近3,184,118
。
1.5M
的值是原始Reddit帖子达到的最佳观察结果。
完整的BTFD
谜团已解决。关键在于:
value
线不是真实的,因为它始终杠杆,而不仅仅是在购买时
high
和low
之间至少有 1%的差异时购买
16 倍
(从100,000
增加到大约1,592,000
),在任何情况下都不会达到31 倍
(从100,000
增加到3.1M
)
$ ./btfd.py --help
usage: btfd.py [-h] [--offline] [--data TICKER]
[--fromdate YYYY-MM-DD[THH:MM:SS]]
[--todate YYYY-MM-DD[THH:MM:SS]] [--cerebro kwargs]
[--broker kwargs] [--valobserver kwargs] [--strat kwargs]
[--comminfo kwargs] [--plot [kwargs]]
BTFD - http://dark-bid.com/BTFD-only-strategy-that-matters.html - https://www.
reddit.com/r/algotrading/comments/5jez2b/can_anyone_replicate_this_strategy/
optional arguments:
-h, --help show this help message and exit
--offline Use offline file with ticker name (default: False)
--data TICKER Yahoo ticker to download (default: ^GSPC)
--fromdate YYYY-MM-DD[THH:MM:SS]
Starting date[time] (default: 1990-01-01)
--todate YYYY-MM-DD[THH:MM:SS]
Ending date[time] (default: 2016-10-01)
--cerebro kwargs kwargs in key=value format (default: stdstats=False)
--broker kwargs kwargs in key=value format (default: cash=100000.0,
coc=True)
--valobserver kwargs kwargs in key=value format (default:
assetstart=100000.0)
--strat kwargs kwargs in key=value format (default:
approach="highlow")
--comminfo kwargs kwargs in key=value format (default: leverage=2.0)
--plot [kwargs] kwargs in key=value format (default: )
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# References:
# - https://www.reddit.com/r/algotrading/comments/5jez2b/can_anyone_replicate_this_strategy/
# - http://dark-bid.com/BTFD-only-strategy-that-matters.html
import argparse
import datetime
import backtrader as bt
class ValueUnlever(bt.observers.Value):
'''Extension of regular Value observer to add leveraged view'''
lines = ('value_lever', 'asset')
params = (('assetstart', 100000.0), ('lever', True),)
def next(self):
super(ValueUnlever, self).next()
if self.p.lever:
self.lines.value_lever[0] = self._owner.broker._valuelever
if len(self) == 1:
self.lines.asset[0] = self.p.assetstart
else:
change = self.data[0] / self.data[-1]
self.lines.asset[0] = change * self.lines.asset[-1]
class St(bt.Strategy):
params = (
('fall', -0.01),
('hold', 2),
('approach', 'highlow'),
('target', 1.0),
('prorder', False),
('prtrade', False),
('prdata', False),
)
def __init__(self):
if self.p.approach == 'closeclose':
self.pctdown = self.data.close / self.data.close(-1) - 1.0
elif self.p.approach == 'openclose':
self.pctdown = self.data.close / self.data.open - 1.0
elif self.p.approach == 'highclose':
self.pctdown = self.data.close / self.data.high - 1.0
elif self.p.approach == 'highlow':
self.pctdown = self.data.low / self.data.high - 1.0
def next(self):
if self.position:
if len(self) == self.barexit:
self.close()
if self.p.prdata:
print(','.join(str(x) for x in
['DATA', 'CLOSE',
self.data.datetime.date().isoformat(),
self.data.close[0],
float('NaN')]))
else:
if self.pctdown <= self.p.fall:
self.order_target_percent(target=self.p.target)
self.barexit = len(self) + self.p.hold
if self.p.prdata:
print(','.join(str(x) for x in
['DATA', 'OPEN',
self.data.datetime.date().isoformat(),
self.data.close[0],
self.pctdown[0]]))
def start(self):
if self.p.prtrade:
print(','.join(
['TRADE', 'Status', 'Date', 'Value', 'PnL', 'Commission']))
if self.p.prorder:
print(','.join(
['ORDER', 'Type', 'Date', 'Price', 'Size', 'Commission']))
if self.p.prdata:
print(','.join(['DATA', 'Action', 'Date', 'Price', 'PctDown']))
def notify_order(self, order):
if order.status in [order.Margin, order.Rejected, order.Canceled]:
print('ORDER FAILED with status:', order.getstatusname())
elif order.status == order.Completed:
if self.p.prorder:
print(','.join(map(str, [
'ORDER', 'BUY' * order.isbuy() or 'SELL',
self.data.num2date(order.executed.dt).date().isoformat(),
order.executed.price,
order.executed.size,
order.executed.comm,
]
)))
def notify_trade(self, trade):
if not self.p.prtrade:
return
if trade.isclosed:
print(','.join(map(str, [
'TRADE', 'CLOSE',
self.data.num2date(trade.dtclose).date().isoformat(),
trade.value,
trade.pnl,
trade.commission,
]
)))
elif trade.justopened:
print(','.join(map(str, [
'TRADE', 'OPEN',
self.data.num2date(trade.dtopen).date().isoformat(),
trade.value,
trade.pnl,
trade.commission,
]
)))
def runstrat(args=None):
args = parse_args(args)
cerebro = bt.Cerebro()
# Data feed kwargs
kwargs = dict()
# Parse from/to-date
dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S'
for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']):
kwargs[d] = datetime.datetime.strptime(a, dtfmt + tmfmt * ('T' in a))
if not args.offline:
YahooData = bt.feeds.YahooFinanceData
else:
YahooData = bt.feeds.YahooFinanceCSVData
# Data feed - no plot - observer will do the job
data = YahooData(dataname=args.data, plot=False, **kwargs)
cerebro.adddata(data)
# Broker
cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')'))
# Add a commission
cerebro.broker.setcommission(**eval('dict(' + args.comminfo + ')'))
# Strategy
cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))
# Add specific observer
cerebro.addobserver(ValueUnlever, **eval('dict(' + args.valobserver + ')'))
# Execute
cerebro.run(**eval('dict(' + args.cerebro + ')'))
if args.plot: # Plot if requested to
cerebro.plot(**eval('dict(' + args.plot + ')'))
def parse_args(pargs=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description=(' - '.join([
'BTFD',
'http://dark-bid.com/BTFD-only-strategy-that-matters.html',
('https://www.reddit.com/r/algotrading/comments/5jez2b/'
'can_anyone_replicate_this_strategy/')]))
)
parser.add_argument('--offline', required=False, action='store_true',
help='Use offline file with ticker name')
parser.add_argument('--data', required=False, default='^GSPC',
metavar='TICKER', help='Yahoo ticker to download')
parser.add_argument('--fromdate', required=False, default='1990-01-01',
metavar='YYYY-MM-DD[THH:MM:SS]',
help='Starting date[time]')
parser.add_argument('--todate', required=False, default='2016-10-01',
metavar='YYYY-MM-DD[THH:MM:SS]',
help='Ending date[time]')
parser.add_argument('--cerebro', required=False, default='stdstats=False',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--broker', required=False,
default='cash=100000.0, coc=True',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--valobserver', required=False,
default='assetstart=100000.0',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--strat', required=False,
default='approach="highlow"',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--comminfo', required=False, default='leverage=2.0',
metavar='kwargs', help='kwargs in key=value format')
parser.add_argument('--plot', required=False, default='',
nargs='?', const='volume=False',
metavar='kwargs', help='kwargs in key=value format')
return parser.parse_args(pargs)
if __name__ == '__main__':
runstrat()
CloseClose
方法执行:
$ ./btfd.py --strat approach="'closeclose'",prorder=True,prdata=True
结果:
ORDER,Type,Date,Price,Size,Commission
DATA,Action,Date,Price,PctDown
DATA,OPEN,1990-01-09,349.62,-0.0117866530993
ORDER,BUY,1990-01-09,349.62,572,0.0
...
DATA,CLOSE,1990-01-11,348.53,nan
ORDER,BUY,2016-09-09,2127.81,336,0.0
DATA,CLOSE,2016-09-13,2127.02,nan
ORDER,SELL,2016-09-13,2127.02,-336,0.0