diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d7ace131c..abf43b24d 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,6 +22,7 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] DRY_RUN_WALLET = 999.9 +MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons TICKER_INTERVALS = [ '1m', '3m', '5m', '15m', '30m', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f56b1f2ea..3f7eab27a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -6,6 +6,7 @@ import copy import logging import traceback from datetime import datetime +from math import isclose from typing import Any, Dict, List, Optional, Tuple import arrow @@ -510,7 +511,7 @@ class FreqtradeBot: trade.pair.startswith(exectrade['fee']['currency'])): fee_abs += exectrade['fee']['cost'] - if amount != order_amount: + if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): logger.warning(f"Amount {amount} does not match amount {trade.amount}") raise OperationalException("Half bought? Amounts don't match") real_amount = amount - fee_abs @@ -535,7 +536,7 @@ class FreqtradeBot: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) - if order['amount'] != new_amount: + if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c26186e9d..ee28f2e58 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4,6 +4,7 @@ import logging import time from copy import deepcopy +from math import isclose from unittest.mock import MagicMock, PropertyMock import arrow @@ -12,6 +13,7 @@ import requests from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError, constants) +from freqtrade.constants import MATH_CLOSE_PREC from freqtrade.data.dataprovider import DataProvider from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade @@ -1635,6 +1637,31 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ assert trade.amount == limit_buy_order['amount'] +def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, + limit_buy_order, mocker, caplog): + trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + # get_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + patch_exchange(mocker) + Trade.session = MagicMock() + amount = sum(x['amount'] for x in trades_for_order) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456", + is_open=True, + open_date=arrow.utcnow().datetime, + ) + freqtrade.update_trade_state(trade, limit_buy_order) + assert trade.amount != amount + assert trade.amount == limit_buy_order['amount'] + assert log_has_re(r'Applying fee on amount for .*', caplog) + + def test_update_trade_state_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -3205,6 +3232,54 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order assert freqtrade.get_real_amount(trade, limit_buy_order) == amount +def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, mocker): + limit_buy_order = deepcopy(buy_order_fee) + limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001 + + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + amount = float(sum(x['amount'] for x in trades_for_order)) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Amount does not change + with pytest.raises(OperationalException, match=r"Half bought\? Amounts don't match"): + freqtrade.get_real_amount(trade, limit_buy_order) + + +def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, + mocker): + # Floats should not be compared directly. + limit_buy_order = deepcopy(buy_order_fee) + trades_for_order[0]['amount'] = trades_for_order[0]['amount'] + 1e-15 + + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + amount = float(sum(x['amount'] for x in trades_for_order)) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Amount changes by fee amount. + assert isclose(freqtrade.get_real_amount(trade, limit_buy_order), amount - (amount * 0.001), + abs_tol=MATH_CLOSE_PREC,) + + def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker): # Remove "Currency" from fee dict trades_for_order[0]['fee'] = {'cost': 0.008}