Skip to content

ENH: Add exit_tag support for trades and positions to indicate closur… - #1403

Open
Harsh4r0ra wants to merge 1 commit into
kernc:masterfrom
Harsh4r0ra:feat/trade-exit-tag
Open

ENH: Add exit_tag support for trades and positions to indicate closur…#1403
Harsh4r0ra wants to merge 1 commit into
kernc:masterfrom
Harsh4r0ra:feat/trade-exit-tag

Conversation

@Harsh4r0ra

Copy link
Copy Markdown

Expose why a trade closed: tag= on Position.close()/Trade.close(), Trade.exit_tag, auto "sl"/"tp" tagging

Closes [kernc/backtesting.py#1352](#1352)

Summary

Order.tag/Trade.tag already let you tag why a trade was opened, but there was no way to know why a trade was closed — hit its stop-loss, hit its take-profit, an explicit position.close()/trade.close() call, or closed as a side effect of an opposing signal. Trade.close() always re-used the trade's opening tag on the closing order, so that information was lost.

This implements the design agreed on in the issue thread between kernc and atharvajoshi01:

  1. Position.close() / Trade.close() gain an optional keyword-only tag= so callers can say why they're closing, e.g. self.position.close(tag="stop condition X").
  2. SL/TP-triggered closes are auto-tagged "sl" / "tp" with no extra API needed — the reason is always inferable.
  3. A new Trade.exit_tag property surfaces this, parallel to the existing (opening) Trade.tag.
  4. A new 'ExitTag' column is added to stats._trades, alongside the existing 'Tag' column (which keeps its current meaning — the opening tag — unchanged).

Changes

backtesting/backtesting.py

  • Position.close(self, portion=1., *, tag=None) — threads tag to each trade.close(portion, tag=tag) call.
  • Trade.close(self, portion=1., *, tag=None) — uses tag for the closing order if given; otherwise falls back to the trade's opening tag (unchanged behavior — fully backward compatible).
  • Trade.exit_tag (new property) — the tag of the order that closed the trade; None while the trade is still open.
  • Trade.tag is untouched — still the opening tag, fixed for the life of the trade.
  • _Broker._process_orders() — when the closing order is trade._sl_order or trade._tp_order, the exit tag defaults to "sl"/"tp" unless that order's tag was already explicitly set to something other than the trade's opening tag (i.e. order.tag == trade.tag is treated as "unset"). An explicit trade.close(tag=...)/position.close(tag=...) order always uses its own tag and is never overwritten.
  • _Broker._reduce_trade() / _close_trade() — gained an exit_tag=None parameter that's threaded through and stamped onto the closed Trade via trade._replace(exit_tag=...). For partial closes, _reduce_trade() correctly stamps the tag onto the closed copy of the trade, not the remaining open one.

backtesting/_stats.py

  • Added 'ExitTag' column to trades_df, sourced from t.exit_tag, next to the existing 'Tag' column. 'Tag' itself is untouched.

backtesting/test/_test.py

  • New tests covering: explicit tag= on trade.close()/position.close(); automatic "sl"/"tp" tagging on SL/TP-triggered exits; the None default for a trade closed via an opposing order fill (no trade.close() call at all); a partial close landing the tag on the correct (closed) half while the remaining open trade keeps exit_tag is None; and backward compatibility (no tag= used anywhere still produces identical Trade.tag/'Tag' behavior as before).
  • Updated test_compute_stats's hardcoded list of expected stats._trades columns to include 'ExitTag'.

CHANGELOG.md

  • Added an entry under ### 0.x.x (unreleased).

Judgment calls — flagging for @kernc

  1. Exit tag for a trade closed via an opposing order fill (FIFO close), with no trade.close() call anywhere: I default this to None. There's no natural tag to infer here since the close isn't driven by any tagged order/call — happy to change this if you'd rather it inherit the new order's tag instead.
  2. "Was the SL/TP order's tag explicitly overridden?" rule: I use order.tag == trade.tag to mean "not explicitly set," since Trade.__set_contingent() (backing trade.sl = / trade.tp =) doesn't currently accept a custom tag for the contingent order — so in practice this branch always fires today. This just future-proofs the rule per your note in the issue, without adding new public API surface right now (no tag= kwarg was requested for sl/tp assignment itself).

Test plan

python -m pytest backtesting/test/_test.py -v -k "trade or close or tag or sl or tp"  # 35 passed
python -m pytest backtesting/test/_test.py -v                                        # 86 passed, 1 skipped (pre-existing)
flake8 backtesting/backtesting.py backtesting/_stats.py backtesting/test/_test.py    # clean
mypy backtesting/backtesting.py --config-file setup.cfg                              # same 13 pre-existing errors as on master, 0 new

Red/green-verified: temporarily disabled the SL/TP auto-tagging branch — the two new SL/TP tests failed as expected (None != 'sl', None != 'tp'); restored and both pass again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant