From df85f1e3e642313b6e5171e62d05bd8f76935dc5 Mon Sep 17 00:00:00 2001 From: mutt0-ds Date: Wed, 15 Jun 2022 10:12:56 +0200 Subject: [PATCH 1/3] added fixed commision to backtesting as suggested in #623 --- backtesting/backtesting.py | 52 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 742a40f2..ea3b92b1 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -664,12 +664,15 @@ def __set_contingent(self, type, price): class _Broker: - def __init__(self, *, data, cash, commission, margin, - trade_on_close, hedging, exclusive_orders, index): + def __init__(self, *, data, cash, commission, margin, trade_on_close, + hedging, exclusive_orders, use_fixed_commission, index): assert 0 < cash, f"cash should be >0, is {cash}" - assert -.1 <= commission < .1, \ - ("commission should be between -10% " - f"(e.g. market-maker's rebates) and 10% (fees), is {commission}") + if use_fixed_commission: + assert commission >= 0, f"fixed commission should be be >0, is {commission}" + else: + assert -.1 <= commission < .1, \ + ("commission should be between -10% " + f"(e.g. market-maker's rebates) and 10% (fees), is {commission}") assert 0 < margin <= 1, f"margin should be between 0 and 1, is {margin}" self._data: _Data = data self._cash = cash @@ -678,6 +681,7 @@ def __init__(self, *, data, cash, commission, margin, self._trade_on_close = trade_on_close self._hedging = hedging self._exclusive_orders = exclusive_orders + self._use_fixed_commission = use_fixed_commission self._equity = np.tile(np.nan, len(index)) self.orders: List[Order] = [] @@ -744,11 +748,12 @@ def last_price(self) -> float: return self._data.Close[-1] def _adjusted_price(self, size=None, price=None) -> float: - """ - Long/short `price`, adjusted for commisions. - In long positions, the adjusted price is a fraction higher, and vice versa. - """ - return (price or self.last_price) * (1 + copysign(self._commission, size)) + """Long/shortprice, adjusted for commissions (fixed or percentage). In long positions, + the adjusted price is a fraction higher, and vice versa.""" + if self._use_fixed_commission: + return ((price or self.last_price) + copysign(self._commission, size)) + else: + return ((price or self.last_price) * (1 + copysign(self._commission, size))) @property def equity(self) -> float: @@ -989,7 +994,8 @@ def __init__(self, margin: float = 1., trade_on_close=False, hedging=False, - exclusive_orders=False + exclusive_orders=False, + use_fixed_commission=False ): """ Initialize a backtest. Requires data and a strategy to test. @@ -1011,11 +1017,13 @@ def __init__(self, `cash` is the initial cash to start with. - `commission` is the commission ratio. E.g. if your broker's commission - is 1% of trade value, set commission to `0.01`. Note, if you wish to - account for bid-ask spread, you can approximate doing so by increasing - the commission, e.g. set it to `0.0002` for commission-less forex - trading where the average spread is roughly 0.2‰ of asking price. + `commission` is the commission ratio, a percentage by default.E.g. + if your broker's commission is 1% of trade value, set commission to `0.01`. + Note, if you wish to account for bid-ask spread, you can approximate + doing so by increasing the commission, e.g. set it to `0.0002` for + commission-less forex trading where the average spread is roughly + 0.2‰ of asking price. If you want to use a fixed commission in dollar + value, set the `use_fixed_commission` parameter as `True`. `margin` is the required margin (ratio) of a leveraged account. No difference is made between initial and maintenance margins. @@ -1034,6 +1042,9 @@ def __init__(self, trade/position, making at most a single trade (long or short) in effect at each time. + If `use_fixed_commission` is `True`, the commission won't be considered + as a percentage, but as a flat fee on every order. + [FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp """ @@ -1042,8 +1053,9 @@ def __init__(self, if not isinstance(data, pd.DataFrame): raise TypeError("`data` must be a pandas.DataFrame with columns") if not isinstance(commission, Number): - raise TypeError('`commission` must be a float value, percent of ' - 'entry order price') + raise TypeError('`commission` must be a number, percent of ' + 'entry order price. Set `use_fixed_commission` to `True` ' + 'if you want to use a flat fee instead') data = data.copy(deep=False) @@ -1087,8 +1099,8 @@ def __init__(self, self._data: pd.DataFrame = data self._broker = partial( _Broker, cash=cash, commission=commission, margin=margin, - trade_on_close=trade_on_close, hedging=hedging, - exclusive_orders=exclusive_orders, index=data.index, + trade_on_close=trade_on_close, hedging=hedging, exclusive_orders=exclusive_orders, + use_fixed_commission=use_fixed_commission, index=data.index ) self._strategy = strategy self._results: Optional[pd.Series] = None From 52a09b897897c64560f9487005c66f1a75edd04c Mon Sep 17 00:00:00 2001 From: mutt0-ds Date: Wed, 15 Jun 2022 10:15:44 +0200 Subject: [PATCH 2/3] fixed indentation --- backtesting/backtesting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index ea3b92b1..b7c05151 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -748,8 +748,10 @@ def last_price(self) -> float: return self._data.Close[-1] def _adjusted_price(self, size=None, price=None) -> float: - """Long/shortprice, adjusted for commissions (fixed or percentage). In long positions, - the adjusted price is a fraction higher, and vice versa.""" + """ + Long/shortprice, adjusted for commissions (fixed or percentage). + In long positions, the adjusted price is a fraction higher, and vice versa. + """ if self._use_fixed_commission: return ((price or self.last_price) + copysign(self._commission, size)) else: From 5569c54b2f5ad9a1ab79b36a7b7f4e28e48b4b37 Mon Sep 17 00:00:00 2001 From: mutt0-ds Date: Wed, 15 Jun 2022 10:17:00 +0200 Subject: [PATCH 3/3] fix typo --- backtesting/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index b7c05151..4464ea65 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -749,7 +749,7 @@ def last_price(self) -> float: def _adjusted_price(self, size=None, price=None) -> float: """ - Long/shortprice, adjusted for commissions (fixed or percentage). + Long/short `price`, adjusted for commissions (fixed or percentage). In long positions, the adjusted price is a fraction higher, and vice versa. """ if self._use_fixed_commission: