Target Orders
Until version 1.8.10.96 smart staking was possible with backtrader over the Strategy methods: buy and sell. It was all about adding a Sizer to the equation which is responsible for the size of the stake.
What a Sizer cannot do is decide if the operation has to be a buy or a sell. And that means that a new concept is needed in which a small intelligence layer is added to make such decision.
Here is where the family of order_target_xxx
methods in the Strategy come
into play. Inspired by the ones in zipline
, the methods offer the chance to
simply specify the final target, be the target:
-
size
-> amount of shares, contracts in the portfolio of a specific asset -
value
-> value in monetary units of the asset in the portfolio -
percent
-> percentage (from current portfolio) value of the asset in the current portfolio
Note
The reference for the methods can be found in Strategy reference. The
summary is that the methods use the same signature as buy
and
sell
except for the parameter size
which is replaced by the
parameter target
In this case it is all about specifying the final target and the method
decides if an operation will be a buy or a sell. The same logic applies to
the 3 methods. Let’s tart with order_target_size
-
If the target is greater than the position a buy is issued, with the difference
target - position_size
Examples:
-
Pos:
0
, target:7
-> buy(size=7 - 0) -> buy(size=7) -
Pos:
3
, target:7
-> buy(size=7 - 3) -> buy(size=4) -
Pos:
-3
, target:7
-> buy(size=7 - -3) -> buy(size=10) -
Pos:
-3
, target:-2
-> buy(size=-2 - -3) -> buy(size=1)
-
-
If the target is smaller than the position a sell is issued with the difference
position_size - target
Examples:
-
Pos:
0
, target:-7
-> sell(size=0 - -7) -> sell(size=7) -
Pos:
3
, target:-7
-> sell(size=3 - -7) -> sell(size=10) -
Pos:
-3
, target:-7
-> sell(size=-3 - -7) -> sell(size=4) -
Pos:
3
, target:2
-> sell(size=3 - 2) -> sell(size=1)
-
When targetting a value with order_target_value
, the current value of the
asset in the portfolio and the position size are both taken into account to
decide what the final underlying operation will be. The reasoning:
- If position size is negative (short) and the target value has to be greater than the current value, this means: sell more
As such the logic works as follows:
-
If
target > value
andsize >=0
-> buy -
If
target > value
andsize < 0
-> sell -
If
target < value
andsize >= 0
-> sell -
If
target < value
andsize\* < 0
-> buy
The logic for order_target_percent
is the same as that of
order_target_value
. This method simply takes into account the current total
value of the portfolio to determine the target value for the asset.
The Sample
backtrader tries to have a sample for each new functionality and this is no
exception. No bells and whistles, just something to test the results are as
expected. This one is under the order_target
directory in the samples.
The logic in the sample is rather dumb and only meaant for testing:
-
During odd months (Jan, Mar, …), use the day as target (in the case of
order_target_value
multiplying the day by1000
)This mimics an increasing target
-
During even months (Feb, Apr, …) use
31 - day
as the targetThis mimics an decreasing target
order_target_size
Let’s see what happens in Jan and Feb.
$ ./order_target.py --target-size -- plot 0001 - 2005-01-03 - Position Size: 00 - Value 1000000.00 0001 - 2005-01-03 - Order Target Size: 03 0002 - 2005-01-04 - Position Size: 03 - Value 999994.39 0002 - 2005-01-04 - Order Target Size: 04 0003 - 2005-01-05 - Position Size: 04 - Value 999992.48 0003 - 2005-01-05 - Order Target Size: 05 0004 - 2005-01-06 - Position Size: 05 - Value 999988.79 ... 0020 - 2005-01-31 - Position Size: 28 - Value 999968.70 0020 - 2005-01-31 - Order Target Size: 31 0021 - 2005-02-01 - Position Size: 31 - Value 999954.68 0021 - 2005-02-01 - Order Target Size: 30 0022 - 2005-02-02 - Position Size: 30 - Value 999979.65 0022 - 2005-02-02 - Order Target Size: 29 0023 - 2005-02-03 - Position Size: 29 - Value 999966.33 0023 - 2005-02-03 - Order Target Size: 28 ...
In Jan the target starts at 3
with the 1st trading day of the year and
increases. And the position size moves initially from 0
to 3
and then
in increments of 1
.
Finishing Jan the last order_target is for 31
and that position size
is reported when entering the 1st day of Feb, when the new target side is
requested to be 30
and goes changing along with the position in decrements
of ´1`.
order_target_value
A similar behavior is expected from target values
$ ./order_target.py --target-value --plot 0001 - 2005-01-03 - Position Size: 00 - Value 1000000.00 0001 - 2005-01-03 - data value 0.00 0001 - 2005-01-03 - Order Target Value: 3000.00 0002 - 2005-01-04 - Position Size: 78 - Value 999854.14 0002 - 2005-01-04 - data value 2853.24 0002 - 2005-01-04 - Order Target Value: 4000.00 0003 - 2005-01-05 - Position Size: 109 - Value 999801.68 0003 - 2005-01-05 - data value 3938.17 0003 - 2005-01-05 - Order Target Value: 5000.00 0004 - 2005-01-06 - Position Size: 138 - Value 999699.57 ... 0020 - 2005-01-31 - Position Size: 808 - Value 999206.37 0020 - 2005-01-31 - data value 28449.68 0020 - 2005-01-31 - Order Target Value: 31000.00 0021 - 2005-02-01 - Position Size: 880 - Value 998807.33 0021 - 2005-02-01 - data value 30580.00 0021 - 2005-02-01 - Order Target Value: 30000.00 0022 - 2005-02-02 - Position Size: 864 - Value 999510.21 0022 - 2005-02-02 - data value 30706.56 0022 - 2005-02-02 - Order Target Value: 29000.00 0023 - 2005-02-03 - Position Size: 816 - Value 999130.05 0023 - 2005-02-03 - data value 28633.44 0023 - 2005-02-03 - Order Target Value: 28000.00 ...
There is an extra line of information telling what the actual data value (in the portfolio) is. This helps in finding out if the target value has been reachec.
The initial target is 3000.0
and the reported initial value is
2853.24
. The question here is whether this is close enough. And the
answer is Yes
-
The sample uses a
Market
order at the end of a daily bar and the last available price to calculate a target size which meets the target value -
The execution uses then the
open
price of the next day and this is unlikely to be the previousclose
Doing it in any other way would mean one is cheating him/herfself.
The next target value and final value are much closer: 4000
and
3938.17
.
When changing into Feb the target value starts decreasing from 31000
to
30000
and 29000\
. So does the *data value* with from ``30580.00to
30706.56and then to
28633.44`. Wait:
-
30580
->30706.56
is a positive changeIndeed. In this case the calculated size for the target value met an opening price which bumped the value to
30706.56
How this effect can be avoided:
-
The sample uses a
Market
type execution for the orders and this effect cannot be avoided -
The methods
order_target_xxx
allow specifying the execution type and price.One could specify
Limit
as the execution order and let the price be the close price (chosen by the method if nothing else be provided) or even provide specific pricing
order_target_value
In this case it is simply a percentage of the current portfolio value.
$ ./order_target.py --target-percent --plot 0001 - 2005-01-03 - Position Size: 00 - Value 1000000.00 0001 - 2005-01-03 - data percent 0.00 0001 - 2005-01-03 - Order Target Percent: 0.03 0002 - 2005-01-04 - Position Size: 785 - Value 998532.05 0002 - 2005-01-04 - data percent 0.03 0002 - 2005-01-04 - Order Target Percent: 0.04 0003 - 2005-01-05 - Position Size: 1091 - Value 998007.44 0003 - 2005-01-05 - data percent 0.04 0003 - 2005-01-05 - Order Target Percent: 0.05 0004 - 2005-01-06 - Position Size: 1381 - Value 996985.64 ... 0020 - 2005-01-31 - Position Size: 7985 - Value 991966.28 0020 - 2005-01-31 - data percent 0.28 0020 - 2005-01-31 - Order Target Percent: 0.31 0021 - 2005-02-01 - Position Size: 8733 - Value 988008.94 0021 - 2005-02-01 - data percent 0.31 0021 - 2005-02-01 - Order Target Percent: 0.30 0022 - 2005-02-02 - Position Size: 8530 - Value 995005.45 0022 - 2005-02-02 - data percent 0.30 0022 - 2005-02-02 - Order Target Percent: 0.29 0023 - 2005-02-03 - Position Size: 8120 - Value 991240.75 0023 - 2005-02-03 - data percent 0.29 0023 - 2005-02-03 - Order Target Percent: 0.28 ...
And the information has been changed to see the %
the data represents in
the portfolio.
Sample Usage
$ ./order_target.py --help usage: order_target.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--cash CASH] (--target-size | --target-value | --target-percent) [--plot [kwargs]] Sample for Order Target optional arguments: -h, --help show this help message and exit --data DATA Specific data to be read in (default: ../../datas/yhoo-1996-2015.txt) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: 2005-01-01) --todate TODATE Ending date in YYYY-MM-DD format (default: 2006-12-31) --cash CASH Ending date in YYYY-MM-DD format (default: 1000000) --target-size Use order_target_size (default: False) --target-value Use order_target_value (default: False) --target-percent Use order_target_percent (default: False) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style="candle" (to plot candles) (default: None)
Sample Code
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse from datetime import datetime import backtrader as bt class TheStrategy(bt.Strategy): ''' This strategy is loosely based on some of the examples from the Van K. Tharp book: *Trade Your Way To Financial Freedom*. The logic: - Enter the market if: - The MACD.macd line crosses the MACD.signal line to the upside - The Simple Moving Average has a negative direction in the last x periods (actual value below value x periods ago) - Set a stop price x times the ATR value away from the close - If in the market: - Check if the current close has gone below the stop price. If yes, exit. - If not, update the stop price if the new stop price would be higher than the current ''' params = ( ('use_target_size', False), ('use_target_value', False), ('use_target_percent', False), ) def notify_order(self, order): if order.status == order.Completed: pass if not order.alive(): self.order = None # indicate no order is pending def start(self): self.order = None # sentinel to avoid operrations on pending order def next(self): dt = self.data.datetime.date() portfolio_value = self.broker.get_value() print('%04d - %s - Position Size: %02d - Value %.2f' % (len(self), dt.isoformat(), self.position.size, portfolio_value)) data_value = self.broker.get_value([self.data]) if self.p.use_target_value: print('%04d - %s - data value %.2f' % (len(self), dt.isoformat(), data_value)) elif self.p.use_target_percent: port_perc = data_value / portfolio_value print('%04d - %s - data percent %.2f' % (len(self), dt.isoformat(), port_perc)) if self.order: return # pending order execution size = dt.day if (dt.month % 2) == 0: size = 31 - size if self.p.use_target_size: target = size print('%04d - %s - Order Target Size: %02d' % (len(self), dt.isoformat(), size)) self.order = self.order_target_size(target=size) elif self.p.use_target_value: value = size * 1000 print('%04d - %s - Order Target Value: %.2f' % (len(self), dt.isoformat(), value)) self.order = self.order_target_value(target=value) elif self.p.use_target_percent: percent = size / 100.0 print('%04d - %s - Order Target Percent: %.2f' % (len(self), dt.isoformat(), percent)) self.order = self.order_target_percent(target=percent) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() cerebro.broker.setcash(args.cash) dkwargs = dict() if args.fromdate is not None: dkwargs['fromdate'] = datetime.strptime(args.fromdate, '%Y-%m-%d') if args.todate is not None: dkwargs['todate'] = datetime.strptime(args.todate, '%Y-%m-%d') # data data = bt.feeds.YahooFinanceCSVData(dataname=args.data, **dkwargs) cerebro.adddata(data) # strategy cerebro.addstrategy(TheStrategy, use_target_size=args.target_size, use_target_value=args.target_value, use_target_percent=args.target_percent) cerebro.run() if args.plot: pkwargs = dict(style='bar') if args.plot is not True: # evals to True but is not True npkwargs = eval('dict(' + args.plot + ')') # args were passed pkwargs.update(npkwargs) cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for Order Target') parser.add_argument('--data', required=False, default='../../datas/yhoo-1996-2015.txt', help='Specific data to be read in') parser.add_argument('--fromdate', required=False, default='2005-01-01', help='Starting date in YYYY-MM-DD format') parser.add_argument('--todate', required=False, default='2006-12-31', help='Ending date in YYYY-MM-DD format') parser.add_argument('--cash', required=False, action='store', type=float, default=1000000, help='Ending date in YYYY-MM-DD format') pgroup = parser.add_mutually_exclusive_group(required=True) pgroup.add_argument('--target-size', required=False, action='store_true', help=('Use order_target_size')) pgroup.add_argument('--target-value', required=False, action='store_true', help=('Use order_target_value')) pgroup.add_argument('--target-percent', required=False, action='store_true', help=('Use order_target_percent')) # Plot options parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' '\n' 'For example:\n' '\n' ' --plot style="candle" (to plot candles)\n')) if pargs is not None: return parser.parse_args(pargs) return parser.parse_args() if __name__ == '__main__': runstrat()