@@ -698,16 +698,27 @@ def __set_contingent(self, type, price):
698698
699699
700700class _Broker :
701- def __init__ (self , * , data , cash , commission , margin ,
701+ def __init__ (self , * , data , cash , spread , commission , margin ,
702702 trade_on_close , hedging , exclusive_orders , index ):
703703 assert 0 < cash , f"cash should be >0, is { cash } "
704- assert - .1 <= commission < .1 , \
705- ("commission should be between -10% "
706- f"(e.g. market-maker's rebates) and 10% (fees), is { commission } " )
707704 assert 0 < margin <= 1 , f"margin should be between 0 and 1, is { margin } "
708705 self ._data : _Data = data
709706 self ._cash = cash
710- self ._commission = commission
707+
708+ if callable (commission ):
709+ self ._commission = commission
710+ else :
711+ try :
712+ self ._commission_fixed , self ._commission_relative = commission
713+ except TypeError :
714+ self ._commission_fixed , self ._commission_relative = 0 , commission
715+ assert self ._commission_fixed >= 0 , 'Need fixed cash commission in $ >= 0'
716+ assert - .1 <= self ._commission_relative < .1 , \
717+ ("commission should be between -10% "
718+ f"(e.g. market-maker's rebates) and 10% (fees), is { self ._commission_relative } " )
719+ self ._commission = self ._commission_func
720+
721+ self ._spread = spread
711722 self ._leverage = 1 / margin
712723 self ._trade_on_close = trade_on_close
713724 self ._hedging = hedging
@@ -719,6 +730,9 @@ def __init__(self, *, data, cash, commission, margin,
719730 self .position = Position (self )
720731 self .closed_trades : List [Trade ] = []
721732
733+ def _commission_func (self , order_size , price ):
734+ return self ._commission_fixed + abs (order_size ) * price * self ._commission_relative
735+
722736 def __repr__ (self ):
723737 return f'<Broker: { self ._cash :.0f} { self .position .pl :+.1f} ({ len (self .trades )} trades)>'
724738
@@ -781,10 +795,10 @@ def last_price(self) -> float:
781795
782796 def _adjusted_price (self , size = None , price = None ) -> float :
783797 """
784- Long/short `price`, adjusted for commisions .
798+ Long/short `price`, adjusted for spread .
785799 In long positions, the adjusted price is a fraction higher, and vice versa.
786800 """
787- return (price or self .last_price ) * (1 + copysign (self ._commission , size ))
801+ return (price or self .last_price ) * (1 + copysign (self ._spread , size ))
788802
789803 @property
790804 def equity (self ) -> float :
@@ -892,15 +906,17 @@ def _process_orders(self):
892906 # Adjust price to include commission (or bid-ask spread).
893907 # In long positions, the adjusted price is a fraction higher, and vice versa.
894908 adjusted_price = self ._adjusted_price (order .size , price )
909+ adjusted_price_plus_commission = adjusted_price + self ._commission (order .size , price )
895910
896911 # If order size was specified proportionally,
897912 # precompute true size in units, accounting for margin and spread/commissions
898913 size = order .size
899914 if - 1 < size < 1 :
900915 size = copysign (int ((self .margin_available * self ._leverage * abs (size ))
901- // adjusted_price ), size )
916+ // adjusted_price_plus_commission ), size )
902917 # Not enough cash/margin even for a single unit
903918 if not size :
919+ # XXX: The order is canceled by the broker?
904920 self .orders .remove (order )
905921 continue
906922 assert size == round (size )
@@ -929,8 +945,9 @@ def _process_orders(self):
929945 if not need_size :
930946 break
931947
932- # If we don't have enough liquidity to cover for the order, cancel it
933- if abs (need_size ) * adjusted_price > self .margin_available * self ._leverage :
948+ # If we don't have enough liquidity to cover for the order, the broker CANCELS it
949+ if abs (need_size ) * adjusted_price_plus_commission > \
950+ self .margin_available * self ._leverage :
934951 self .orders .remove (order )
935952 continue
936953
@@ -997,12 +1014,15 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
9971014 self .orders .remove (trade ._tp_order )
9981015
9991016 self .closed_trades .append (trade ._replace (exit_price = price , exit_bar = time_index ))
1000- self ._cash += trade .pl
1017+ # Apply commission one more time at trade exit
1018+ self ._cash += trade .pl - self ._commission (trade .size , price )
10011019
10021020 def _open_trade (self , price : float , size : int ,
10031021 sl : Optional [float ], tp : Optional [float ], time_index : int , tag ):
10041022 trade = Trade (self , size , price , time_index , tag )
10051023 self .trades .append (trade )
1024+ # Apply broker commission at trade open
1025+ self ._cash -= self ._commission (size , price )
10061026 # Create SL/TP (bracket) orders.
10071027 # Make sure SL order is created first so it gets adversarially processed before TP order
10081028 # in case of an ambiguous tie (both hit within a single bar).
@@ -1028,7 +1048,8 @@ def __init__(self,
10281048 strategy : Type [Strategy ],
10291049 * ,
10301050 cash : float = 10_000 ,
1031- commission : float = .0 ,
1051+ spread : float = .0 ,
1052+ commission : Union [float , Tuple [float , float ]] = .0 ,
10321053 margin : float = 1. ,
10331054 trade_on_close = False ,
10341055 hedging = False ,
@@ -1054,11 +1075,25 @@ def __init__(self,
10541075
10551076 `cash` is the initial cash to start with.
10561077
1057- `commission` is the commission ratio. E.g. if your broker's commission
1058- is 1% of trade value, set commission to `0.01`. Note, if you wish to
1059- account for bid-ask spread, you can approximate doing so by increasing
1060- the commission, e.g. set it to `0.0002` for commission-less forex
1061- trading where the average spread is roughly 0.2‰ of asking price.
1078+ `spread` is the the constant bid-ask spread rate (relative to the price).
1079+ E.g. set it to `0.0002` for commission-less forex
1080+ trading where the average spread is roughly 0.2‰ of the asking price.
1081+
1082+ `commission` is the commission rate. E.g. if your broker's commission
1083+ is 1% of order value, set commission to `0.01`.
1084+ The commission is applied twice: at trade entry and at trade exit.
1085+ Besides one single floating value, `commission` can also be a tuple of floating
1086+ values `(fixed, relative)`. E.g. set it to `(100, .01)`
1087+ if your broker charges minimum $100 + 1%.
1088+ Additionally, `commission` can be a callable
1089+ `func(order_size: int, price: float) -> float`
1090+ (note, order size is negative for short orders),
1091+ which can be used to model more complex commission structures.
1092+ Negative commission values are interpreted as market-maker's rebates.
1093+
1094+ .. note::
1095+ Before v0.4.0, the commission was only applied once, like `spread` is now.
1096+ If you want to keep the old behavior, simply set `spread` instead.
10621097
10631098 .. note::
10641099 With nonzero `commission`, long and short orders will be placed
@@ -1092,9 +1127,14 @@ def __init__(self,
10921127 raise TypeError ('`strategy` must be a Strategy sub-type' )
10931128 if not isinstance (data , pd .DataFrame ):
10941129 raise TypeError ("`data` must be a pandas.DataFrame with columns" )
1095- if not isinstance (commission , Number ):
1096- raise TypeError ('`commission ` must be a float value, percent of '
1130+ if not isinstance (spread , Number ):
1131+ raise TypeError ('`spread ` must be a float value, percent of '
10971132 'entry order price' )
1133+ if not isinstance (commission , (Number , tuple )) and not callable (commission ):
1134+ raise TypeError ('`commission` must be a float percent of order value, '
1135+ 'a tuple of `(fixed, relative)` commission, '
1136+ 'or a function that takes `(order_size, price)`'
1137+ 'and returns commission dollar value' )
10981138
10991139 data = data .copy (deep = False )
11001140
@@ -1137,7 +1177,7 @@ def __init__(self,
11371177
11381178 self ._data : pd .DataFrame = data
11391179 self ._broker = partial (
1140- _Broker , cash = cash , commission = commission , margin = margin ,
1180+ _Broker , cash = cash , spread = spread , commission = commission , margin = margin ,
11411181 trade_on_close = trade_on_close , hedging = hedging ,
11421182 exclusive_orders = exclusive_orders , index = data .index ,
11431183 )
0 commit comments