From e8803477dfa269f9f95932f169c95554e22b504f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 May 2022 22:56:55 -0600 Subject: [PATCH 1/8] exchange/exchange add param taker_or_maker to add_dry_order_fee --- freqtrade/exchange/exchange.py | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 08bdab265..f0ff7e514 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -785,6 +785,26 @@ class Exchange: # Dry-run methods + def taker_or_maker( + self, + order_reason: Literal['entry', 'exit', 'stoploss'], # TODO: stoploss + ): + order_type = self._config['order_types'][order_reason] + if order_type == 'market' or order_reason == 'stoploss': + return 'taker' + else: + return ( + 'maker' if ( + ( + order_reason == 'entry' and + self._config['entry_pricing']['price_side'] == 'same' + ) or ( + order_reason == 'exit' and + self._config['exit_pricing']['price_side'] == 'same' + ) + ) else 'taker' + ) + def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}, stop_loss: bool = False) -> Dict[str, Any]: @@ -824,7 +844,7 @@ class Exchange: 'filled': _amount, 'cost': (dry_order['amount'] * average) / leverage }) - dry_order = self.add_dry_order_fee(pair, dry_order) + dry_order = self.add_dry_order_fee(pair, dry_order, self.taker_or_maker('entry')) dry_order = self.check_dry_limit_order_filled(dry_order) @@ -832,12 +852,17 @@ class Exchange: # Copy order and close it - so the returned order is open unless it's a market order return dry_order - def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]: + def add_dry_order_fee( + self, + pair: str, + dry_order: Dict[str, Any], + taker_or_maker: Literal['taker', 'maker'], + ) -> Dict[str, Any]: dry_order.update({ 'fee': { 'currency': self.get_pair_quote_currency(pair), - 'cost': dry_order['cost'] * self.get_fee(pair), - 'rate': self.get_fee(pair) + 'cost': dry_order['cost'] * self.get_fee(pair, taker_or_maker=taker_or_maker), + 'rate': self.get_fee(pair, taker_or_maker=taker_or_maker) } }) return dry_order @@ -917,7 +942,16 @@ class Exchange: 'filled': order['amount'], 'remaining': 0, }) - self.add_dry_order_fee(pair, order) + enter_long = not order['is_short'] and order['side'] == 'buy' + enter_short = order['is_short'] and order['side'] == 'sell' + entry_or_exit: Literal['entry', 'exit'] = ( + 'entry' if (enter_short or enter_long) else 'exit' + ) + self.add_dry_order_fee( + pair, + order, + self.taker_or_maker(entry_or_exit) + ) return order From 5d9aee6b7e6591ddb1953159223af8ebbb9cf6a1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 May 2022 23:18:13 -0600 Subject: [PATCH 2/8] test_taker_or_maker --- tests/exchange/test_exchange.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1368bcb85..3edb5187c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4957,3 +4957,24 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert order['cost'] == 100 assert order['filled'] == 100 assert order['remaining'] == 100 + + +@pytest.mark.parametrize('order_reason,price_side,order_type,taker_or_maker', [ + ("entry", "same", "limit", "maker"), + ("exit", "same", "limit", "maker"), + ("entry", "other", "limit", "taker"), + ("exit", "other", "limit", "taker"), + ("entry", "same", "market", "maker"), + ("exit", "same", "market", "maker"), + ("entry", "other", "market", "taker"), + ("exit", "other", "market", "taker"), + ("stoploss", "same", "limit", "taker"), + ("stoploss", "same", "market", "taker"), + ("stoploss", "other", "limit", "taker"), + ("stoploss", "other", "market", "taker"), +]) +def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): + default_conf[f"{order_reason}_pricing"]["price_side"] = price_side + default_conf["order_types"][order_reason] = order_type + exchange = get_patched_exchange(mocker, default_conf) + assert exchange.taker_or_maker(order_reason) == taker_or_maker From dac9931b4a53aedeb4435483dcbb956203bb5530 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 May 2022 23:52:01 -0600 Subject: [PATCH 3/8] test_create_dry_run_order_fees --- tests/exchange/test_exchange.py | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3edb5187c..ab51362a5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1125,6 +1125,47 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag assert order["cost"] == 1 * 200 / leverage +@pytest.mark.parametrize('side,is_short,order_reason', [ + ("buy", False, "entry"), + ("sell", False, "exit"), + ("buy", True, "exit"), + ("sell", True, "entry"), +]) +@pytest.mark.parametrize("order_type,price_side,fee", [ + ("limit", "same", 1.0), + ("limit", "other", 2.0), + ("market", "same", 2.0), + ("market", "other", 2.0), +]) +def test_create_dry_run_order_fees( + default_conf, + mocker, + side, + order_type, + is_short, + order_reason, + price_side, + fee, +): + default_conf[f"{order_reason}_pricing"]["price_side"] = "same" + default_conf["order_types"][order_reason] = order_type + mocker.patch( + 'freqtrade.exchange.Exchange.get_fee', + lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + ) + exchange = get_patched_exchange(mocker, default_conf) + + order = exchange.create_dry_run_order( + pair='ADA/USDT', + ordertype=order_type, + side=side, + amount=10, + rate=2.0, + ) + + assert order['ft_fee_base'] == fee + + @pytest.mark.parametrize("side,startprice,endprice", [ ("buy", 25.563, 25.566), ("sell", 25.566, 25.563) From 86ad5dd02a7ae680a1d566f7b83b55e5cf65f399 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 4 May 2022 00:08:41 -0600 Subject: [PATCH 4/8] test_exchange::test_taker_or_maker fixes --- tests/exchange/test_exchange.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ab51362a5..a13dca4b6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5015,7 +5015,9 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun ("stoploss", "other", "market", "taker"), ]) def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): - default_conf[f"{order_reason}_pricing"]["price_side"] = price_side + if order_reason != 'stoploss': + default_conf[f"{order_reason}_pricing"]["price_side"] = price_side + default_conf["order_types"] = {} default_conf["order_types"][order_reason] = order_type exchange = get_patched_exchange(mocker, default_conf) assert exchange.taker_or_maker(order_reason) == taker_or_maker From 10cbb5e67c32b184445f868366e4eef38afe5878 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 4 May 2022 00:10:09 -0600 Subject: [PATCH 5/8] test_exchange::test_taker_or_maker fixes --- tests/exchange/test_exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a13dca4b6..86928a2f6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5005,13 +5005,13 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun ("exit", "same", "limit", "maker"), ("entry", "other", "limit", "taker"), ("exit", "other", "limit", "taker"), - ("entry", "same", "market", "maker"), - ("exit", "same", "market", "maker"), + ("stoploss", "same", "limit", "taker"), + ("stoploss", "other", "limit", "taker"), + ("entry", "same", "market", "taker"), + ("exit", "same", "market", "taker"), ("entry", "other", "market", "taker"), ("exit", "other", "market", "taker"), - ("stoploss", "same", "limit", "taker"), ("stoploss", "same", "market", "taker"), - ("stoploss", "other", "limit", "taker"), ("stoploss", "other", "market", "taker"), ]) def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): From 8b2535a8da3eabbbb53a70eeebb6348afaf0edd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 15:42:17 +0200 Subject: [PATCH 6/8] Update Typing for fees --- freqtrade/constants.py | 1 + freqtrade/exchange/exchange.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ce7c0ff83..6d74ceafd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -542,3 +542,4 @@ TradeList = List[List] LongShort = Literal['long', 'short'] EntryExit = Literal['entry', 'exit'] BuySell = Literal['buy', 'sell'] +MakerTaker = Literal['maker', 'taker'] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index db7a8ce41..2deb2b70d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, - EntryExit, ListPairsWithTimeframes, PairWithTimeframe) + EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, @@ -870,7 +870,8 @@ class Exchange: 'filled': _amount, 'cost': (dry_order['amount'] * average) / leverage }) - dry_order = self.add_dry_order_fee(pair, dry_order, self.taker_or_maker('entry')) + # market orders will always incurr taker fees + dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') dry_order = self.check_dry_limit_order_filled(dry_order) @@ -882,7 +883,7 @@ class Exchange: self, pair: str, dry_order: Dict[str, Any], - taker_or_maker: Literal['taker', 'maker'], + taker_or_maker: MakerTaker, ) -> Dict[str, Any]: dry_order.update({ 'fee': { @@ -970,7 +971,7 @@ class Exchange: }) enter_long = not order['is_short'] and order['side'] == 'buy' enter_short = order['is_short'] and order['side'] == 'sell' - entry_or_exit: Literal['entry', 'exit'] = ( + entry_or_exit: EntryExit = ( 'entry' if (enter_short or enter_long) else 'exit' ) self.add_dry_order_fee( @@ -1635,7 +1636,7 @@ class Exchange: @retrier def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, - price: float = 1, taker_or_maker: str = 'maker') -> float: + price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float: try: if self._config['dry_run'] and self._config.get('fee', None) is not None: return self._config['fee'] From 4172f92bfc492a140103d79027fc326348183cc0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 17:03:45 +0200 Subject: [PATCH 7/8] simplify dry-run taker/maker selection --- freqtrade/exchange/exchange.py | 33 ++++--------------------- tests/exchange/test_exchange.py | 43 ++++++++++++--------------------- 2 files changed, 20 insertions(+), 56 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2deb2b70d..efb33ee74 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -811,26 +811,6 @@ class Exchange: # Dry-run methods - def taker_or_maker( - self, - order_reason: Literal['entry', 'exit', 'stoploss'], # TODO: stoploss - ): - order_type = self._config['order_types'][order_reason] - if order_type == 'market' or order_reason == 'stoploss': - return 'taker' - else: - return ( - 'maker' if ( - ( - order_reason == 'entry' and - self._config['entry_pricing']['price_side'] == 'same' - ) or ( - order_reason == 'exit' and - self._config['exit_pricing']['price_side'] == 'same' - ) - ) else 'taker' - ) - def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}, stop_loss: bool = False) -> Dict[str, Any]: @@ -873,7 +853,7 @@ class Exchange: # market orders will always incurr taker fees dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') - dry_order = self.check_dry_limit_order_filled(dry_order) + dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True) self._dry_run_open_orders[dry_order["id"]] = dry_order # Copy order and close it - so the returned order is open unless it's a market order @@ -955,7 +935,8 @@ class Exchange: pass return False - def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]: + def check_dry_limit_order_filled( + self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]: """ Check dry-run limit order fill and update fee (if it filled). """ @@ -969,15 +950,11 @@ class Exchange: 'filled': order['amount'], 'remaining': 0, }) - enter_long = not order['is_short'] and order['side'] == 'buy' - enter_short = order['is_short'] and order['side'] == 'sell' - entry_or_exit: EntryExit = ( - 'entry' if (enter_short or enter_long) else 'exit' - ) + self.add_dry_order_fee( pair, order, - self.taker_or_maker(entry_or_exit) + 'taker' if immediate else 'maker', ) return order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0c4df8f5a..ff8b4b40c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1160,23 +1160,33 @@ def test_create_dry_run_order_fees( price_side, fee, ): - default_conf[f"{order_reason}_pricing"]["price_side"] = "same" - default_conf["order_types"][order_reason] = order_type mocker.patch( 'freqtrade.exchange.Exchange.get_fee', - lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 ) + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', + return_value=price_side == 'other') exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_dry_run_order( - pair='ADA/USDT', + pair='LTC/USDT', ordertype=order_type, side=side, amount=10, rate=2.0, + leverage=1.0 ) + if price_side == 'other' or order_type == 'market': + assert order['fee']['rate'] == fee + return + else: + assert order['fee'] is None - assert order['ft_fee_base'] == fee + mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', + return_value=price_side != 'other') + + order1 = exchange.fetch_dry_run_order(order['id']) + assert order1['fee']['rate'] == fee @pytest.mark.parametrize("side,startprice,endprice", [ @@ -5101,26 +5111,3 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert order['cost'] == 100 assert order['filled'] == 100 assert order['remaining'] == 100 - - -@pytest.mark.parametrize('order_reason,price_side,order_type,taker_or_maker', [ - ("entry", "same", "limit", "maker"), - ("exit", "same", "limit", "maker"), - ("entry", "other", "limit", "taker"), - ("exit", "other", "limit", "taker"), - ("stoploss", "same", "limit", "taker"), - ("stoploss", "other", "limit", "taker"), - ("entry", "same", "market", "taker"), - ("exit", "same", "market", "taker"), - ("entry", "other", "market", "taker"), - ("exit", "other", "market", "taker"), - ("stoploss", "same", "market", "taker"), - ("stoploss", "other", "market", "taker"), -]) -def test_taker_or_maker(mocker, default_conf, order_reason, price_side, order_type, taker_or_maker): - if order_reason != 'stoploss': - default_conf[f"{order_reason}_pricing"]["price_side"] = price_side - default_conf["order_types"] = {} - default_conf["order_types"][order_reason] = order_type - exchange = get_patched_exchange(mocker, default_conf) - assert exchange.taker_or_maker(order_reason) == taker_or_maker From 423af371c0ec8822f03cd9f10528e2c0d58e3512 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 17:59:05 +0200 Subject: [PATCH 8/8] Simplify calculation by calling "get_fee" only once --- freqtrade/exchange/exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index efb33ee74..a430cdac5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -865,11 +865,12 @@ class Exchange: dry_order: Dict[str, Any], taker_or_maker: MakerTaker, ) -> Dict[str, Any]: + fee = self.get_fee(pair, taker_or_maker=taker_or_maker) dry_order.update({ 'fee': { 'currency': self.get_pair_quote_currency(pair), - 'cost': dry_order['cost'] * self.get_fee(pair, taker_or_maker=taker_or_maker), - 'rate': self.get_fee(pair, taker_or_maker=taker_or_maker) + 'cost': dry_order['cost'] * fee, + 'rate': fee } }) return dry_order