@@ -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
@@ -780,10 +794,10 @@ def last_price(self) -> float:
780
794
781
795
def _adjusted_price (self , size = None , price = None ) -> float :
782
796
"""
783
- Long/short `price`, adjusted for commisions .
797
+ Long/short `price`, adjusted for spread .
784
798
In long positions, the adjusted price is a fraction higher, and vice versa.
785
799
"""
786
- return (price or self .last_price ) * (1 + copysign (self ._commission , size ))
800
+ return (price or self .last_price ) * (1 + copysign (self ._spread , size ))
787
801
788
802
@property
789
803
def equity (self ) -> float :
@@ -890,15 +904,17 @@ def _process_orders(self):
890
904
# Adjust price to include commission (or bid-ask spread).
891
905
# In long positions, the adjusted price is a fraction higher, and vice versa.
892
906
adjusted_price = self ._adjusted_price (order .size , price )
907
+ adjusted_price_plus_commission = adjusted_price + self ._commission (order .size , price )
893
908
894
909
# If order size was specified proportionally,
895
910
# precompute true size in units, accounting for margin and spread/commissions
896
911
size = order .size
897
912
if - 1 < size < 1 :
898
913
size = copysign (int ((self .margin_available * self ._leverage * abs (size ))
899
- // adjusted_price ), size )
914
+ // adjusted_price_plus_commission ), size )
900
915
# Not enough cash/margin even for a single unit
901
916
if not size :
917
+ # XXX: The order is canceled by the broker?
902
918
self .orders .remove (order )
903
919
continue
904
920
assert size == round (size )
@@ -927,8 +943,9 @@ def _process_orders(self):
927
943
if not need_size :
928
944
break
929
945
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 :
932
949
self .orders .remove (order )
933
950
continue
934
951
@@ -995,12 +1012,15 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
995
1012
self .orders .remove (trade ._tp_order )
996
1013
997
1014
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 )
999
1017
1000
1018
def _open_trade (self , price : float , size : int ,
1001
1019
sl : Optional [float ], tp : Optional [float ], time_index : int , tag ):
1002
1020
trade = Trade (self , size , price , time_index , tag )
1003
1021
self .trades .append (trade )
1022
+ # Apply broker commission at trade open
1023
+ self ._cash -= self ._commission (size , price )
1004
1024
# Create SL/TP (bracket) orders.
1005
1025
# Make sure SL order is created first so it gets adversarially processed before TP order
1006
1026
# in case of an ambiguous tie (both hit within a single bar).
@@ -1026,7 +1046,8 @@ def __init__(self,
1026
1046
strategy : Type [Strategy ],
1027
1047
* ,
1028
1048
cash : float = 10_000 ,
1029
- commission : float = .0 ,
1049
+ spread : float = .0 ,
1050
+ commission : Union [float , Tuple [float , float ]] = .0 ,
1030
1051
margin : float = 1. ,
1031
1052
trade_on_close = False ,
1032
1053
hedging = False ,
@@ -1052,11 +1073,25 @@ def __init__(self,
1052
1073
1053
1074
`cash` is the initial cash to start with.
1054
1075
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.
1060
1095
1061
1096
`margin` is the required margin (ratio) of a leveraged account.
1062
1097
No difference is made between initial and maintenance margins.
@@ -1082,9 +1117,14 @@ def __init__(self,
1082
1117
raise TypeError ('`strategy` must be a Strategy sub-type' )
1083
1118
if not isinstance (data , pd .DataFrame ):
1084
1119
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 '
1087
1122
'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' )
1088
1128
1089
1129
data = data .copy (deep = False )
1090
1130
@@ -1127,7 +1167,7 @@ def __init__(self,
1127
1167
1128
1168
self ._data : pd .DataFrame = data
1129
1169
self ._broker = partial (
1130
- _Broker , cash = cash , commission = commission , margin = margin ,
1170
+ _Broker , cash = cash , spread = spread , commission = commission , margin = margin ,
1131
1171
trade_on_close = trade_on_close , hedging = hedging ,
1132
1172
exclusive_orders = exclusive_orders , index = data .index ,
1133
1173
)
0 commit comments