@@ -80,7 +80,7 @@ def I(self, # noqa: E743
8080 name = None , plot = True , overlay = None , color = None , scatter = False ,
8181 ** kwargs ) -> np .ndarray :
8282 """
83- Declare indicator. An indicator is just an array of values,
83+ Declare an indicator. An indicator is just an array of values,
8484 but one that is revealed gradually in
8585 `backtesting.backtesting.Strategy.next` much like
8686 `backtesting.backtesting.Strategy.data` is.
@@ -133,7 +133,7 @@ def init():
133133
134134 if value is not None :
135135 value = try_ (lambda : np .asarray (value , order = 'C' ), None )
136- is_arraylike = value is not None
136+ is_arraylike = bool ( value is not None and value . shape )
137137
138138 # Optionally flip the array if the user returned e.g. `df.values`
139139 if is_arraylike and np .argmax (value .shape ) == 0 :
@@ -142,7 +142,7 @@ def init():
142142 if not is_arraylike or not 1 <= value .ndim <= 2 or value .shape [- 1 ] != len (self ._data .Close ):
143143 raise ValueError (
144144 'Indicators must return (optionally a tuple of) numpy.arrays of same '
145- f'length as `data` (data shape: { self ._data .Close .shape } ; indicator "{ name } "'
145+ f'length as `data` (data shape: { self ._data .Close .shape } ; indicator "{ name } " '
146146 f'shape: { getattr (value , "shape" , "" )} , returned value: { value } )' )
147147
148148 if plot and overlay is None and np .issubdtype (value .dtype , np .number ):
@@ -199,7 +199,8 @@ def buy(self, *,
199199 limit : Optional [float ] = None ,
200200 stop : Optional [float ] = None ,
201201 sl : Optional [float ] = None ,
202- tp : Optional [float ] = None ):
202+ tp : Optional [float ] = None ,
203+ tag : object = None ):
203204 """
204205 Place a new long order. For explanation of parameters, see `Order` and its properties.
205206
@@ -209,14 +210,15 @@ def buy(self, *,
209210 """
210211 assert 0 < size < 1 or round (size ) == size , \
211212 "size must be a positive fraction of equity, or a positive whole number of units"
212- return self ._broker .new_order (size , limit , stop , sl , tp )
213+ return self ._broker .new_order (size , limit , stop , sl , tp , tag )
213214
214215 def sell (self , * ,
215216 size : float = _FULL_EQUITY ,
216217 limit : Optional [float ] = None ,
217218 stop : Optional [float ] = None ,
218219 sl : Optional [float ] = None ,
219- tp : Optional [float ] = None ):
220+ tp : Optional [float ] = None ,
221+ tag : object = None ):
220222 """
221223 Place a new short order. For explanation of parameters, see `Order` and its properties.
222224
@@ -228,7 +230,7 @@ def sell(self, *,
228230 """
229231 assert 0 < size < 1 or round (size ) == size , \
230232 "size must be a positive fraction of equity, or a positive whole number of units"
231- return self ._broker .new_order (- size , limit , stop , sl , tp )
233+ return self ._broker .new_order (- size , limit , stop , sl , tp , tag )
232234
233235 @property
234236 def equity (self ) -> float :
@@ -386,7 +388,8 @@ def __init__(self, broker: '_Broker',
386388 stop_price : Optional [float ] = None ,
387389 sl_price : Optional [float ] = None ,
388390 tp_price : Optional [float ] = None ,
389- parent_trade : Optional ['Trade' ] = None ):
391+ parent_trade : Optional ['Trade' ] = None ,
392+ tag : object = None ):
390393 self .__broker = broker
391394 assert size != 0
392395 self .__size = size
@@ -395,6 +398,7 @@ def __init__(self, broker: '_Broker',
395398 self .__sl_price = sl_price
396399 self .__tp_price = tp_price
397400 self .__parent_trade = parent_trade
401+ self .__tag = tag
398402
399403 def _replace (self , ** kwargs ):
400404 for k , v in kwargs .items ():
@@ -410,6 +414,7 @@ def __repr__(self):
410414 ('sl' , self .__sl_price ),
411415 ('tp' , self .__tp_price ),
412416 ('contingent' , self .is_contingent ),
417+ ('tag' , self .__tag ),
413418 ) if value is not None ))
414419
415420 def cancel (self ):
@@ -481,6 +486,14 @@ def tp(self) -> Optional[float]:
481486 def parent_trade (self ):
482487 return self .__parent_trade
483488
489+ @property
490+ def tag (self ):
491+ """
492+ Arbitrary value (such as a string) which, if set, enables tracking
493+ of this order and the associated `Trade` (see `Trade.tag`).
494+ """
495+ return self .__tag
496+
484497 __pdoc__ ['Order.parent_trade' ] = False
485498
486499 # Extra properties
@@ -515,7 +528,7 @@ class Trade:
515528 When an `Order` is filled, it results in an active `Trade`.
516529 Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`.
517530 """
518- def __init__ (self , broker : '_Broker' , size : int , entry_price : float , entry_bar ):
531+ def __init__ (self , broker : '_Broker' , size : int , entry_price : float , entry_bar , tag ):
519532 self .__broker = broker
520533 self .__size = size
521534 self .__entry_price = entry_price
@@ -524,10 +537,12 @@ def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar):
524537 self .__exit_bar : Optional [int ] = None
525538 self .__sl_order : Optional [Order ] = None
526539 self .__tp_order : Optional [Order ] = None
540+ self .__tag = tag
527541
528542 def __repr__ (self ):
529543 return f'<Trade size={ self .__size } time={ self .__entry_bar } -{ self .__exit_bar or "" } ' \
530- f'price={ self .__entry_price } -{ self .__exit_price or "" } pl={ self .pl :.0f} >'
544+ f'price={ self .__entry_price } -{ self .__exit_price or "" } pl={ self .pl :.0f} ' \
545+ f'{ " tag=" + str (self .__tag ) if self .__tag is not None else "" } >'
531546
532547 def _replace (self , ** kwargs ):
533548 for k , v in kwargs .items ():
@@ -541,7 +556,7 @@ def close(self, portion: float = 1.):
541556 """Place new `Order` to close `portion` of the trade at next market price."""
542557 assert 0 < portion <= 1 , "portion must be a fraction between 0 and 1"
543558 size = copysign (max (1 , round (abs (self .__size ) * portion )), - self .__size )
544- order = Order (self .__broker , size , parent_trade = self )
559+ order = Order (self .__broker , size , parent_trade = self , tag = self . __tag )
545560 self .__broker .orders .insert (0 , order )
546561
547562 # Fields getters
@@ -574,6 +589,19 @@ def exit_bar(self) -> Optional[int]:
574589 """
575590 return self .__exit_bar
576591
592+ @property
593+ def tag (self ):
594+ """
595+ A tag value inherited from the `Order` that opened
596+ this trade.
597+
598+ This can be used to track trades and apply conditional
599+ logic / subgroup analysis.
600+
601+ See also `Order.tag`.
602+ """
603+ return self .__tag
604+
577605 @property
578606 def _sl_order (self ):
579607 return self .__sl_order
@@ -665,7 +693,7 @@ def __set_contingent(self, type, price):
665693 order .cancel ()
666694 if price :
667695 kwargs = {'stop' : price } if type == 'sl' else {'limit' : price }
668- order = self .__broker .new_order (- self .size , trade = self , ** kwargs )
696+ order = self .__broker .new_order (- self .size , trade = self , tag = self . tag , ** kwargs )
669697 setattr (self , attr , order )
670698
671699
@@ -700,6 +728,7 @@ def new_order(self,
700728 stop : Optional [float ] = None ,
701729 sl : Optional [float ] = None ,
702730 tp : Optional [float ] = None ,
731+ tag : object = None ,
703732 * ,
704733 trade : Optional [Trade ] = None ):
705734 """
@@ -725,7 +754,7 @@ def new_order(self,
725754 "Short orders require: "
726755 f"TP ({ tp } ) < LIMIT ({ limit or stop or adjusted_price } ) < SL ({ sl } )" )
727756
728- order = Order (self , size , limit , stop , sl , tp , trade )
757+ order = Order (self , size , limit , stop , sl , tp , trade , tag )
729758 # Put the new order in the order queue,
730759 # inserting SL/TP/trade-closing orders in-front
731760 if trade :
@@ -905,7 +934,12 @@ def _process_orders(self):
905934
906935 # Open a new trade
907936 if need_size :
908- self ._open_trade (adjusted_price , need_size , order .sl , order .tp , time_index )
937+ self ._open_trade (adjusted_price ,
938+ need_size ,
939+ order .sl ,
940+ order .tp ,
941+ time_index ,
942+ order .tag )
909943
910944 # We need to reprocess the SL/TP orders newly added to the queue.
911945 # This allows e.g. SL hitting in the same bar the order was open.
@@ -964,8 +998,8 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
964998 self ._cash += trade .pl
965999
9661000 def _open_trade (self , price : float , size : int ,
967- sl : Optional [float ], tp : Optional [float ], time_index : int ):
968- trade = Trade (self , size , price , time_index )
1001+ sl : Optional [float ], tp : Optional [float ], time_index : int , tag ):
1002+ trade = Trade (self , size , price , time_index , tag )
9691003 self .trades .append (trade )
9701004 # Create SL/TP (bracket) orders.
9711005 # Make sure SL order is created first so it gets adversarially processed before TP order
@@ -1143,6 +1177,13 @@ def run(self, **kwargs) -> pd.Series:
11431177 _equity_curve Eq...
11441178 _trades Size EntryB...
11451179 dtype: object
1180+
1181+ .. warning::
1182+ You may obtain different results for different strategy parameters.
1183+ E.g. if you use 50- and 200-bar SMA, the trading simulation will
1184+ begin on bar 201. The actual length of delay is equal to the lookback
1185+ period of the `Strategy.I` indicator which lags the most.
1186+ Obviously, this can affect results.
11461187 """
11471188 data = _Data (self ._data .copy (deep = False ))
11481189 broker : _Broker = self ._broker (data = data )
@@ -1518,7 +1559,7 @@ def _mp_task(backtest_uuid, batch_index):
15181559
15191560 def plot (self , * , results : pd .Series = None , filename = None , plot_width = None ,
15201561 plot_equity = True , plot_return = False , plot_pl = True ,
1521- plot_volume = True , plot_drawdown = False ,
1562+ plot_volume = True , plot_drawdown = False , plot_trades = True ,
15221563 smooth_equity = False , relative_equity = True ,
15231564 superimpose : Union [bool , str ] = True ,
15241565 resample = True , reverse_indicators = False ,
@@ -1557,6 +1598,9 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
15571598 If `plot_drawdown` is `True`, the resulting plot will contain
15581599 a separate drawdown graph section.
15591600
1601+ If `plot_trades` is `True`, the stretches between trade entries
1602+ and trade exits are marked by hash-marked tractor beams.
1603+
15601604 If `smooth_equity` is `True`, the equity graph will be
15611605 interpolated between fixed points at trade closing times,
15621606 unaffected by any interim asset volatility.
@@ -1615,6 +1659,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
16151659 plot_pl = plot_pl ,
16161660 plot_volume = plot_volume ,
16171661 plot_drawdown = plot_drawdown ,
1662+ plot_trades = plot_trades ,
16181663 smooth_equity = smooth_equity ,
16191664 relative_equity = relative_equity ,
16201665 superimpose = superimpose ,
0 commit comments