@@ -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
@@ -780,10 +794,10 @@ def last_price(self) -> float:
780794
781795 def _adjusted_price (self , size = None , price = None ) -> float :
782796 """
783- Long/short `price`, adjusted for commisions .
797+ Long/short `price`, adjusted for spread .
784798 In long positions, the adjusted price is a fraction higher, and vice versa.
785799 """
786- return (price or self .last_price ) * (1 + copysign (self ._commission , size ))
800+ return (price or self .last_price ) * (1 + copysign (self ._spread , size ))
787801
788802 @property
789803 def equity (self ) -> float :
@@ -890,15 +904,17 @@ def _process_orders(self):
890904 # Adjust price to include commission (or bid-ask spread).
891905 # In long positions, the adjusted price is a fraction higher, and vice versa.
892906 adjusted_price = self ._adjusted_price (order .size , price )
907+ adjusted_price_plus_commission = adjusted_price + self ._commission (order .size , price )
893908
894909 # If order size was specified proportionally,
895910 # precompute true size in units, accounting for margin and spread/commissions
896911 size = order .size
897912 if - 1 < size < 1 :
898913 size = copysign (int ((self .margin_available * self ._leverage * abs (size ))
899- // adjusted_price ), size )
914+ // adjusted_price_plus_commission ), size )
900915 # Not enough cash/margin even for a single unit
901916 if not size :
917+ # XXX: The order is canceled by the broker?
902918 self .orders .remove (order )
903919 continue
904920 assert size == round (size )
@@ -927,8 +943,9 @@ def _process_orders(self):
927943 if not need_size :
928944 break
929945
930- # If we don't have enough liquidity to cover for the order, cancel it
931- if abs (need_size ) * adjusted_price > self .margin_available * self ._leverage :
946+ # If we don't have enough liquidity to cover for the order, the broker CANCELS it
947+ if abs (need_size ) * adjusted_price_plus_commission > \
948+ self .margin_available * self ._leverage :
932949 self .orders .remove (order )
933950 continue
934951
@@ -995,12 +1012,15 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
9951012 self .orders .remove (trade ._tp_order )
9961013
9971014 self .closed_trades .append (trade ._replace (exit_price = price , exit_bar = time_index ))
998- self ._cash += trade .pl
1015+ # Apply commission one more time at trade exit
1016+ self ._cash += trade .pl - self ._commission (trade .size , price )
9991017
10001018 def _open_trade (self , price : float , size : int ,
10011019 sl : Optional [float ], tp : Optional [float ], time_index : int , tag ):
10021020 trade = Trade (self , size , price , time_index , tag )
10031021 self .trades .append (trade )
1022+ # Apply broker commission at trade open
1023+ self ._cash -= self ._commission (size , price )
10041024 # Create SL/TP (bracket) orders.
10051025 # Make sure SL order is created first so it gets adversarially processed before TP order
10061026 # in case of an ambiguous tie (both hit within a single bar).
@@ -1026,7 +1046,8 @@ def __init__(self,
10261046 strategy : Type [Strategy ],
10271047 * ,
10281048 cash : float = 10_000 ,
1029- commission : float = .0 ,
1049+ spread : float = .0 ,
1050+ commission : Union [float , Tuple [float , float ]] = .0 ,
10301051 margin : float = 1. ,
10311052 trade_on_close = False ,
10321053 hedging = False ,
@@ -1052,11 +1073,25 @@ def __init__(self,
10521073
10531074 `cash` is the initial cash to start with.
10541075
1055- `commission` is the commission ratio. E.g. if your broker's commission
1056- is 1% of trade value, set commission to `0.01`. Note, if you wish to
1057- account for bid-ask spread, you can approximate doing so by increasing
1058- the commission, e.g. set it to `0.0002` for commission-less forex
1059- trading where the average spread is roughly 0.2‰ of asking price.
1076+ `spread` is the the constant bid-ask spread rate (relative to the price).
1077+ E.g. set it to `0.0002` for commission-less forex
1078+ trading where the average spread is roughly 0.2‰ of the asking price.
1079+
1080+ `commission` is the commission rate. E.g. if your broker's commission
1081+ is 1% of order value, set commission to `0.01`.
1082+ The commission is applied twice: at trade entry and at trade exit.
1083+ Besides one single floating value, `commission` can also be a tuple of floating
1084+ values `(fixed, relative)`. E.g. set it to `(100, .01)`
1085+ if your broker charges minimum $100 + 1%.
1086+ Additionally, `commission` can be a callable
1087+ `func(order_size: int, price: float) -> float`
1088+ (note, order size is negative for short orders),
1089+ which can be used to model more complex commission structures.
1090+ Negative commission values are interpreted as market-maker's rebates.
1091+
1092+ .. note::
1093+ Before v0.4.0, the commission was only applied once, like `spread` is now.
1094+ If you want to keep the old behavior, simply set `spread` instead.
10601095
10611096 `margin` is the required margin (ratio) of a leveraged account.
10621097 No difference is made between initial and maintenance margins.
@@ -1082,9 +1117,14 @@ def __init__(self,
10821117 raise TypeError ('`strategy` must be a Strategy sub-type' )
10831118 if not isinstance (data , pd .DataFrame ):
10841119 raise TypeError ("`data` must be a pandas.DataFrame with columns" )
1085- if not isinstance (commission , Number ):
1086- raise TypeError ('`commission ` must be a float value, percent of '
1120+ if not isinstance (spread , Number ):
1121+ raise TypeError ('`spread ` must be a float value, percent of '
10871122 'entry order price' )
1123+ if not isinstance (commission , (Number , tuple )) and not callable (commission ):
1124+ raise TypeError ('`commission` must be a float percent of order value, '
1125+ 'a tuple of `(fixed, relative)` commission, '
1126+ 'or a function that takes `(order_size, price)`'
1127+ 'and returns commission dollar value' )
10881128
10891129 data = data .copy (deep = False )
10901130
@@ -1127,7 +1167,7 @@ def __init__(self,
11271167
11281168 self ._data : pd .DataFrame = data
11291169 self ._broker = partial (
1130- _Broker , cash = cash , commission = commission , margin = margin ,
1170+ _Broker , cash = cash , spread = spread , commission = commission , margin = margin ,
11311171 trade_on_close = trade_on_close , hedging = hedging ,
11321172 exclusive_orders = exclusive_orders , index = data .index ,
11331173 )
0 commit comments