@@ -367,12 +367,15 @@ def is_short(self) -> bool:
367367 """True if the position is short (position size is negative)."""
368368 return self .size < 0
369369
370- def close (self , portion : float = 1. ):
370+ def close (self , portion : float = 1. , * , tag = None ):
371371 """
372372 Close portion of position by closing `portion` of each active trade. See `Trade.close`.
373+
374+ If `tag` is given, it is passed through to each `Trade.close()` call,
375+ ending up as `Trade.exit_tag` of every closed trade.
373376 """
374377 for trade in self .__broker .trades :
375- trade .close (portion )
378+ trade .close (portion , tag = tag )
376379
377380 def __repr__ (self ):
378381 return f'<Position: { self .size } ({ len (self .__broker .trades )} trades)>'
@@ -555,6 +558,7 @@ def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar,
555558 self .__sl_order : Optional [Order ] = None
556559 self .__tp_order : Optional [Order ] = None
557560 self .__tag = tag
561+ self .__exit_tag = None
558562 self ._commissions = 0
559563
560564 def __repr__ (self ):
@@ -570,12 +574,20 @@ def _replace(self, **kwargs):
570574 def _copy (self , ** kwargs ):
571575 return copy (self )._replace (** kwargs )
572576
573- def close (self , portion : float = 1. ):
574- """Place new `Order` to close `portion` of the trade at next market price."""
577+ def close (self , portion : float = 1. , * , tag = None ):
578+ """
579+ Place new `Order` to close `portion` of the trade at next market price.
580+
581+ If `tag` is given, it is used as the tag of the closing `Order` and ends up as
582+ `Trade.exit_tag` of this trade once closed, so it can be used to record *why*
583+ the trade was closed. If not given, the closing order (and hence `Trade.exit_tag`)
584+ falls back to this trade's opening `Trade.tag`, preserving prior behavior.
585+ """
575586 assert 0 < portion <= 1 , "portion must be a fraction between 0 and 1"
576587 # Ensure size is an int to avoid rounding errors on 32-bit OS
577588 size = copysign (max (1 , int (round (abs (self .__size ) * portion ))), - self .__size )
578- order = Order (self .__broker , size , parent_trade = self , tag = self .__tag )
589+ order = Order (self .__broker , size , parent_trade = self ,
590+ tag = (self .__tag if tag is None else tag ))
579591 self .__broker .orders .insert (0 , order )
580592
581593 # Fields getters
@@ -621,6 +633,21 @@ def tag(self):
621633 """
622634 return self .__tag
623635
636+ @property
637+ def exit_tag (self ):
638+ """
639+ A tag value indicating why/how the trade was closed, or `None` while the
640+ trade is still active.
641+
642+ Unlike `Trade.tag` (the *opening* tag, fixed for the life of the trade),
643+ `exit_tag` reflects the *closing* order: `"sl"` or `"tp"` when the trade
644+ was closed automatically by its stop-loss or take-profit order, the
645+ `tag=` passed to `Trade.close()` / `Position.close()` when closed
646+ explicitly, or `None` for a plain close with no tag given (including
647+ trades closed as a side effect of an opposing order filling).
648+ """
649+ return self .__exit_tag
650+
624651 @property
625652 def _sl_order (self ):
626653 return self .__sl_order
@@ -934,9 +961,19 @@ def _process_orders(self):
934961 # If order.size is "greater" than trade.size, this order is a trade.close()
935962 # order and part of the trade was already closed beforehand
936963 size = copysign (min (abs (_prev_size ), abs (order .size )), order .size )
964+ # Determine why the trade is being closed, for `Trade.exit_tag`.
965+ # SL/TP orders are auto-tagged "sl"/"tp" unless their tag was
966+ # explicitly set to something other than the trade's opening tag.
967+ if order is trade ._sl_order :
968+ exit_tag = 'sl' if order .tag == trade .tag else order .tag
969+ elif order is trade ._tp_order :
970+ exit_tag = 'tp' if order .tag == trade .tag else order .tag
971+ else :
972+ # It's a trade.close()/position.close() order
973+ exit_tag = order .tag
937974 # If this trade isn't already closed (e.g. on multiple `trade.close(.5)` calls)
938975 if trade in self .trades :
939- self ._reduce_trade (trade , price , size , time_index )
976+ self ._reduce_trade (trade , price , size , time_index , exit_tag )
940977 assert order .size != - _prev_size or trade not in self .trades
941978 if order is trade ._sl_order :
942979 # Set SL back on the order for stats._trades["SL"]
@@ -1052,7 +1089,8 @@ def _process_orders(self):
10521089 if reprocess_orders :
10531090 self ._process_orders ()
10541091
1055- def _reduce_trade (self , trade : Trade , price : float , size : float , time_index : int ):
1092+ def _reduce_trade (self , trade : Trade , price : float , size : float , time_index : int ,
1093+ exit_tag = None ):
10561094 assert trade .size * size < 0
10571095 assert abs (trade .size ) >= abs (size )
10581096 self ._trades_cache_clear ()
@@ -1073,17 +1111,17 @@ def _reduce_trade(self, trade: Trade, price: float, size: float, time_index: int
10731111 close_trade = trade ._copy (size = - size , sl_order = None , tp_order = None )
10741112 self .trades .append (close_trade )
10751113
1076- self ._close_trade (close_trade , price , time_index )
1114+ self ._close_trade (close_trade , price , time_index , exit_tag )
10771115
1078- def _close_trade (self , trade : Trade , price : float , time_index : int ):
1116+ def _close_trade (self , trade : Trade , price : float , time_index : int , exit_tag = None ):
10791117 self ._trades_cache_clear ()
10801118 self .trades .remove (trade )
10811119 if trade ._sl_order :
10821120 self .orders .remove (trade ._sl_order )
10831121 if trade ._tp_order :
10841122 self .orders .remove (trade ._tp_order )
10851123
1086- closed_trade = trade ._replace (exit_price = price , exit_bar = time_index )
1124+ closed_trade = trade ._replace (exit_price = price , exit_bar = time_index , exit_tag = exit_tag )
10871125 self .closed_trades .append (closed_trade )
10881126 # Apply commission one more time at trade exit
10891127 commission = self ._commission (trade .size , price )
0 commit comments