Skip to content

Commit 5340474

Browse files
author
qacollective
committed
Add tagging, object typing, fix pep8
1 parent bbcb7ba commit 5340474

File tree

1 file changed

+50
-13
lines changed

1 file changed

+50
-13
lines changed

backtesting/backtesting.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Strategy(metaclass=ABCMeta):
4747
`backtesting.backtesting.Strategy.next` to define
4848
your own strategy.
4949
"""
50+
5051
def __init__(self, broker, data, params):
5152
self._indicators = []
5253
self._broker: _Broker = broker
@@ -193,30 +194,32 @@ def buy(self, *,
193194
limit: float = None,
194195
stop: float = None,
195196
sl: float = None,
196-
tp: float = None):
197+
tp: float = None,
198+
tag: object = None):
197199
"""
198200
Place a new long order. For explanation of parameters, see `Order` and its properties.
199201
200202
See also `Strategy.sell()`.
201203
"""
202204
assert 0 < size < 1 or round(size) == size, \
203205
"size must be a positive fraction of equity, or a positive whole number of units"
204-
return self._broker.new_order(size, limit, stop, sl, tp)
206+
return self._broker.new_order(size, limit, stop, sl, tp, tag)
205207

206208
def sell(self, *,
207209
size: float = 1 - sys.float_info.epsilon,
208210
limit: float = None,
209211
stop: float = None,
210212
sl: float = None,
211-
tp: float = None):
213+
tp: float = None,
214+
tag: object = None):
212215
"""
213216
Place a new short order. For explanation of parameters, see `Order` and its properties.
214217
215218
See also `Strategy.buy()`.
216219
"""
217220
assert 0 < size < 1 or round(size) == size, \
218221
"size must be a positive fraction of equity, or a positive whole number of units"
219-
return self._broker.new_order(-size, limit, stop, sl, tp)
222+
return self._broker.new_order(-size, limit, stop, sl, tp, tag)
220223

221224
@property
222225
def equity(self) -> float:
@@ -277,6 +280,7 @@ class _Orders(tuple):
277280
"""
278281
TODO: remove this class. Only for deprecation.
279282
"""
283+
280284
def cancel(self):
281285
"""Cancel all non-contingent (i.e. SL/TP) orders."""
282286
for order in self:
@@ -304,6 +308,7 @@ class Position:
304308
if self.position:
305309
... # we have a position, either long or short
306310
"""
311+
307312
def __init__(self, broker: '_Broker'):
308313
self.__broker = broker
309314

@@ -368,13 +373,15 @@ class Order:
368373
[filled]: https://www.investopedia.com/terms/f/fill.asp
369374
[Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp
370375
"""
376+
371377
def __init__(self, broker: '_Broker',
372378
size: float,
373379
limit_price: float = None,
374380
stop_price: float = None,
375381
sl_price: float = None,
376382
tp_price: float = None,
377-
parent_trade: 'Trade' = None):
383+
parent_trade: 'Trade' = None,
384+
tag: object = None):
378385
self.__broker = broker
379386
assert size != 0
380387
self.__size = size
@@ -383,6 +390,7 @@ def __init__(self, broker: '_Broker',
383390
self.__sl_price = sl_price
384391
self.__tp_price = tp_price
385392
self.__parent_trade = parent_trade
393+
self.__tag = tag
386394

387395
def _replace(self, **kwargs):
388396
for k, v in kwargs.items():
@@ -398,6 +406,7 @@ def __repr__(self):
398406
('sl', self.__sl_price),
399407
('tp', self.__tp_price),
400408
('contingent', self.is_contingent),
409+
('tag', self.__tag),
401410
) if value is not None))
402411

403412
def cancel(self):
@@ -468,6 +477,15 @@ def tp(self) -> Optional[float]:
468477
def parent_trade(self):
469478
return self.__parent_trade
470479

480+
@property
481+
def tag(self) -> Optional[object]:
482+
"""
483+
An attribute which, if set, persists to enable tracking of this order
484+
by an external identifier if it becomes a trade in `Strategy.trades`
485+
and when closed in `Strategy.closed_trades`.
486+
"""
487+
return self.__tag
488+
471489
__pdoc__['Order.parent_trade'] = False
472490

473491
# Extra properties
@@ -502,7 +520,8 @@ class Trade:
502520
When an `Order` is filled, it results in an active `Trade`.
503521
Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`.
504522
"""
505-
def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar):
523+
524+
def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar, tag: object):
506525
self.__broker = broker
507526
self.__size = size
508527
self.__entry_price = entry_price
@@ -511,10 +530,12 @@ def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar):
511530
self.__exit_bar: Optional[int] = None
512531
self.__sl_order: Optional[Order] = None
513532
self.__tp_order: Optional[Order] = None
533+
self.__tag = tag
514534

