Merge pull request #2763 from freqtrade/fix/precision_rounding
Fix/precision rounding
This commit is contained in:
commit
2f82122fc4
@ -41,7 +41,7 @@ class Binance(Exchange):
|
||||
"""
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.symbol_price_prec(pair, stop_price)
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price <= rate:
|
||||
@ -57,9 +57,9 @@ class Binance(Exchange):
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
rate = self.symbol_price_prec(pair, rate)
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
order = self._api.create_order(pair, ordertype, 'sell',
|
||||
amount, rate, params)
|
||||
|
@ -7,14 +7,15 @@ import inspect
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from math import ceil, floor
|
||||
from math import ceil
|
||||
from random import randint
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
import ccxt.async_support as ccxt_async
|
||||
from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP
|
||||
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
@ -189,6 +190,11 @@ class Exchange:
|
||||
self._load_markets()
|
||||
return self._api.markets
|
||||
|
||||
@property
|
||||
def precisionMode(self) -> str:
|
||||
"""exchange ccxt precisionMode"""
|
||||
return self._api.precisionMode
|
||||
|
||||
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
|
||||
pairs_only: bool = False, active_only: bool = False) -> Dict:
|
||||
"""
|
||||
@ -386,32 +392,49 @@ class Exchange:
|
||||
"""
|
||||
return endpoint in self._api.has and self._api.has[endpoint]
|
||||
|
||||
def symbol_amount_prec(self, pair, amount: float):
|
||||
def amount_to_precision(self, pair, amount: float) -> float:
|
||||
'''
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
Rounded down
|
||||
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
based on our definitions.
|
||||
'''
|
||||
if self.markets[pair]['precision']['amount']:
|
||||
symbol_prec = self.markets[pair]['precision']['amount']
|
||||
big_amount = amount * pow(10, symbol_prec)
|
||||
amount = floor(big_amount) / pow(10, symbol_prec)
|
||||
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||
precision=self.markets[pair]['precision']['amount'],
|
||||
counting_mode=self.precisionMode,
|
||||
))
|
||||
|
||||
return amount
|
||||
|
||||
def symbol_price_prec(self, pair, price: float):
|
||||
def price_to_precision(self, pair, price: float) -> float:
|
||||
'''
|
||||
Returns the price buying or selling with to the precision the Exchange accepts
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Partial Reimplementation of ccxt internal method decimal_to_precision(),
|
||||
which does not support rounding up
|
||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||
align with amount_to_precision().
|
||||
Rounds up
|
||||
'''
|
||||
if self.markets[pair]['precision']['price']:
|
||||
symbol_prec = self.markets[pair]['precision']['price']
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
||||
# precision=self.markets[pair]['precision']['price'],
|
||||
# counting_mode=self.precisionMode,
|
||||
# ))
|
||||
if self.precisionMode == TICK_SIZE:
|
||||
precision = self.markets[pair]['precision']['price']
|
||||
missing = price % precision
|
||||
if missing != 0:
|
||||
price = price - missing + precision
|
||||
else:
|
||||
symbol_prec = self.markets[pair]['precision']['price']
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
return price
|
||||
|
||||
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{randint(0, 10**6)}'
|
||||
_amount = self.symbol_amount_prec(pair, amount)
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order = {
|
||||
"id": order_id,
|
||||
'pair': pair,
|
||||
@ -446,13 +469,13 @@ class Exchange:
|
||||
rate: float, params: Dict = {}) -> Dict:
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
needs_price = (ordertype != 'market'
|
||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||
rate = self.symbol_price_prec(pair, rate) if needs_price else None
|
||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||
|
||||
return self._api.create_order(pair, ordertype, side,
|
||||
amount, rate, params)
|
||||
amount, rate_for_order, params)
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
|
@ -35,8 +35,8 @@ class PrecisionFilter(IPairList):
|
||||
"""
|
||||
stop_price = ticker['ask'] * stoploss
|
||||
# Adjust stop-prices to precision
|
||||
sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price)
|
||||
stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99)
|
||||
sp = self._exchange.price_to_precision(ticker["symbol"], stop_price)
|
||||
stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99)
|
||||
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
|
||||
if sp <= stop_gap_price:
|
||||
logger.info(f"Removed {ticker['symbol']} from whitelist, "
|
||||
|
@ -63,6 +63,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||
if mock_markets:
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||
PropertyMock(return_value=get_markets()))
|
||||
|
@ -22,8 +22,8 @@ def test_stoploss_limit_order(default_conf, mocker):
|
||||
})
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
@ -71,8 +71,8 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop_loss_limit'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
|
@ -175,35 +175,78 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
||||
ex.validate_order_time_in_force(tif2)
|
||||
|
||||
|
||||
def test_symbol_amount_prec(default_conf, mocker):
|
||||
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
|
||||
(2.34559, 2, 4, 2.3455),
|
||||
(2.34559, 2, 5, 2.34559),
|
||||
(2.34559, 2, 3, 2.345),
|
||||
(2.9999, 2, 3, 2.999),
|
||||
(2.9909, 2, 3, 2.990),
|
||||
# Tests for Tick-size
|
||||
(2.34559, 4, 0.0001, 2.3455),
|
||||
(2.34559, 4, 0.00001, 2.34559),
|
||||
(2.34559, 4, 0.001, 2.345),
|
||||
(2.9999, 4, 0.001, 2.999),
|
||||
(2.9909, 4, 0.001, 2.990),
|
||||
(2.9909, 4, 0.005, 2.990),
|
||||
(2.9999, 4, 0.005, 2.995),
|
||||
])
|
||||
def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected):
|
||||
'''
|
||||
Test rounds down to 4 Decimal places
|
||||
Test rounds down
|
||||
'''
|
||||
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}})
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}})
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
# digits counting mode
|
||||
# DECIMAL_PLACES = 2
|
||||
# SIGNIFICANT_DIGITS = 3
|
||||
# TICK_SIZE = 4
|
||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
|
||||
PropertyMock(return_value=precision_mode))
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||
|
||||
pair = 'ETH/BTC'
|
||||
assert exchange.amount_to_precision(pair, amount) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("price,precision_mode,precision,expected", [
|
||||
(2.34559, 2, 4, 2.3456),
|
||||
(2.34559, 2, 5, 2.34559),
|
||||
(2.34559, 2, 3, 2.346),
|
||||
(2.9999, 2, 3, 3.000),
|
||||
(2.9909, 2, 3, 2.991),
|
||||
# Tests for Tick_size
|
||||
(2.34559, 4, 0.0001, 2.3456),
|
||||
(2.34559, 4, 0.00001, 2.34559),
|
||||
(2.34559, 4, 0.001, 2.346),
|
||||
(2.9999, 4, 0.001, 3.000),
|
||||
(2.9909, 4, 0.001, 2.991),
|
||||
(2.9909, 4, 0.005, 2.995),
|
||||
(2.9973, 4, 0.005, 3.0),
|
||||
(2.9977, 4, 0.005, 3.0),
|
||||
(234.43, 4, 0.5, 234.5),
|
||||
(234.53, 4, 0.5, 235.0),
|
||||
(0.891534, 4, 0.0001, 0.8916),
|
||||
|
||||
])
|
||||
def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
|
||||
'''
|
||||
Test price to precision
|
||||
'''
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}})
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||
# digits counting mode
|
||||
# DECIMAL_PLACES = 2
|
||||
# SIGNIFICANT_DIGITS = 3
|
||||
# TICK_SIZE = 4
|
||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode',
|
||||
PropertyMock(return_value=precision_mode))
|
||||
|
||||
amount = 2.34559
|
||||
pair = 'ETH/BTC'
|
||||
amount = exchange.symbol_amount_prec(pair, amount)
|
||||
assert amount == 2.3455
|
||||
|
||||
|
||||
def test_symbol_price_prec(default_conf, mocker):
|
||||
'''
|
||||
Test rounds up to 4 decimal places
|
||||
'''
|
||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||
|
||||
price = 2.34559
|
||||
pair = 'ETH/BTC'
|
||||
price = exchange.symbol_price_prec(pair, price)
|
||||
assert price == 2.3456
|
||||
assert pytest.approx(exchange.price_to_precision(pair, price)) == expected
|
||||
|
||||
|
||||
def test_set_sandbox(default_conf, mocker):
|
||||
@ -657,8 +700,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order = exchange.create_order(
|
||||
@ -698,8 +741,8 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
@ -772,8 +815,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order_type = 'limit'
|
||||
@ -834,8 +877,8 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
@ -898,8 +941,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
||||
})
|
||||
api_mock.options = {}
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order_type = 'limit'
|
||||
|
@ -21,8 +21,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
@ -53,8 +53,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
@ -2435,8 +2435,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
symbol_amount_prec=lambda s, x, y: y,
|
||||
symbol_price_prec=lambda s, x, y: y,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
stoploss_limit=stoploss_limit,
|
||||
cancel_order=cancel_order,
|
||||
)
|
||||
@ -2478,8 +2478,8 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
symbol_amount_prec=lambda s, x, y: y,
|
||||
symbol_price_prec=lambda s, x, y: y,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
|
||||
stoploss_limit = MagicMock(return_value={
|
||||
|
@ -58,8 +58,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
symbol_amount_prec=lambda s, x, y: y,
|
||||
symbol_price_prec=lambda s, x, y: y,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
get_order=stoploss_order_mock,
|
||||
cancel_order=cancel_order_mock,
|
||||
)
|
||||
@ -137,8 +137,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
symbol_amount_prec=lambda s, x, y: y,
|
||||
symbol_price_prec=lambda s, x, y: y,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
|
||||
mocker.patch.multiple(
|
||||
|
Loading…
Reference in New Issue
Block a user