Repository: freqtrade/freqtrade

Issue:
FTX symbol_amount_prec wrong 
## Step 1: Have you search for this issue before posting it?

Yes

## Step 2: Describe your environment

  * Operating system: ubuntu19.10
  * Python Version: 3.7
  * CCXT version: latest
  * Branch: Develop
  
## Step 3: Describe the problem:

Bot found buy opportunities but can't buy using market 

### Steps to reproduce:

  1. Run with linkbull/usd pair on ftx
  2. Wait
 
  
### Observed Results:

  * What happened? It said couldn't buy 0.0
  * What did you expect to happen? To buy as usual. Do know that you can buy as small as 1 dollar on ftx spot markets (even less) 

### Relevant code exceptions or logs:

  ```
  2020-01-11 08:13:20,316 - freqtrade.freqtradebot - INFO - Buy signal found: about create a new trade with stake_amount: 20 ...
2020-01-11 08:13:20,317 - freqtrade.freqtradebot - INFO - Using Last Ask / Last Price
2020-01-11 08:13:20,469 - freqtrade.freqtradebot - WARNING - Unable to create trade for LINKBULL/USD: Could not create market buy order on market LINKBULL/USD.Tried to buy amount 0.0 at rate None.Message: ftx {"error":"Size too small","success":false}                                                                                 
2020
  ```



Discussion:
This is not limited to market-buys - limit buys will probably suffer from the same problem (depending on the pair - most likely).

The problem is caused by ccxt returning precision as decimals - while the ccxt documentation states it should be "The amounts of decimal digits accepted"

> `precision`. The amounts of decimal digits accepted in order values by exchanges upon order placement for price, amount and cost.
[source](https://github.com/ccxt/ccxt/wiki/Manual#market-structure)

I've opened an issue with [ccxt](https://github.com/ccxt/ccxt/issues/6364) for this ... but honestly, this problem leads me to belive that FTX is not well integraded / tested with ccxt... which will mean that support in freqtrade is far far away.


Don't worry, I'll debug it. 
I already did - as opened in the ccxt issue https://github.com/ccxt/ccxt/issues/6364 .

Still doesn't mean it's a well tested exchange (from a ccxt point of view) ... 
that's all i'm saying

Resolving patch:
diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py
--- a/freqtrade/exchange/binance.py
+++ b/freqtrade/exchange/binance.py
@@ -41,7 +41,7 @@ def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: floa
         """
         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 @@ def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: floa
             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)
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -7,14 +7,15 @@
 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
@@ -188,6 +189,11 @@ def markets(self) -> Dict:
             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:
         """
@@ -362,32 +368,49 @@ def exchange_has(self, endpoint: str) -> bool:
         """
         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,
@@ -422,13 +445,13 @@ def create_order(self, pair: str, ordertype: str, side: str, amount: float,
                      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(
diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py
--- a/freqtrade/pairlist/PrecisionFilter.py
+++ b/freqtrade/pairlist/PrecisionFilter.py
@@ -35,8 +35,8 @@ def _validate_precision_filter(self, ticker: dict, stoploss: float) -> bool:
         """
         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, "


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -62,6 +62,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No
     mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', 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()))
diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py
--- a/tests/exchange/test_binance.py
+++ b/tests/exchange/test_binance.py
@@ -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')
 
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -173,35 +173,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)
 
-    amount = 2.34559
     pair = 'ETH/BTC'
-    amount = exchange.symbol_amount_prec(pair, amount)
-    assert amount == 2.3455
-
+    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_symbol_price_prec(default_conf, mocker):
+])
+def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected):
     '''
-    Test rounds up to 4 decimal places
+    Test price to precision
     '''
-    markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
+    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))
 
-    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):
@@ -596,8 +639,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(
@@ -637,8 +680,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,
@@ -711,8 +754,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'
@@ -773,8 +816,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)
@@ -837,8 +880,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'
diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py
--- a/tests/exchange/test_kraken.py
+++ b/tests/exchange/test_kraken.py
@@ -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)
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -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={
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -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(