515535
def __repr__(self):
516536
return f'<Trade size={self.__size} time={self.__entry_bar}-{self.__exit_bar or ""} ' \
517-
f'price={self.__entry_price}-{self.__exit_price or ""} pl={self.pl:.0f}>'
537+
f'price={self.__entry_price}-{self.__exit_price or ""} pl={self.pl:.0f}' \
538+
f'{" tag="+str(self.__tag) if self.__tag is not None else ""}>'
518539

519540
def _replace(self, **kwargs):
520541
for k, v in kwargs.items():
@@ -528,7 +549,7 @@ def close(self, portion: float = 1.):
528549
"""Place new `Order` to close `portion` of the trade at next market price."""
529550
assert 0 < portion <= 1, "portion must be a fraction between 0 and 1"
530551
size = copysign(max(1, round(abs(self.__size) * portion)), -self.__size)
531-
order = Order(self.__broker, size, parent_trade=self)
552+
order = Order(self.__broker, size, parent_trade=self, tag=self.__tag)
532553
self.__broker.orders.insert(0, order)
533554

534555
# Fields getters
@@ -561,6 +582,15 @@ def exit_bar(self) -> Optional[int]:
561582
"""
562583
return self.__exit_bar
563584

585+
@property
586+
def tag(self) -> Optional[object]:
587+
"""
588+
A tag attribute optionally set when placing an order with
589+
`Strategy.buy()` or `Strategy.sell()`.
590+
See `Order.tag`.
591+
"""
592+
return self.__tag
593+
564594
@property
565595
def _sl_order(self):
566596
return self.__sl_order
@@ -652,7 +682,7 @@ def __set_contingent(self, type, price):
652682
order.cancel()
653683
if price:
654684
kwargs = dict(stop=price) if type == 'sl' else dict(limit=price)
655-
order = self.__broker.new_order(-self.size, trade=self, **kwargs)
685+
order = self.__broker.new_order(-self.size, trade=self, tag=self.tag, **kwargs)
656686
setattr(self, attr, order)
657687

658688

@@ -685,6 +715,7 @@ def new_order(self,
685715
stop: float = None,
686716
sl: float = None,
687717
tp: float = None,
718+
tag: object = None,
688719
*,
689720
trade: Trade = None):
690721
"""
@@ -710,7 +741,7 @@ def new_order(self,
710741
"Short orders require: "
711742
f"TP ({tp}) < LIMIT ({limit or stop or adjusted_price}) < SL ({sl})")
712743

713-
order = Order(self, size, limit, stop, sl, tp, trade)
744+
order = Order(self, size, limit, stop, sl, tp, trade, tag)
714745
# Put the new order in the order queue,
715746
# inserting SL/TP/trade-closing orders in-front
716747
if trade:
@@ -890,7 +921,12 @@ def _process_orders(self):
890921

891922
# Open a new trade
892923
if need_size:
893-
self._open_trade(adjusted_price, need_size, order.sl, order.tp, time_index)
924+
self._open_trade(adjusted_price,
925+
need_size,
926+
order.sl,
927+
order.tp,
928+
time_index,
929+
order.tag)
894930

895931
# We need to reprocess the SL/TP orders newly added to the queue.
896932
# This allows e.g. SL hitting in the same bar the order was open.
@@ -948,8 +984,8 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
948984
self.closed_trades.append(trade._replace(exit_price=price, exit_bar=time_index))
949985
self._cash += trade.pl
950986

951-
def _open_trade(self, price: float, size: int, sl: float, tp: float, time_index: int):
952-
trade = Trade(self, size, price, time_index)
987+
def _open_trade(self, price: float, size: int, sl: float, tp: float, time_index: int, tag: object):
988+
trade = Trade(self, size, price, time_index, tag)
953989
self.trades.append(trade)
954990
# Create SL/TP (bracket) orders.
955991
# Make sure SL order is created first so it gets adversarially processed before TP order
@@ -971,6 +1007,7 @@ class Backtest:
9711007
instance, or `backtesting.backtesting.Backtest.optimize` to
9721008
optimize it.
9731009
"""
1010+
9741011
def __init__(self,
9751012
data: pd.DataFrame,
9761013
strategy: Type[Strategy],

0 commit comments

Comments
 (0)