1616from itertools import chain , compress , product , repeat
1717from math import copysign
1818from numbers import Number
19- from typing import Callable , Dict , List , Optional , Sequence , Tuple , Type , Union
19+ from typing import Callable , Dict , List , Optional , Sequence , Tuple , Type , Union , Any , SupportsRound
2020
2121import numpy as np
2222import pandas as pd
@@ -200,7 +200,8 @@ def buy(self, *,
200200 stop : Optional [float ] = None ,
201201 sl : Optional [float ] = None ,
202202 tp : Optional [float ] = None ,
203- tag : object = None ):
203+ tag : SupportsRound = None ,
204+ comment : str = None ):
204205 """
205206 Place a new long order. For explanation of parameters, see `Order` and its properties.
206207
@@ -210,15 +211,16 @@ def buy(self, *,
210211 """
211212 assert 0 < size < 1 or round (size ) == size , \
212213 "size must be a positive fraction of equity, or a positive whole number of units"
213- return self ._broker .new_order (size , limit , stop , sl , tp , tag )
214+ return self ._broker .new_order (size , limit , stop , sl , tp , tag , comment )
214215
215216 def sell (self , * ,
216217 size : float = _FULL_EQUITY ,
217218 limit : Optional [float ] = None ,
218219 stop : Optional [float ] = None ,
219220 sl : Optional [float ] = None ,
220221 tp : Optional [float ] = None ,
221- tag : object = None ):
222+ tag : SupportsRound = None ,
223+ comment : str = None ):
222224 """
223225 Place a new short order. For explanation of parameters, see `Order` and its properties.
224226
@@ -230,7 +232,7 @@ def sell(self, *,
230232 """
231233 assert 0 < size < 1 or round (size ) == size , \
232234 "size must be a positive fraction of equity, or a positive whole number of units"
233- return self ._broker .new_order (- size , limit , stop , sl , tp , tag )
235+ return self ._broker .new_order (- size , limit , stop , sl , tp , tag , comment )
234236
235237 @property
236238 def equity (self ) -> float :
@@ -352,12 +354,12 @@ def is_short(self) -> bool:
352354 """True if the position is short (position size is negative)."""
353355 return self .size < 0
354356
355- def close (self , portion : float = 1. ):
357+ def close (self , portion : float = 1. , stop : float = None , limit : float = None , comment : str = None ):
356358 """
357359 Close portion of position by closing `portion` of each active trade. See `Trade.close`.
358360 """
359361 for trade in self .__broker .trades :
360- trade .close (portion )
362+ trade .close (portion , stop , limit , comment )
361363
362364 def __repr__ (self ):
363365 return f'<Position: { self .size } ({ len (self .__broker .trades )} trades)>'
@@ -389,7 +391,8 @@ def __init__(self, broker: '_Broker',
389391 sl_price : Optional [float ] = None ,
390392 tp_price : Optional [float ] = None ,
391393 parent_trade : Optional ['Trade' ] = None ,
392- tag : object = None ):
394+ tag : SupportsRound = None ,
395+ comment : str = None ):
393396 self .__broker = broker
394397 assert size != 0
395398 self .__size = size
@@ -399,6 +402,7 @@ def __init__(self, broker: '_Broker',
399402 self .__tp_price = tp_price
400403 self .__parent_trade = parent_trade
401404 self .__tag = tag
405+ self .__comment = comment
402406
403407 def _replace (self , ** kwargs ):
404408 for k , v in kwargs .items ():
@@ -415,7 +419,7 @@ def __repr__(self):
415419 ('tp' , self .__tp_price ),
416420 ('contingent' , self .is_contingent ),
417421 ('tag' , self .__tag ),
418- ) if value is not None ))
422+ ) if value is not None ) + f', comment= { self . __comment } ' )
419423
420424 def cancel (self ):
421425 """Cancel the order."""
@@ -489,7 +493,7 @@ def parent_trade(self):
489493 @property
490494 def tag (self ):
491495 """
492- Arbitrary value (such as a string ) which, if set, enables tracking
496+ A number (int or float or anything that supports rounding ) which, if set, enables tracking
493497 of this order and the associated `Trade` (see `Trade.tag`).
494498 """
495499 return self .__tag
@@ -522,13 +526,24 @@ def is_contingent(self):
522526 """
523527 return bool (self .__parent_trade )
524528
529+ @property
530+ def comment (self ):
531+ """
532+ A string comment associated with this order. This is different from tag as it can only carry a string while tag can only carry numbers (something that can be rounded).
533+ """
534+ return self .__comment
535+
536+ @comment .setter
537+ def comment (self , value ):
538+ self .__comment = value
539+
525540
526541class Trade :
527542 """
528543 When an `Order` is filled, it results in an active `Trade`.
529544 Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`.
530545 """
531- def __init__ (self , broker : '_Broker' , size : int , entry_price : float , entry_bar , tag ):
546+ def __init__ (self , broker : '_Broker' , size : int , entry_price : float , entry_bar , tag : SupportsRound , comment : str ):
532547 self .__broker = broker
533548 self .__size = size
534549 self .__entry_price = entry_price
@@ -537,7 +552,10 @@ def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar,
537552 self .__exit_bar : Optional [int ] = None
538553 self .__sl_order : Optional [Order ] = None
539554 self .__tp_order : Optional [Order ] = None
555+ self .__tp_comment : Optional [str ] = None
556+ self .__sl_comment : Optional [str ] = None
540557 self .__tag = tag
558+ self .__comment = comment
541559
542560 def __repr__ (self ):
543561 return f'<Trade size={ self .__size } time={ self .__entry_bar } -{ self .__exit_bar or "" } ' \
@@ -552,12 +570,13 @@ def _replace(self, **kwargs):
552570 def _copy (self , ** kwargs ):
553571 return copy (self )._replace (** kwargs )
554572
555- def close (self , portion : float = 1. ) :
573+ def close (self , portion : float = 1. , stop : float = None , limit : float = None , comment : str = None ) -> Order :
556574 """Place new `Order` to close `portion` of the trade at next market price."""
557575 assert 0 < portion <= 1 , "portion must be a fraction between 0 and 1"
558576 size = copysign (max (1 , round (abs (self .__size ) * portion )), - self .__size )
559- order = Order (self .__broker , size , parent_trade = self , tag = self .__tag )
577+ order = Order (self .__broker , size , stop_price = stop , limit_price = limit , parent_trade = self , tag = self .__tag , comment = comment )
560578 self .__broker .orders .insert (0 , order )
579+ return order
561580
562581 # Fields getters
563582
@@ -602,6 +621,13 @@ def tag(self):
602621 """
603622 return self .__tag
604623
624+ @property
625+ def comment (self ):
626+ """
627+ A string comment associated with this trade.
628+ """
629+ return self .__comment
630+
605631 @property
606632 def _sl_order (self ):
607633 return self .__sl_order
@@ -654,6 +680,17 @@ def value(self):
654680
655681 # SL/TP management API
656682
683+ @property
684+ def sl_comment (self ):
685+ """Comment associated with the SL order."""
686+ return self .__sl_comment
687+
688+ @sl_comment .setter
689+ def sl_comment (self , comment : str ):
690+ self .__sl_comment = comment
691+ if self .__sl_order :
692+ self .__sl_order .comment = comment
693+
657694 @property
658695 def sl (self ):
659696 """
@@ -669,6 +706,17 @@ def sl(self):
669706 def sl (self , price : float ):
670707 self .__set_contingent ('sl' , price )
671708
709+ @property
710+ def tp_comment (self ):
711+ """Comment associated with the TP order."""
712+ return self .__tp_comment
713+
714+ @tp_comment .setter
715+ def tp_comment (self , comment : str ):
716+ self .__tp_comment = comment
717+ if self .__tp_order :
718+ self .__tp_order .comment = comment
719+
672720 @property
673721 def tp (self ):
674722 """
@@ -692,7 +740,7 @@ def __set_contingent(self, type, price):
692740 if order :
693741 order .cancel ()
694742 if price :
695- kwargs = {'stop' : price } if type == 'sl' else {'limit' : price }
743+ kwargs = {'stop' : price , 'comment' : self . sl_comment } if type == 'sl' else {'limit' : price , 'comment' : self . tp_comment }
696744 order = self .__broker .new_order (- self .size , trade = self , tag = self .tag , ** kwargs )
697745 setattr (self , attr , order )
698746
@@ -728,7 +776,8 @@ def new_order(self,
728776 stop : Optional [float ] = None ,
729777 sl : Optional [float ] = None ,
730778 tp : Optional [float ] = None ,
731- tag : object = None ,
779+ tag : SupportsRound = None ,
780+ comment : str = None ,
732781 * ,
733782 trade : Optional [Trade ] = None ):
734783 """
@@ -754,7 +803,7 @@ def new_order(self,
754803 "Short orders require: "
755804 f"TP ({ tp } ) < LIMIT ({ limit or stop or adjusted_price } ) < SL ({ sl } )" )
756805
757- order = Order (self , size , limit , stop , sl , tp , trade , tag )
806+ order = Order (self , size , limit , stop , sl , tp , trade , tag , comment )
758807 # Put the new order in the order queue,
759808 # inserting SL/TP/trade-closing orders in-front
760809 if trade :
@@ -780,7 +829,7 @@ def last_price(self) -> float:
780829
781830 def _adjusted_price (self , size = None , price = None ) -> float :
782831 """
783- Long/short `price`, adjusted for commisions .
832+ Long/short `price`, adjusted for commissions .
784833 In long positions, the adjusted price is a fraction higher, and vice versa.
785834 """
786835 return (price or self .last_price ) * (1 + copysign (self ._commission , size ))
@@ -873,7 +922,7 @@ def _process_orders(self):
873922 size = copysign (min (abs (_prev_size ), abs (order .size )), order .size )
874923 # If this trade isn't already closed (e.g. on multiple `trade.close(.5)` calls)
875924 if trade in self .trades :
876- self ._reduce_trade (trade , price , size , time_index )
925+ self ._reduce_trade (trade , price , size , time_index , order . comment )
877926 assert order .size != - _prev_size or trade not in self .trades
878927 if order in (trade ._sl_order ,
879928 trade ._tp_order ):
@@ -921,7 +970,7 @@ def _process_orders(self):
921970 else :
922971 # The existing trade is larger than the new order,
923972 # so it will only be closed partially
924- self ._reduce_trade (trade , price , need_size , time_index )
973+ self ._reduce_trade (trade , price , need_size , time_index , order . comment )
925974 need_size = 0
926975
927976 if not need_size :
@@ -939,7 +988,8 @@ def _process_orders(self):
939988 order .sl ,
940989 order .tp ,
941990 time_index ,
942- order .tag )
991+ order .tag ,
992+ order .comment )
943993
944994 # We need to reprocess the SL/TP orders newly added to the queue.
945995 # This allows e.g. SL hitting in the same bar the order was open.
@@ -965,7 +1015,7 @@ def _process_orders(self):
9651015 if reprocess_orders :
9661016 self ._process_orders ()
9671017
968- def _reduce_trade (self , trade : Trade , price : float , size : float , time_index : int ):
1018+ def _reduce_trade (self , trade : Trade , price : float , size : float , time_index : int , comment : str = None ):
9691019 assert trade .size * size < 0
9701020 assert abs (trade .size ) >= abs (size )
9711021
@@ -985,21 +1035,21 @@ def _reduce_trade(self, trade: Trade, price: float, size: float, time_index: int
9851035 close_trade = trade ._copy (size = - size , sl_order = None , tp_order = None )
9861036 self .trades .append (close_trade )
9871037
988- self ._close_trade (close_trade , price , time_index )
1038+ self ._close_trade (close_trade , price , time_index , comment )
9891039
990- def _close_trade (self , trade : Trade , price : float , time_index : int ):
1040+ def _close_trade (self , trade : Trade , price : float , time_index : int , comment : str = None ):
9911041 self .trades .remove (trade )
9921042 if trade ._sl_order :
9931043 self .orders .remove (trade ._sl_order )
9941044 if trade ._tp_order :
9951045 self .orders .remove (trade ._tp_order )
9961046
997- self .closed_trades .append (trade ._replace (exit_price = price , exit_bar = time_index ))
1047+ self .closed_trades .append (trade ._replace (exit_price = price , exit_bar = time_index , comment = f' { trade . comment } / { comment } ' ))
9981048 self ._cash += trade .pl
9991049
10001050 def _open_trade (self , price : float , size : int ,
1001- sl : Optional [float ], tp : Optional [float ], time_index : int , tag ):
1002- trade = Trade (self , size , price , time_index , tag )
1051+ sl : Optional [float ], tp : Optional [float ], time_index : int , tag : SupportsRound , comment : str ):
1052+ trade = Trade (self , size , price , time_index , tag , comment )
10031053 self .trades .append (trade )
10041054 # Create SL/TP (bracket) orders.
10051055 # Make sure SL order is created first so it gets adversarially processed before TP order
@@ -1411,7 +1461,7 @@ def _batch(seq):
14111461 try :
14121462 # If multiprocessing start method is 'fork' (i.e. on POSIX), use
14131463 # a pool of processes to compute results in parallel.
1414- # Otherwise (i.e. on Windos ), sequential computation will be "faster".
1464+ # Otherwise (i.e. on Windows ), sequential computation will be "faster".
14151465 if mp .get_start_method (allow_none = False ) == 'fork' :
14161466 with ProcessPoolExecutor () as executor :
14171467 futures = [executor .submit (Backtest ._mp_task , backtest_uuid , i )
0 commit comments