@@ -698,16 +698,27 @@ def __set_contingent(self, type, price):
698
698
699
699
700
700
class _Broker :
701
- def __init__ (self , * , data , cash , commission , margin ,
701
+ def __init__ (self , * , data , cash , spread , commission , margin ,
702
702
trade_on_close , hedging , exclusive_orders , index ):
703
703
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 } " )
707
704
assert 0 < margin <= 1 , f"margin should be between 0 and 1, is { margin } "
708
705
self ._data : _Data = data
709
706
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
711
722
self ._leverage = 1 / margin
712
723
self ._trade_on_close = trade_on_close
713
724
self ._hedging = hedging
@@ -719,6 +730,9 @@ def __init__(self, *, data, cash, commission, margin,
719
730
self .position = Position (self )
720
731
self .closed_trades : List [Trade ] = []
721
732
733
+ def _commission_func (self , order_size , price ):
734
+ return self ._commission_fixed + abs (order_size ) * price * self ._commission_relative
735
+
722
736
def __repr__ (self ):
723
737
return f'<Broker: { self ._cash :.0f} { self .position .pl :+.1f} ({ len (self .trades )} trades)>'
724
738
@@ -781,10 +795,10 @@ def last_price(self) -> float:
781
795
782
796
def _adjusted_price (self , size = None , price = None ) -> float :
783
797
"""
784
- Long/short `price`, adjusted for commisions .
798
+ Long/short `price`, adjusted for spread .
785
799
In long positions, the adjusted price is a fraction higher, and vice versa.
786
800
"""
787
- return (price or self .last_price ) * (1 + copysign (self ._commission , size ))
801
+ return (price or self .last_price ) * (1 + copysign (self ._spread , size ))
788
802
789
803
@property
790
804
def equity (self ) -> float :
@@ -892,15 +906,17 @@ def _process_orders(self):
892
906
# Adjust price to include commission (or bid-ask spread).
893
907
# In long positions, the adjusted price is a fraction higher, and vice versa.
894
908
adjusted_price = self ._adjusted_price (order .size , price )
909
+ adjusted_price_plus_commission = adjusted_price + self ._commission (order .size , price )
895
910
896
911
# If order size was specified proportionally,
897
912
# precompute true size in units, accounting for margin and spread/commissions
898
913
size = order .size
899
914
if - 1 < size < 1 :
900
915
size = copysign (int ((self .margin_available * self ._leverage * abs (size ))
901
- // adjusted_price ), size )
916
+ // adjusted_price_plus_commission ), size )
902
917
# Not enough cash/margin even for a single unit
903
918
if not size :
919
+ # XXX: The order is canceled by the broker?
904
920
self .orders .remove (order )
905
921
continue
906
922
assert size == round (size )
@@ -929,8 +945,9 @@ def _process_orders(self):
929
945
if not need_size :
930
946
break
931
947
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 :
934
951
self .orders .remove (order )
935
952
continue
936
953
@@ -997,12 +1014,15 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
997
1014
self .orders .remove (trade ._tp_order )
998
1015
999
1016
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 )
1001
1019
1002
1020
def _open_trade (self , price : float , size : int ,
1003
1021
sl : Optional [float ], tp : Optional [float ], time_index : int , tag ):
1004
1022
trade = Trade (self , size , price , time_index , tag )
1005
1023
self .trades .append (trade )
1024
+ # Apply broker commission at trade open
1025
+ self ._cash -= self ._commission (size , price )
1006
1026
# Create SL/TP (bracket) orders.
1007
1027
# Make sure SL order is created first so it gets adversarially processed before TP order
1008
1028
# in case of an ambiguous tie (both hit within a single bar).
@@ -1028,7 +1048,8 @@ def __init__(self,
1028
1048
strategy : Type [Strategy ],
1029
1049
* ,
1030
1050
cash : float = 10_000 ,
1031
- commission : float = .0 ,
1051
+ spread : float = .0 ,
1052
+ commission : Union [float , Tuple [float , float ]] = .0 ,
1032
1053
margin : float = 1. ,
1033
1054
trade_on_close = False ,
1034
1055
hedging = False ,
@@ -1054,11 +1075,25 @@ def __init__(self,
1054
1075
1055
1076
`cash` is the initial cash to start with.
1056
1077
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.
1062
1097
1063
1098
.. note::
1064
1099
With nonzero `commission`, long and short orders will be placed
@@ -1092,9 +1127,14 @@ def __init__(self,
1092
1127
raise TypeError ('`strategy` must be a Strategy sub-type' )
1093
1128
if not isinstance (data , pd .DataFrame ):
1094
1129
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 '
1097
1132
'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' )
1098
1138
1099
1139
data = data .copy (deep = False )
1100
1140
@@ -1137,7 +1177,7 @@ def __init__(self,
1137
1177
1138
1178
self ._data : pd .DataFrame = data
1139
1179
self ._broker = partial (
1140
- _Broker , cash = cash , commission = commission , margin = margin ,
1180
+ _Broker , cash = cash , spread = spread , commission = commission , margin = margin ,
1141
1181
trade_on_close = trade_on_close , hedging = hedging ,
1142
1182
exclusive_orders = exclusive_orders , index = data .index ,
1143
1183
)
0 commit comments