Simplify base fee handling

This commit is contained in:
Matthias 2022-08-30 20:46:06 +02:00
parent 50e2808667
commit c9aa09ec89
2 changed files with 43 additions and 45 deletions

View File

@ -1778,7 +1778,7 @@ class FreqtradeBot(LoggingMixin):
self.rpc.send_msg(msg) self.rpc.send_msg(msg)
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, 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). Applies the fee to amount (either from Order or from Trades).
Can eat into dust if more than the required asset is available. 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 - " logger.info(f"Fee amount for {trade} was in base currency - "
f"Eating Fee {fee_abs} into dust.") f"Eating Fee {fee_abs} into dust.")
elif fee_abs != 0: 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}, fee={fee_abs}.")
logger.info(f"Applying fee on amount for {trade} " return fee_abs
f"(from {amount} to {real_amount}).") return None
return real_amount
return amount
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None: def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order, order_obj) fee_abs = self.get_real_amount(trade, order, order_obj)
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, if fee_abs is not None:
abs_tol=constants.MATH_CLOSE_PREC): order_obj.ft_fee_base = fee_abs
order_obj.ft_fee_base = trade.amount - new_amount
except DependencyException as exception: except DependencyException as exception:
logger.warning("Could not update trade amount: %s", 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. Detect and update trade fee.
Calls trade.update_fee() upon correct detection. Calls trade.update_fee() upon correct detection.
Returns modified amount if the fee was taken from the destination currency. Returns modified amount if the fee was taken from the destination currency.
Necessary for exchanges which charge fees in base currency (e.g. binance) 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 # Init variables
order_amount = safe_value_fallback(order, 'filled', 'amount') order_amount = safe_value_fallback(order, 'filled', 'amount')
# Only run for closed orders # Only run for closed orders
if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': 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) trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible # use fee from order-dict if possible
@ -1837,12 +1834,12 @@ class FreqtradeBot(LoggingMixin):
# Apply fee to amount # Apply fee to amount
return self.apply_fee_conditional(trade, trade_base_currency, return self.apply_fee_conditional(trade, trade_base_currency,
amount=order_amount, fee_abs=fee_cost) amount=order_amount, fee_abs=fee_cost)
return order_amount return None
return self.fee_detection_from_trades( return self.fee_detection_from_trades(
trade, order, order_obj, order_amount, order.get('trades', [])) trade, order, order_obj, order_amount, order.get('trades', []))
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order, 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. fee-detection fallback to Trades.
Either uses provided trades list or the result of fetch_my_trades to get correct fee. 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: if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount return None
fee_currency = None fee_currency = None
amount = 0 amount = 0
fee_abs = 0.0 fee_abs = 0.0
@ -1897,8 +1894,7 @@ class FreqtradeBot(LoggingMixin):
if fee_abs != 0: if fee_abs != 0:
return self.apply_fee_conditional(trade, trade_base_currency, return self.apply_fee_conditional(trade, trade_base_currency,
amount=amount, fee_abs=fee_abs) amount=amount, fee_abs=fee_abs)
else: return None
return amount
def get_valid_price(self, custom_price: float, proposed_price: float) -> float: def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
""" """

View File

@ -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.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.0)
return_value=order['amount'])
order_id = order['id'] order_id = order['id']
trade = Trade( 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'] assert trade.amount == order['amount']
trade.open_order_id = order_id trade.open_order_id = order_id
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01)
assert trade.amount != 90.81 assert trade.amount == 30.0
# test amount modified by fee-logic # test amount modified by fee-logic
freqtrade.update_trade_state(trade, order_id) freqtrade.update_trade_state(trade, order_id)
assert trade.amount == 90.81 assert trade.amount == 29.99
assert trade.open_order_id is None assert trade.open_order_id is None
trade.is_open = True 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() caplog.clear()
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
# Amount is reduced by "fee" # 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( assert log_has(
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, is_short=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.992).', ' leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.008.',
caplog caplog
) )
@ -4296,7 +4295,7 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord
walletmock.reset_mock() walletmock.reset_mock()
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
# Amount is kept as is # 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 walletmock.call_count == 1
assert log_has_re(r'Fee amount for Trade.* was in base currency ' assert log_has_re(r'Fee amount for Trade.* was in base currency '
'- Eating Fee 0.008 into dust', caplog) '- 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') order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
# Amount is reduced by "fee" # 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( assert log_has(
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' '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: ' '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 # from order
({'cost': 0.004, 'currency': 'LTC'}, 0.004, False, ( ({'cost': 0.004, 'currency': 'LTC'}, 0.004, False, (
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' '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' 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), fee=0.004.'
' 8.0 to 7.996).'
)), )),
# invalid, no currency in from fee dict # invalid, no currency in from fee dict
({'cost': 0.008, 'currency': None}, 0, True, None), ({'cost': 0.008, 'currency': None}, 0, True, None),
@ -4376,7 +4374,11 @@ def test_get_real_amount(
caplog.clear() caplog.clear()
order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') 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: if expected_log:
assert log_has(expected_log, caplog) assert log_has(expected_log, caplog)
@ -4422,14 +4424,14 @@ def test_get_real_amount_multi(
return_value={'ask': 0.19, 'last': 0.2}) return_value={'ask': 0.19, 'last': 0.2})
# Amount is reduced by "fee" # 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') 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 freqtrade.get_real_amount(trade, buy_order_fee, order_obj) == expected_amount
assert log_has( assert log_has(
( (
'Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' '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) ' 'is_short=False, leverage=1.0, open_rate=0.24544100, open_since=closed), '
f'(from 8.0 to {expected_log_amount}).' f'fee={expected_amount}.'
), ),
caplog 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') order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
# Amount does not change # 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, 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 # Amount does not change
assert trade.fee_open == 0.0025 assert trade.fee_open == 0.0025
order_obj = Order.parse_from_ccxt_object(market_buy_order_usdt_doublefee, 'LTC/ETH', 'buy') 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 assert tfo_mock.call_count == 0
# Fetch fees from trades dict if available to get "proper" values # Fetch fees from trades dict if available to get "proper" values
assert round(trade.fee_open, 4) == 0.001 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') order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
# Amount changes by fee amount. # Amount changes by fee amount.
assert pytest.approx(freqtrade.get_real_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): 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) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
order_obj = Order.parse_from_ccxt_object(order, 'LTC/ETH', 'buy') 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): 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') 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) 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_currency is None
assert trade.fee_open_cost is None assert trade.fee_open_cost is None
message = "Not updating buy-fee - rate: None, POINT." 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() caplog.clear()
freqtrade.config['exchange']['unknown_fee_rate'] = 1 freqtrade.config['exchange']['unknown_fee_rate'] = 1
res = freqtrade.get_real_amount(trade, limit_buy_order_usdt, order_obj) 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 trade.fee_open_currency == 'POINT'
assert pytest.approx(trade.fee_open_cost) == 0.3046651026 assert pytest.approx(trade.fee_open_cost) == 0.3046651026
assert trade.fee_open == 0.002 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', [ @pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [
(8.0, 0.0, 10, 8), (8.0, 0.0, 10, None),
(8.0, 0.0, 0, 8), (8.0, 0.0, 0, None),
(8.0, 0.1, 0, 7.9), (8.0, 0.1, 0, 0.1),
(8.0, 0.1, 10, 8), (8.0, 0.1, 10, None),
(8.0, 0.1, 8.0, 8.0), (8.0, 0.1, 8.0, None),
(8.0, 0.1, 7.9, 7.9), (8.0, 0.1, 7.9, 0.1),
]) ])
def test_apply_fee_conditional(default_conf_usdt, fee, mocker, def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
amount, fee_abs, wallet, amount_exp): amount, fee_abs, wallet, amount_exp):