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"
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)

View File

@ -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(

View File

@ -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, "

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.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()))

View File

@ -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')

View File

@ -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'

View File

@ -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)

View File

@ -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={

View File

@ -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(