From c9aa09ec89d37d6f8fcea4c9f15c092021a82a93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Aug 2022 20:46:06 +0200 Subject: [PATCH] Simplify base fee handling --- freqtrade/freqtradebot.py | 32 ++++++++++------------ tests/test_freqtradebot.py | 56 ++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ea9221213..5393e3d39 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1778,7 +1778,7 @@ class FreqtradeBot(LoggingMixin): self.rpc.send_msg(msg) def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, - amount: float, fee_abs: float) -> float: + amount: float, fee_abs: float) -> Optional[float]: """ Applies the fee to amount (either from Order or from Trades). Can eat into dust if more than the required asset is available. @@ -1791,35 +1791,32 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: - real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs) - logger.info(f"Applying fee on amount for {trade} " - f"(from {amount} to {real_amount}).") - return real_amount - return amount + logger.info(f"Applying fee on amount for {trade}, fee={fee_abs}.") + return fee_abs + return None def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None: # Try update amount (binance-fix) try: - new_amount = self.get_real_amount(trade, order, order_obj) - if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, - abs_tol=constants.MATH_CLOSE_PREC): - order_obj.ft_fee_base = trade.amount - new_amount + fee_abs = self.get_real_amount(trade, order, order_obj) + if fee_abs is not None: + order_obj.ft_fee_base = fee_abs except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) - def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> float: + def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> Optional[float]: """ Detect and update trade fee. Calls trade.update_fee() upon correct detection. Returns modified amount if the fee was taken from the destination currency. Necessary for exchanges which charge fees in base currency (e.g. binance) - :return: identical (or new) amount for the trade + :return: Absolute fee to apply for this order or None """ # Init variables order_amount = safe_value_fallback(order, 'filled', 'amount') # Only run for closed orders if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': - return order_amount + return None trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) # use fee from order-dict if possible @@ -1837,12 +1834,12 @@ class FreqtradeBot(LoggingMixin): # Apply fee to amount return self.apply_fee_conditional(trade, trade_base_currency, amount=order_amount, fee_abs=fee_cost) - return order_amount + return None return self.fee_detection_from_trades( trade, order, order_obj, order_amount, order.get('trades', [])) def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order, - order_amount: float, trades: List) -> float: + order_amount: float, trades: List) -> Optional[float]: """ fee-detection fallback to Trades. Either uses provided trades list or the result of fetch_my_trades to get correct fee. @@ -1853,7 +1850,7 @@ class FreqtradeBot(LoggingMixin): if len(trades) == 0: logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) - return order_amount + return None fee_currency = None amount = 0 fee_abs = 0.0 @@ -1897,8 +1894,7 @@ class FreqtradeBot(LoggingMixin): if fee_abs != 0: return self.apply_fee_conditional(trade, trade_base_currency, amount=amount, fee_abs=fee_abs) - else: - return amount + return None def get_valid_price(self, custom_price: float, proposed_price: float) -> float: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 138527053..902343c1e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1927,8 +1927,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', - return_value=order['amount']) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.0) order_id = order['id'] trade = Trade( @@ -1960,11 +1959,11 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca assert trade.amount == order['amount'] trade.open_order_id = order_id - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) - assert trade.amount != 90.81 + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01) + assert trade.amount == 30.0 # test amount modified by fee-logic freqtrade.update_trade_state(trade, order_id) - assert trade.amount == 90.81 + assert trade.amount == 29.99 assert trade.open_order_id is None trade.is_open = True @@ -4268,10 +4267,10 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe caplog.clear() order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') # Amount is reduced by "fee" - assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount - (amount * 0.001) + assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == (amount * 0.001) assert log_has( 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=False,' - ' leverage=1.0, open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', + ' leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.008.', caplog ) @@ -4296,7 +4295,7 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord walletmock.reset_mock() order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') # Amount is kept as is - assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount + assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None assert walletmock.call_count == 1 assert log_has_re(r'Fee amount for Trade.* was in base currency ' '- Eating Fee 0.008 into dust', caplog) @@ -4319,7 +4318,7 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') # Amount is reduced by "fee" - assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == amount + assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) is None assert log_has( 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) failed: ' @@ -4343,8 +4342,7 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock # from order ({'cost': 0.004, 'currency': 'LTC'}, 0.004, False, ( 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) (from' - ' 8.0 to 7.996).' + 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.004.' )), # invalid, no currency in from fee dict ({'cost': 0.008, 'currency': None}, 0, True, None), @@ -4376,7 +4374,11 @@ def test_get_real_amount( caplog.clear() order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') - assert freqtrade.get_real_amount(trade, buy_order, order_obj) == amount - fee_reduction_amount + res = freqtrade.get_real_amount(trade, buy_order, order_obj) + if fee_reduction_amount == 0: + assert res is None + else: + assert res == fee_reduction_amount if expected_log: assert log_has(expected_log, caplog) @@ -4422,14 +4424,14 @@ def test_get_real_amount_multi( return_value={'ask': 0.19, 'last': 0.2}) # Amount is reduced by "fee" - expected_amount = amount - (amount * fee_reduction_amount) + expected_amount = amount * fee_reduction_amount order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') assert freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount assert log_has( ( 'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' - 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed) ' - f'(from 8.0 to {expected_log_amount}).' + 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), ' + f'fee={expected_amount}.' ), caplog ) @@ -4462,7 +4464,7 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_ order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') # Amount does not change - assert freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) == amount + assert freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) is None def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee, @@ -4485,7 +4487,7 @@ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_dou # Amount does not change assert trade.fee_open == 0.0025 order_obj = Order.parse_from_ccxt_object(market_buy_order_usdt_doublefee, 'LTC/ETH', 'buy') - assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee, order_obj) == 30.0 + assert freqtrade.get_real_amount(trade, market_buy_order_usdt_doublefee, order_obj) is None assert tfo_mock.call_count == 0 # Fetch fees from trades dict if available to get "proper" values assert round(trade.fee_open, 4) == 0.001 @@ -4537,7 +4539,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') # Amount changes by fee amount. assert pytest.approx(freqtrade.get_real_amount( - trade, limit_buy_order_usdt, order_obj)) == amount - (amount * 0.001) + trade, limit_buy_order_usdt, order_obj)) == (amount * 0.001) def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker): @@ -4559,7 +4561,7 @@ def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker): } freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) order_obj = Order.parse_from_ccxt_object(order, 'LTC/ETH', 'buy') - assert freqtrade.get_real_amount(trade, order, order_obj) == amount + assert freqtrade.get_real_amount(trade, order, order_obj) is None def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, caplog): @@ -4616,7 +4618,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') res = freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) - assert res == amount + assert res is None assert trade.fee_open_currency is None assert trade.fee_open_cost is None message = "Not updating buy-fee - rate: None, POINT." @@ -4624,7 +4626,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, caplog.clear() freqtrade.config['exchange']['unknown_fee_rate'] = 1 res = freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) - assert res == amount + assert res is None assert trade.fee_open_currency == 'POINT' assert pytest.approx(trade.fee_open_cost) == 0.3046651026 assert trade.fee_open == 0.002 @@ -4633,12 +4635,12 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, @pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [ - (8.0, 0.0, 10, 8), - (8.0, 0.0, 0, 8), - (8.0, 0.1, 0, 7.9), - (8.0, 0.1, 10, 8), - (8.0, 0.1, 8.0, 8.0), - (8.0, 0.1, 7.9, 7.9), + (8.0, 0.0, 10, None), + (8.0, 0.0, 0, None), + (8.0, 0.1, 0, 0.1), + (8.0, 0.1, 10, None), + (8.0, 0.1, 8.0, None), + (8.0, 0.1, 7.9, 0.1), ]) def test_apply_fee_conditional(default_conf_usdt, fee, mocker, amount, fee_abs, wallet, amount_exp):