Merge pull request #2763 from freqtrade/fix/precision_rounding

Fix/precision rounding
This commit is contained in:
hroff-1902 2020-01-17 01:25:30 +03:00 committed by GitHub
commit 2f82122fc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 135 additions and 68 deletions

View File

@ -41,7 +41,7 @@ class Binance(Exchange):
""" """
ordertype = "stop_loss_limit" 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 # Ensure rate is less than stop price
if stop_price <= rate: if stop_price <= rate:
@ -57,9 +57,9 @@ class Binance(Exchange):
params = self._params.copy() params = self._params.copy()
params.update({'stopPrice': stop_price}) 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', order = self._api.create_order(pair, ordertype, 'sell',
amount, rate, params) amount, rate, params)

View File

@ -7,14 +7,15 @@ import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from math import ceil, floor from math import ceil
from random import randint from random import randint
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async 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 pandas import DataFrame
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
@ -189,6 +190,11 @@ class Exchange:
self._load_markets() self._load_markets()
return self._api.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, def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
pairs_only: bool = False, active_only: bool = False) -> Dict: 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] 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 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']: if self.markets[pair]['precision']['amount']:
symbol_prec = self.markets[pair]['precision']['amount'] amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
big_amount = amount * pow(10, symbol_prec) precision=self.markets[pair]['precision']['amount'],
amount = floor(big_amount) / pow(10, symbol_prec) counting_mode=self.precisionMode,
))
return amount 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 Rounds up
''' '''
if self.markets[pair]['precision']['price']: if self.markets[pair]['precision']['price']:
symbol_prec = self.markets[pair]['precision']['price'] # price = float(decimal_to_precision(price, rounding_mode=ROUND,
big_price = price * pow(10, symbol_prec) # precision=self.markets[pair]['precision']['price'],
price = ceil(big_price) / pow(10, symbol_prec) # 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 return price
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]: rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{randint(0, 10**6)}' 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 = { dry_order = {
"id": order_id, "id": order_id,
'pair': pair, 'pair': pair,
@ -446,13 +469,13 @@ class Exchange:
rate: float, params: Dict = {}) -> Dict: rate: float, params: Dict = {}) -> Dict:
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange # 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' needs_price = (ordertype != 'market'
or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) 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, return self._api.create_order(pair, ordertype, side,
amount, rate, params) amount, rate_for_order, params)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(

View File

@ -35,8 +35,8 @@ class PrecisionFilter(IPairList):
""" """
stop_price = ticker['ask'] * stoploss stop_price = ticker['ask'] * stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision
sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price) sp = self._exchange.price_to_precision(ticker["symbol"], stop_price)
stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99) stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99)
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price: if sp <= stop_gap_price:
logger.info(f"Removed {ticker['symbol']} from whitelist, " logger.info(f"Removed {ticker['symbol']} from whitelist, "

View File

@ -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.validate_stakecurrency', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) 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.name', PropertyMock(return_value=id.title()))
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
if mock_markets: if mock_markets:
mocker.patch('freqtrade.exchange.Exchange.markets', mocker.patch('freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=get_markets())) PropertyMock(return_value=get_markets()))

View File

@ -22,8 +22,8 @@ def test_stoploss_limit_order(default_conf, mocker):
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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') 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() api_mock = MagicMock()
order_type = 'stop_loss_limit' order_type = 'stop_loss_limit'
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')

View File

@ -175,35 +175,78 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
ex.validate_order_time_in_force(tif2) 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") exchange = get_patched_exchange(mocker, default_conf, id="binance")
mocker.patch('freqtrade.exchange.Exchange.markets', markets) 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' pair = 'ETH/BTC'
amount = exchange.symbol_amount_prec(pair, amount) assert pytest.approx(exchange.price_to_precision(pair, price)) == expected
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
def test_set_sandbox(default_conf, mocker): 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 default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
order = exchange.create_order( order = exchange.create_order(
@ -698,8 +741,8 @@ def test_buy_prod(default_conf, mocker, exchange_name):
} }
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
order = exchange.buy(pair='ETH/BTC', ordertype=order_type, 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 default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
order_type = 'limit' order_type = 'limit'
@ -834,8 +877,8 @@ def test_sell_prod(default_conf, mocker, exchange_name):
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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) 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) 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 = {} api_mock.options = {}
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
order_type = 'limit' order_type = 'limit'

View File

@ -21,8 +21,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
order = exchange.buy(pair='ETH/BTC', ordertype=order_type, 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 default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', 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") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)

View File

@ -2435,8 +2435,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
symbol_amount_prec=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
symbol_price_prec=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
stoploss_limit=stoploss_limit, stoploss_limit=stoploss_limit,
cancel_order=cancel_order, 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', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
symbol_amount_prec=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
symbol_price_prec=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
) )
stoploss_limit = MagicMock(return_value={ stoploss_limit = MagicMock(return_value={

View File

@ -58,8 +58,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
symbol_amount_prec=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
symbol_price_prec=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
get_order=stoploss_order_mock, get_order=stoploss_order_mock,
cancel_order=cancel_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', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_fee=fee, get_fee=fee,
symbol_amount_prec=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
symbol_price_prec=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
) )
mocker.patch.multiple( mocker.patch.multiple(