Skip to content

Add Order.tag for tracking orders and trades #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backtesting/_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def compute_stats(
'ReturnPct': [t.pl_pct for t in trades],
'EntryTime': [t.entry_time for t in trades],
'ExitTime': [t.exit_time for t in trades],
'Tag': [t.tag for t in trades],
})
trades_df['Duration'] = trades_df['ExitTime'] - trades_df['EntryTime']
del trades
Expand Down
60 changes: 47 additions & 13 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ def buy(self, *,
limit: Optional[float] = None,
stop: Optional[float] = None,
sl: Optional[float] = None,
tp: Optional[float] = None):
tp: Optional[float] = None,
tag: object = None):
"""
Place a new long order. For explanation of parameters, see `Order` and its properties.

Expand All @@ -209,14 +210,15 @@ def buy(self, *,
"""
assert 0 < size < 1 or round(size) == size, \
"size must be a positive fraction of equity, or a positive whole number of units"
return self._broker.new_order(size, limit, stop, sl, tp)
return self._broker.new_order(size, limit, stop, sl, tp, tag)

def sell(self, *,
size: float = _FULL_EQUITY,
limit: Optional[float] = None,
stop: Optional[float] = None,
sl: Optional[float] = None,
tp: Optional[float] = None):
tp: Optional[float] = None,
tag: object = None):
"""
Place a new short order. For explanation of parameters, see `Order` and its properties.

Expand All @@ -228,7 +230,7 @@ def sell(self, *,
"""
assert 0 < size < 1 or round(size) == size, \
"size must be a positive fraction of equity, or a positive whole number of units"
return self._broker.new_order(-size, limit, stop, sl, tp)
return self._broker.new_order(-size, limit, stop, sl, tp, tag)

@property
def equity(self) -> float:
Expand Down Expand Up @@ -386,7 +388,8 @@ def __init__(self, broker: '_Broker',
stop_price: Optional[float] = None,
sl_price: Optional[float] = None,
tp_price: Optional[float] = None,
parent_trade: Optional['Trade'] = None):
parent_trade: Optional['Trade'] = None,
tag: object = None):
self.__broker = broker
assert size != 0
self.__size = size
Expand All @@ -395,6 +398,7 @@ def __init__(self, broker: '_Broker',
self.__sl_price = sl_price
self.__tp_price = tp_price
self.__parent_trade = parent_trade
self.__tag = tag

def _replace(self, **kwargs):
for k, v in kwargs.items():
Expand All @@ -410,6 +414,7 @@ def __repr__(self):
('sl', self.__sl_price),
('tp', self.__tp_price),
('contingent', self.is_contingent),
('tag', self.__tag),
) if value is not None))

def cancel(self):
Expand Down Expand Up @@ -481,6 +486,14 @@ def tp(self) -> Optional[float]:
def parent_trade(self):
return self.__parent_trade

@property
def tag(self):
"""
Arbitrary value (such as a string) which, if set, enables tracking
of this order and the associated `Trade` (see `Trade.tag`).
"""
return self.__tag

__pdoc__['Order.parent_trade'] = False

