feat: Added price_rounding modes in config

This commit is contained in:
ASU 2023-03-09 02:11:31 +02:00
parent ba38a826e9
commit 1132fa6093
4 changed files with 113 additions and 51 deletions

View File

@ -5,6 +5,8 @@ bot constants
""" """
from typing import Any, Dict, List, Literal, Tuple from typing import Any, Dict, List, Literal, Tuple
from ccxt import ROUND, ROUND_DOWN, ROUND_UP, TRUNCATE
from freqtrade.enums import CandleType, PriceType, RPCMessageType from freqtrade.enums import CandleType, PriceType, RPCMessageType
@ -50,6 +52,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
# it has wide consequences for stored trades files # it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost'] DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
TRADING_MODES = ['spot', 'margin', 'futures'] TRADING_MODES = ['spot', 'margin', 'futures']
PRICE_ROUND_MODES = [TRUNCATE, ROUND, ROUND_UP, ROUND_DOWN]
DEFAULT_PRICE_ROUND_MODE = ROUND_UP
MARGIN_MODES = ['cross', 'isolated', ''] MARGIN_MODES = ['cross', 'isolated', '']
LAST_BT_RESULT_FN = '.last_result.json' LAST_BT_RESULT_FN = '.last_result.json'
@ -476,7 +480,9 @@ CONF_SCHEMA = {
'outdated_offset': {'type': 'integer', 'minimum': 1}, 'outdated_offset': {'type': 'integer', 'minimum': 1},
'markets_refresh_interval': {'type': 'integer'}, 'markets_refresh_interval': {'type': 'integer'},
'ccxt_config': {'type': 'object'}, 'ccxt_config': {'type': 'object'},
'ccxt_async_config': {'type': 'object'} 'ccxt_async_config': {'type': 'object'},
'price_rounding_mode': {'type': 'integer', 'enum': PRICE_ROUND_MODES,
'default': DEFAULT_PRICE_ROUND_MODE}
}, },
'required': ['name'] 'required': ['name']
}, },

View File

@ -19,9 +19,9 @@ from ccxt import TICK_SIZE
from dateutil import parser from dateutil import parser
from pandas import DataFrame, concat from pandas import DataFrame, concat
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, DEFAULT_PRICE_ROUND_MODE,
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker, NON_OPEN_EXCHANGE_STATES, BidAsk, BuySell, Config, EntryExit,
OBLiteral, PairWithTimeframe) ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType from freqtrade.enums.pricetype import PriceType
@ -143,7 +143,9 @@ class Exchange:
if config.get('margin_mode') if config.get('margin_mode')
else MarginMode.NONE else MarginMode.NONE
) )
self.liquidation_buffer = config.get('liquidation_buffer', 0.05) self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
self._price_rounding_mode = exchange_config.get("price_rounding_mode",
DEFAULT_PRICE_ROUND_MODE)
# Deep merge ft_has with default ft_has options # Deep merge ft_has with default ft_has options
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
@ -732,10 +734,11 @@ class Exchange:
def price_to_precision(self, pair: str, price: float) -> float: def price_to_precision(self, pair: str, price: float) -> float:
""" """
Returns the price rounded up to the precision the Exchange accepts. Returns the price rounded to the precision the Exchange accepts.
Rounds up The default price_rounding_mode in conf is ROUND_UP
""" """
return price_to_precision(price, self.get_precision_price(pair), self.precisionMode) return price_to_precision(price, self.get_precision_price(pair),
self.precisionMode, self._price_rounding_mode)
def price_get_one_pip(self, pair: str, price: float) -> float: def price_get_one_pip(self, pair: str, price: float) -> float:
""" """

View File

@ -2,11 +2,12 @@
Exchange support utils Exchange support utils
""" """
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil from math import ceil, floor
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
TRUNCATE, decimal_to_precision)
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise
@ -219,35 +220,48 @@ def amount_to_contract_precision(
return amount return amount
def price_to_precision(price: float, price_precision: Optional[float], def price_to_precision(
precisionMode: Optional[int]) -> float: price: float,
price_precision: Optional[float],
precisionMode: Optional[int],
rounding_mode: int = ROUND_UP,
) -> float:
""" """
Returns the price rounded up to the precision the Exchange accepts. Returns the price rounded up to the precision the Exchange accepts.
Partial Re-implementation of ccxt internal method decimal_to_precision(), Partial Re-implementation of ccxt internal method decimal_to_precision(),
which does not support rounding up which does not support rounding up
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
align with amount_to_precision(). align with amount_to_precision().
!!! Rounds up
:param price: price to convert :param price: price to convert
:param price_precision: price precision to use. Used from markets[pair]['precision']['price'] :param price_precision: price precision to use. Used from markets[pair]['precision']['price']
:param precisionMode: precision mode to use. Should be used from precisionMode :param precisionMode: precision mode to use. Should be used from precisionMode
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
:param rounding_mode: rounding mode to use. Defaults to ROUND_UP
:return: price rounded up to the precision the Exchange accepts :return: price rounded up to the precision the Exchange accepts
""" """
if price_precision is not None and precisionMode is not None: if price_precision is not None and precisionMode is not None:
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
# precision=price_precision,
# counting_mode=self.precisionMode,
# ))
if precisionMode == TICK_SIZE: if precisionMode == TICK_SIZE:
if rounding_mode == ROUND:
ticks = price / price_precision
rounded_ticks = round(ticks)
return rounded_ticks * price_precision
precision = FtPrecise(price_precision) precision = FtPrecise(price_precision)
price_str = FtPrecise(price) price_str = FtPrecise(price)
missing = price_str % precision missing = price_str % precision
if not missing == FtPrecise("0"): if not missing == FtPrecise("0"):
price = round(float(str(price_str - missing + precision)), 14) return round(float(str(price_str - missing + precision)), 14)
else: return price
symbol_prec = price_precision elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES):
big_price = price * pow(10, symbol_prec) ndigits = round(price_precision)
price = ceil(big_price) / pow(10, symbol_prec) if rounding_mode == ROUND:
return round(price, ndigits)
ticks = price * (10**ndigits)
if rounding_mode == ROUND_UP:
return ceil(ticks) / (10**ndigits)
if rounding_mode == TRUNCATE:
return int(ticks) / (10**ndigits)
if rounding_mode == ROUND_DOWN:
return floor(ticks) / (10**ndigits)
raise ValueError(f"Unknown rounding_mode {rounding_mode}")
raise ValueError(f"Unknown precisionMode {precisionMode}")
return price return price

View File

@ -8,6 +8,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
import arrow import arrow
import ccxt import ccxt
import pytest import pytest
from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE
from pandas import DataFrame from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
@ -312,35 +313,47 @@ def test_amount_to_precision(amount, precision_mode, precision, expected,):
assert amount_to_precision(amount, precision, precision_mode) == expected assert amount_to_precision(amount, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [ @pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [
(2.34559, 2, 4, 2.3456), # Tests for DECIMAL_PLACES, ROUND_UP
(2.34559, 2, 5, 2.34559), (2.34559, 2, 4, 2.3456, ROUND_UP),
(2.34559, 2, 3, 2.346), (2.34559, 2, 5, 2.34559, ROUND_UP),
(2.9999, 2, 3, 3.000), (2.34559, 2, 3, 2.346, ROUND_UP),
(2.9909, 2, 3, 2.991), (2.9999, 2, 3, 3.000, ROUND_UP),
# Tests for Tick_size (2.9909, 2, 3, 2.991, ROUND_UP),
(2.34559, 4, 0.0001, 2.3456), # Tests for DECIMAL_PLACES, ROUND
(2.34559, 4, 0.00001, 2.34559), (2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.34559, 4, 0.001, 2.346), (2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND),
(2.9999, 4, 0.001, 3.000), (2.49, DECIMAL_PLACES, 0, 2., ROUND),
(2.9909, 4, 0.001, 2.991), (2.51, DECIMAL_PLACES, 0, 3., ROUND),
(2.9909, 4, 0.005, 2.995), (5.1, DECIMAL_PLACES, -1, 10., ROUND),
(2.9973, 4, 0.005, 3.0), (4.9, DECIMAL_PLACES, -1, 0., ROUND),
(2.9977, 4, 0.005, 3.0), # Tests for TICK_SIZE, ROUND_UP
(234.43, 4, 0.5, 234.5), (2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP),
(234.53, 4, 0.5, 235.0), (2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP),
(0.891534, 4, 0.0001, 0.8916), (2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP),
(64968.89, 4, 0.01, 64968.89), (2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP),
(0.000000003483, 4, 1e-12, 0.000000003483), (2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP),
(2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP),
(2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP),
(234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP),
(234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP),
(0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP),
(64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP),
(0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP),
# Tests for TICK_SIZE, ROUND
(2.49, TICK_SIZE, 1., 2., ROUND),
(2.51, TICK_SIZE, 1., 3., ROUND),
(2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND),
(2.000000049, TICK_SIZE, 0.0000001, 2., ROUND),
(2.9909, TICK_SIZE, 0.005, 2.990, ROUND),
(2.9973, TICK_SIZE, 0.005, 2.995, ROUND),
(2.9977, TICK_SIZE, 0.005, 3.0, ROUND),
(234.24, TICK_SIZE, 0.5, 234., ROUND),
(234.26, TICK_SIZE, 0.5, 234.5, ROUND),
]) ])
def test_price_to_precision(price, precision_mode, precision, expected): def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode):
# digits counting mode assert price_to_precision(price, precision, precision_mode, rounding_mode) == expected
# DECIMAL_PLACES = 2
# SIGNIFICANT_DIGITS = 3
# TICK_SIZE = 4
assert price_to_precision(price, precision, precision_mode) == expected
@pytest.mark.parametrize("price,precision_mode,precision,expected", [ @pytest.mark.parametrize("price,precision_mode,precision,expected", [
@ -5307,3 +5320,29 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
assert order['cost'] == 100 assert order['cost'] == 100
assert order['filled'] == 100 assert order['filled'] == 100
assert order['remaining'] == 100 assert order['remaining'] == 100
def test_price_to_precision_with_default_conf(default_conf, mocker):
conf = copy.deepcopy(default_conf)
patched_ex = get_patched_exchange(mocker, conf)
prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101)
assert prec_price == 1.00000002
@pytest.mark.parametrize(
"rounding_mode, price, expected_price",
[
(TRUNCATE, 1.0000000199, 1.00000001),
(ROUND, 1.0000000149, 1.00000001),
(ROUND, 1.0000000151, 1.00000002),
(ROUND_UP, 1.0000000101, 1.00000002),
],
)
def test_price_to_precision_rounding_mode_from_conf(
default_conf, mocker, rounding_mode, price, expected_price
):
conf = copy.deepcopy(default_conf)
conf["exchange"]["price_rounding_mode"] = rounding_mode
patched_ex = get_patched_exchange(mocker, conf)
prec_price = patched_ex.price_to_precision("XRP/USDT", price)
assert prec_price == expected_price