# Extra properties
Expand Down Expand Up @@ -515,7 +528,7 @@ class Trade:
When an `Order` is filled, it results in an active `Trade`.
Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`.
"""
def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar):
def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar, tag):
self.__broker = broker
self.__size = size
self.__entry_price = entry_price
Expand All @@ -524,10 +537,12 @@ def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar):
self.__exit_bar: Optional[int] = None
self.__sl_order: Optional[Order] = None
self.__tp_order: Optional[Order] = None
self.__tag = tag

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

def _replace(self, **kwargs):
for k, v in kwargs.items():
Expand All @@ -541,7 +556,7 @@ def close(self, portion: float = 1.):
"""Place new `Order` to close `portion` of the trade at next market price."""
assert 0 < portion <= 1, "portion must be a fraction between 0 and 1"
size = copysign(max(1, round(abs(self.__size) * portion)), -self.__size)
order = Order(self.__broker, size, parent_trade=self)
order = Order(self.__broker, size, parent_trade=self, tag=self.__tag)
self.__broker.orders.insert(0, order)

# Fields getters
Expand Down Expand Up @@ -574,6 +589,19 @@ def exit_bar(self) -> Optional[int]:
"""
return self.__exit_bar

@property
def tag(self):
"""
A tag value inherited from the `Order` that opened
this trade.

This can be used to track trades and apply conditional
logic / subgroup analysis.

See also `Order.tag`.
"""
return self.__tag

@property
def _sl_order(self):
return self.__sl_order
Expand Down Expand Up @@ -665,7 +693,7 @@ def __set_contingent(self, type, price):
order.cancel()
if price:
kwargs = {'stop': price} if type == 'sl' else {'limit': price}
order = self.__broker.new_order(-self.size, trade=self, **kwargs)
order = self.__broker.new_order(-self.size, trade=self, tag=self.tag, **kwargs)
setattr(self, attr, order)


Expand Down Expand Up @@ -700,6 +728,7 @@ def new_order(self,
stop: Optional[float] = None,
sl: Optional[float] = None,
tp: Optional[float] = None,
tag: object = None,
*,
trade: Optional[Trade] = None):
"""
Expand All @@ -725,7 +754,7 @@ def new_order(self,
"Short orders require: "
f"TP ({tp}) < LIMIT ({limit or stop or adjusted_price}) < SL ({sl})")

order = Order(self, size, limit, stop, sl, tp, trade)
order = Order(self, size, limit, stop, sl, tp, trade, tag)
# Put the new order in the order queue,
# inserting SL/TP/trade-closing orders in-front
if trade:
Expand Down Expand Up @@ -905,7 +934,12 @@ def _process_orders(self):

# Open a new trade
if need_size:
self._open_trade(adjusted_price, need_size, order.sl, order.tp, time_index)
self._open_trade(adjusted_price,
need_size,
order.sl,
order.tp,
time_index,
order.tag)

# We need to reprocess the SL/TP orders newly added to the queue.
# This allows e.g. SL hitting in the same bar the order was open.
Expand Down Expand Up @@ -964,8 +998,8 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
self._cash += trade.pl

def _open_trade(self, price: float, size: int,
sl: Optional[float], tp: Optional[float], time_index: int):
trade = Trade(self, size, price, time_index)
sl: Optional[float], tp: Optional[float], time_index: int, tag):
trade = Trade(self, size, price, time_index, tag)
self.trades.append(trade)
# Create SL/TP (bracket) orders.
# Make sure SL order is created first so it gets adversarially processed before TP order
Expand Down
14 changes: 13 additions & 1 deletion backtesting/test/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def almost_equal(a, b):
self.assertSequenceEqual(
sorted(stats['_trades'].columns),
sorted(['Size', 'EntryBar', 'ExitBar', 'EntryPrice', 'ExitPrice',
'PnL', 'ReturnPct', 'EntryTime', 'ExitTime', 'Duration']))
'PnL', 'ReturnPct', 'EntryTime', 'ExitTime', 'Duration', 'Tag']))

def test_compute_stats_bordercase(self):

Expand Down Expand Up @@ -506,6 +506,18 @@ def coroutine(self):
stats = self._Backtest(coroutine).run()
self.assertEqual(len(stats._trades), 1)

def test_order_tag(self):
def coroutine(self):
yield self.buy(size=2, tag=1)
yield self.sell(size=1, tag='s')
yield self.sell(size=1)

yield self.buy(tag=2)
yield self.position.close()

stats = self._Backtest(coroutine).run()
self.assertEqual(list(stats._trades.Tag), [1, 1, 2])


class TestOptimize(TestCase):
def test_optimize(self):
Expand Down