From 123a556ec89e5e0eace9ca0f279d612b382e24f0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 31 May 2020 13:05:58 +0300 Subject: [PATCH 01/76] Better exchange logging --- freqtrade/exchange/exchange.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index af745e8d0..6275b0eda 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -887,14 +887,20 @@ class Exchange: Async wrapper handling downloading trades using either time or id based methods. """ + logger.debug(f"_async_get_trade_history(), pair: {pair}, " + f"since: {since}, until: {until}, from_id: {from_id}") + + if not until: + exchange_msec = ccxt.Exchange.milliseconds() + logger.debug(f"Exchange milliseconds: {exchange_msec}") + until = exchange_msec + if self._trades_pagination == 'time': return await self._async_get_trade_history_time( - pair=pair, since=since, - until=until or ccxt.Exchange.milliseconds()) + pair=pair, since=since, until=until) elif self._trades_pagination == 'id': return await self._async_get_trade_history_id( - pair=pair, since=since, - until=until or ccxt.Exchange.milliseconds(), from_id=from_id + pair=pair, since=since, until=until, from_id=from_id ) else: raise OperationalException(f"Exchange {self.name} does use neither time, " From 85fedf95e85a871579731a55911694d406660bb4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 2 Jun 2020 18:43:37 +0300 Subject: [PATCH 02/76] Make mypy happy --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6275b0eda..397e0c1a7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -890,7 +890,7 @@ class Exchange: logger.debug(f"_async_get_trade_history(), pair: {pair}, " f"since: {since}, until: {until}, from_id: {from_id}") - if not until: + if until is None: exchange_msec = ccxt.Exchange.milliseconds() logger.debug(f"Exchange milliseconds: {exchange_msec}") until = exchange_msec From f4c2bb13468d0edaf78dc3ab31b89c8876851e35 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 2 Jun 2020 19:37:08 +0300 Subject: [PATCH 03/76] Fix crash in #3404 --- freqtrade/data/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 0ef7955a4..f73775432 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -197,7 +197,7 @@ def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame: df_new['date'] = df_new.index # Drop 0 volume rows df_new = df_new.dropna() - return df_new[DEFAULT_DATAFRAME_COLUMNS] + return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS] def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool): From 48117666fe9ef6fa9510e5daad86dd3a895a0ce1 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Tue, 2 Jun 2020 21:09:23 +0300 Subject: [PATCH 04/76] Update freqtrade/exchange/exchange.py Co-authored-by: Matthias --- freqtrade/exchange/exchange.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 397e0c1a7..031e464b7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -891,9 +891,8 @@ class Exchange: f"since: {since}, until: {until}, from_id: {from_id}") if until is None: - exchange_msec = ccxt.Exchange.milliseconds() - logger.debug(f"Exchange milliseconds: {exchange_msec}") - until = exchange_msec + until = ccxt.Exchange.milliseconds() + logger.debug(f"Exchange milliseconds: {until}") if self._trades_pagination == 'time': return await self._async_get_trade_history_time( From ea954b43389feef6ca0323c95f858df82b17cce6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 20:54:14 +0200 Subject: [PATCH 05/76] Add failing test with testcase from incident Full problem in #3431 --- tests/exchange/test_exchange.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 25aecba5c..32163f696 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2192,12 +2192,18 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: 'fee': {'currency': 'NEO', 'cost': 0.0012}}, 0.001944), ({'symbol': 'ETH/BTC', 'amount': 2.21, 'cost': 0.02992561, 'fee': {'currency': 'NEO', 'cost': 0.00027452}}, 0.00074305), - # TODO: More tests here! # Rate included in return - return as is ({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05, 'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.01}}, 0.01), ({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05, 'fee': {'currency': 'USDT', 'cost': 0.34, 'rate': 0.005}}, 0.005), + # 0.1% filled - no costs (kraken - #3431) + ({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0, + 'fee': {'currency': 'BTC', 'cost': 0.0, 'rate': None}}, None), + ({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0, + 'fee': {'currency': 'ETH', 'cost': 0.0, 'rate': None}}, 0.0), + ({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.0, + 'fee': {'currency': 'NEO', 'cost': 0.0, 'rate': None}}, None), ]) def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081}) From a2551daf12c68434449ea512433482cfc2736696 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 20:55:12 +0200 Subject: [PATCH 06/76] Fix ZeroDivision problem where cost is 0.0 --- freqtrade/exchange/exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7ef0d7750..4acee710a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1104,9 +1104,12 @@ class Exchange: order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8) elif fee_curr in self.get_pair_quote_currency(order['symbol']): # Quote currency - divide by cost - return round(order['fee']['cost'] / order['cost'], 8) + return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None else: # If Fee currency is a different currency + if not order['cost']: + # If cost is None or 0.0 -> falsy, return None + return None try: comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency']) tick = self.fetch_ticker(comb) From ad61673d6feec44a91c48cf805381cca28f11cee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 21:10:12 +0200 Subject: [PATCH 07/76] Fix missing key in test order --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 971f7a5fa..1cd3bcc0f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1590,6 +1590,7 @@ def buy_order_fee(): 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), 'price': 0.245441, 'amount': 8.0, + 'cost': 1.963528, 'remaining': 90.99181073, 'status': 'closed', 'fee': None From 78dea19ffb4e0efa9fa7594fadfddd49010a06e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Mar 2020 20:57:12 +0100 Subject: [PATCH 08/76] Implement first version of FTX stop --- freqtrade/exchange/ftx.py | 53 +++++++++++++++++ tests/exchange/test_ftx.py | 106 ++++++++++++++++++++++++++++++++++ tests/exchange/test_kraken.py | 10 ++-- 3 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 tests/exchange/test_ftx.py diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 75915122b..d06d49e5a 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -2,6 +2,10 @@ import logging from typing import Dict +import ccxt + +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -10,5 +14,54 @@ logger = logging.getLogger(__name__) class Ftx(Exchange): _ft_has: Dict = { + "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, } + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop' and stop_loss > float(order['price']) + + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + """ + Creates a stoploss market order. + Stoploss market orders is the only stoploss type supported by kraken. + """ + + ordertype = "stop" + + stop_price = self.price_to_precision(pair, stop_price) + + if self._config['dry_run']: + dry_order = self.dry_run_order( + pair, ordertype, "sell", amount, stop_price) + return dry_order + + try: + params = self._params.copy() + + amount = self.amount_to_precision(pair, amount) + + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + amount=amount, price=stop_price, params=params) + logger.info('stoploss order added for %s. ' + 'stop price: %s.', pair, stop_price) + return order + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' + f'Message: {e}') from e + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Could not create {ordertype} sell order on market {pair}. ' + f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' + f'Message: {e}') from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py new file mode 100644 index 000000000..f17d5b42e --- /dev/null +++ b/tests/exchange/test_ftx.py @@ -0,0 +1,106 @@ +# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement +# pragma pylint: disable=protected-access +from random import randint +from unittest.mock import MagicMock + +import ccxt +import pytest + +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) +from tests.conftest import get_patched_exchange + + +STOPLOSS_ORDERTYPE = 'stop' + + +def test_stoploss_order_ftx(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = False + 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, 'ftx') + + # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + assert api_mock.create_order.call_count == 1 + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(InvalidOrderException): + api_mock.create_order = MagicMock( + side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(OperationalException, match=r".*DeadBeef.*"): + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_ftx(default_conf, mocker): + api_mock = MagicMock() + default_conf['dry_run'] = True + 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, 'ftx') + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + assert 'id' in order + assert 'info' in order + assert 'type' in order + + assert order['type'] == STOPLOSS_ORDERTYPE + assert order['price'] == 220 + assert order['amount'] == 1 + + +def test_stoploss_adjust_ftx(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='ftx') + order = { + 'type': STOPLOSS_ORDERTYPE, + 'price': 1500, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case ... + order['type'] = 'stop_loss_limit' + assert not exchange.stoploss_adjust(1501, order) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index d63dd66cc..0950979cf 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -11,6 +11,8 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException, from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers +STOPLOSS_ORDERTYPE = 'stop-loss' + def test_buy_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() @@ -159,7 +161,6 @@ def test_get_balances_prod(default_conf, mocker): def test_stoploss_order_kraken(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop-loss' api_mock.create_order = MagicMock(return_value={ 'id': order_id, @@ -187,7 +188,7 @@ def test_stoploss_order_kraken(default_conf, mocker): assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' - assert api_mock.create_order.call_args_list[0][1]['type'] == order_type + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 @@ -218,7 +219,6 @@ def test_stoploss_order_kraken(default_conf, mocker): def test_stoploss_order_dry_run_kraken(default_conf, mocker): api_mock = MagicMock() - order_type = 'stop-loss' default_conf['dry_run'] = True 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) @@ -233,7 +233,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): assert 'info' in order assert 'type' in order - assert order['type'] == order_type + assert order['type'] == STOPLOSS_ORDERTYPE assert order['price'] == 220 assert order['amount'] == 1 @@ -241,7 +241,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): def test_stoploss_adjust_kraken(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf, id='kraken') order = { - 'type': 'stop-loss', + 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } assert exchange.stoploss_adjust(1501, order) From 68a59fd26d720efa3d22f9f67bbafac8ce2d280b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Mar 2020 20:58:05 +0100 Subject: [PATCH 09/76] Add Hint to suggest this is still broken --- freqtrade/exchange/ftx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index d06d49e5a..97257530e 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -28,7 +28,8 @@ class Ftx(Exchange): def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. - Stoploss market orders is the only stoploss type supported by kraken. + Stoploss market orders is the only stoploss type supported by ftx. + TODO: This doesnot work yet as the order cannot be aquired via fetch_orders - so Freqtrade assumes the order as always missing. """ ordertype = "stop" From a808fb3b1068baa7a1c522ac4840ac93d8438b07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 15:32:52 +0100 Subject: [PATCH 10/76] versionbump ccxt to first version that has FTX implemented correctly --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 20963a15f..6d832e3f5 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ setup(name='freqtrade', tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], install_requires=[ # from requirements-common.txt - 'ccxt>=1.18.1080', + 'ccxt>=1.24.96', 'SQLAlchemy', 'python-telegram-bot', 'arrow', From d90d6ed5d01283040b61416925a19dd1541fe037 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 15:50:33 +0100 Subject: [PATCH 11/76] Add ftx to tested exchanges --- tests/exchange/test_exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 32163f696..d8950dd09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -25,7 +25,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re # Make sure to always keep one exchange here which is NOT subclassed!! -EXCHANGES = ['bittrex', 'binance', 'kraken', ] +EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines @@ -1258,7 +1258,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls - since = 5 * 60 * 500 * 1.8 + + since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 From f83c1c5abf5d2b20ec29adfc84c85d3bcca6911d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 17:01:11 +0100 Subject: [PATCH 12/76] Use get_stoploss_order and cancel_stoploss_order This allows exchanges to use stoploss which don't have the same endpoints --- freqtrade/exchange/exchange.py | 8 ++++++- freqtrade/exchange/ftx.py | 44 ++++++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 10 ++++---- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 038fc22bc..e666b07ec 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -79,7 +79,7 @@ class Exchange: if config['dry_run']: logger.info('Instance is running with dry_run enabled') - + logger.info(f"Using CCXT {ccxt.__version__}") exchange_config = config['exchange'] # Deep merge ft_has with default ft_has options @@ -952,6 +952,9 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + # Assign method to get_stoploss_order to allow easy overriding in other classes + cancel_stoploss_order = cancel_order + def is_cancel_order_result_suitable(self, corder) -> bool: if not isinstance(corder, dict): return False @@ -1004,6 +1007,9 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + # Assign method to get_stoploss_order to allow easy overriding in other classes + get_stoploss_order = get_order + @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 97257530e..70f140ac5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -7,6 +7,7 @@ import ccxt from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -66,3 +67,46 @@ class Ftx(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + @retrier + def get_stoploss_order(self, order_id: str, pair: str) -> Dict: + if self._config['dry_run']: + try: + order = self._dry_run_open_orders[order_id] + return order + except KeyError as e: + # Gracefully handle errors with dry-run orders. + raise InvalidOrderException( + f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + try: + orders = self._api.fetch_orders('BNB/USD', None, params={'type': 'stop'}) + + order = [order for order in orders if order['id'] == order_id] + if len(order) == 1: + return order[0] + else: + raise InvalidOrderException(f"Could not get Stoploss Order for id {order_id}") + + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def cancel_stoploss_order(self, order_id: str, pair: str) -> None: + if self._config['dry_run']: + return + try: + return self._api.cancel_order(order_id, pair, params={'type': 'stop'}) + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Could not cancel order. Message: {e}') from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a74b0a5a1..4e4fe6e11 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -774,13 +774,13 @@ class FreqtradeBot: try: # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ + stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] == 'closed': + if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, stoploss_order, sl_order=True) # Lock pair for one candle to prevent immediate rebuys @@ -807,7 +807,7 @@ class FreqtradeBot: return False # If stoploss order is canceled for some reason we add it - if stoploss_order and stoploss_order['status'] == 'canceled': + if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss, rate=trade.stop_loss): return False @@ -840,7 +840,7 @@ class FreqtradeBot: logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) ' 'in order to add another one ...', order['id']) try: - self.exchange.cancel_order(order['id'], trade.pair) + self.exchange.cancel_stoploss_order(order['id'], trade.pair) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {order['id']} " f"for pair {trade.pair}") @@ -1068,7 +1068,7 @@ class FreqtradeBot: # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: try: - self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") From cf50c1cb7be618f48ab9268be506a5b18765d871 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 17:01:45 +0100 Subject: [PATCH 13/76] Add tests for new exchange methods --- tests/exchange/test_exchange.py | 51 +++++++++++++++++++++++++++++++++ tests/exchange/test_ftx.py | 35 +++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d8950dd09..c06b934ba 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1734,6 +1734,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} + assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -1818,6 +1819,25 @@ def test_cancel_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_stoploss_order(default_conf, mocker, exchange_name): + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.cancel_order = MagicMock(return_value=123) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123 + + with pytest.raises(InvalidOrderException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') + assert api_mock.cancel_order.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "cancel_stoploss_order", "cancel_order", + order_id='_', pair='TKN/BTC') + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True @@ -1847,6 +1867,37 @@ def test_get_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_stoploss_order(default_conf, mocker, exchange_name): + # Don't test FTX here - that needs a seperate test + if exchange_name == 'ftx': + return + default_conf['dry_run'] = True + order = MagicMock() + order.myid = 123 + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange._dry_run_open_orders['X'] = order + assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_stoploss_order('Y', 'TKN/BTC') + + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_order = MagicMock(return_value=456) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456 + + with pytest.raises(InvalidOrderException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + assert api_mock.fetch_order.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + 'get_stoploss_order', 'fetch_order', + order_id='_', pair='TKN/BTC') + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index f17d5b42e..2b75e5324 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -9,7 +9,7 @@ import pytest from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from tests.conftest import get_patched_exchange - +from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' @@ -104,3 +104,36 @@ def test_stoploss_adjust_ftx(mocker, default_conf): # Test with invalid order case ... order['type'] = 'stop_loss_limit' assert not exchange.stoploss_adjust(1501, order) + + +def test_get_stoploss_order(default_conf, mocker): + default_conf['dry_run'] = True + order = MagicMock() + order.myid = 123 + exchange = get_patched_exchange(mocker, default_conf, id='ftx') + exchange._dry_run_open_orders['X'] = order + assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_stoploss_order('Y', 'TKN/BTC') + + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') + assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' + + api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') + with pytest.raises(InvalidOrderException, match=r"Could not get Stoploss Order for id X"): + exchange.get_stoploss_order('X', 'TKN/BTC')['status'] + + with pytest.raises(InvalidOrderException): + api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') + exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + assert api_mock.fetch_orders.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', + 'get_stoploss_order', 'fetch_orders', + order_id='_', pair='TKN/BTC') From 3174f37b41b515a886673501055ce4fe4d394947 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 17:02:47 +0100 Subject: [PATCH 14/76] adapt tests to use stoploss_* methods --- tests/test_freqtradebot.py | 33 ++++++++++++++++++--------------- tests/test_integration.py | 4 ++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 487e3a60e..dc5a5c7d3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1126,7 +1126,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1139,7 +1139,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1164,7 +1164,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None @@ -1183,7 +1183,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', + side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1214,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - get_order=MagicMock(return_value={'status': 'canceled'}), + get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=DependencyException()), ) freqtrade = FreqtradeBot(default_conf) @@ -1331,7 +1332,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1346,7 +1347,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds @@ -1429,8 +1430,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1439,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) + cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 @@ -1510,7 +1512,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -1519,7 +1521,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # price goes down 5% @@ -2632,7 +2634,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) sellmock = MagicMock() patch_exchange(mocker) @@ -2680,7 +2683,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, stoploss=stoploss, - cancel_order=cancel_order, + cancel_stoploss_order=cancel_order, ) freqtrade = FreqtradeBot(default_conf) @@ -2771,7 +2774,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None diff --git a/tests/test_integration.py b/tests/test_integration.py index 1396e86f5..57960503e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -62,8 +62,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, get_fee=fee, 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, + get_stoploss_order=stoploss_order_mock, + cancel_stoploss_order=cancel_order_mock, ) mocker.patch.multiple( From 11ebdefd09ef1d587e326b0d7d23020f32d71ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Apr 2020 19:52:21 +0200 Subject: [PATCH 15/76] Fix bug after rebase --- freqtrade/exchange/ftx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 70f140ac5..0bd32b581 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -97,9 +97,9 @@ class Ftx(Exchange): raise OperationalException(e) from e @retrier - def cancel_stoploss_order(self, order_id: str, pair: str) -> None: + def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - return + return {} try: return self._api.cancel_order(order_id, pair, params={'type': 'stop'}) except ccxt.InvalidOrder as e: From b58fd179f231ed304d8d47d85c8d43169218b0a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 10:05:14 +0200 Subject: [PATCH 16/76] Don't hardcode pair ... --- freqtrade/exchange/ftx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 0bd32b581..1bc97c4d3 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -79,7 +79,7 @@ class Ftx(Exchange): raise InvalidOrderException( f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: - orders = self._api.fetch_orders('BNB/USD', None, params={'type': 'stop'}) + orders = self._api.fetch_orders(pair, None, params={'type': 'stop'}) order = [order for order in orders if order['id'] == order_id] if len(order) == 1: From 1d9aeef792a4eaa100646f04aaf6e5d47a2169b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 10:36:23 +0200 Subject: [PATCH 17/76] Support stop order in persistence --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 823bf6dc0..2e02c3999 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -374,7 +374,7 @@ class Trade(_DECL_BASE): elif order_type in ('market', 'limit') and order['side'] == 'sell': self.close(order['price']) logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self) - elif order_type in ('stop_loss_limit', 'stop-loss'): + elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss logger.info('%s is hit for %s.', order_type.upper(), self) From 77a62b845a1d5c13146966e3609f40e99f39fbf2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 11:24:23 +0200 Subject: [PATCH 18/76] Fix some comments --- freqtrade/exchange/ftx.py | 2 +- tests/exchange/test_exchange.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 1bc97c4d3..20d643a83 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -30,7 +30,6 @@ class Ftx(Exchange): """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by ftx. - TODO: This doesnot work yet as the order cannot be aquired via fetch_orders - so Freqtrade assumes the order as always missing. """ ordertype = "stop" @@ -46,6 +45,7 @@ class Ftx(Exchange): params = self._params.copy() amount = self.amount_to_precision(pair, amount) + # set orderPrice to place limit order (?) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=stop_price, params=params) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c06b934ba..dc272de36 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1898,6 +1898,7 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name): 'get_stoploss_order', 'fetch_order', order_id='_', pair='TKN/BTC') + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) From f0eb0bc350a876c514deb63e331cedac6707a884 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 11:33:40 +0200 Subject: [PATCH 19/76] Support limit orders --- freqtrade/exchange/ftx.py | 12 +++++++++--- tests/exchange/test_ftx.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 20d643a83..73347f1eb 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -28,9 +28,13 @@ class Ftx(Exchange): def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ - Creates a stoploss market order. - Stoploss market orders is the only stoploss type supported by ftx. + Creates a stoploss order. + depending on order_types.stoploss configuration, uses 'market' or limit order. + + Limit orders are defined by having orderPrice set, otherwise a market order is used. """ + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + limit_rate = stop_price * limit_price_pct ordertype = "stop" @@ -43,9 +47,11 @@ class Ftx(Exchange): try: params = self._params.copy() + if order_types.get('stoploss', 'market') == 'limit': + # set orderPrice to place limit order, otherwise it's a market order + params['orderPrice'] = limit_rate amount = self.amount_to_precision(pair, amount) - # set orderPrice to place limit order (?) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=stop_price, params=params) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 2b75e5324..bead63096 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -34,6 +34,14 @@ def test_stoploss_order_ftx(default_conf, mocker): # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 190 + assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_count == 1 api_mock.create_order.reset_mock() @@ -48,6 +56,22 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + + api_mock.create_order.reset_mock() + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={'stoploss': 'limit'}) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 # test exception handling with pytest.raises(DependencyException): From 0dc1a8e0379852c01289e851cb36a4c9f13b6327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jun 2020 19:40:30 +0200 Subject: [PATCH 20/76] Add profit sum to api response --- freqtrade/rpc/rpc.py | 20 +++++++++++++++----- freqtrade/rpc/telegram.py | 13 +++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 91b35e9ad..d3fef7041 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -283,8 +283,9 @@ class RPC: # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) - profit_closed_percent = (round(mean(profit_closed_ratio) * 100, 2) if profit_closed_ratio - else 0.0) + profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0 + profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 + profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_coin_sum, stake_currency, @@ -292,7 +293,8 @@ class RPC: ) if self._fiat_converter else 0 profit_all_coin_sum = round(sum(profit_all_coin), 8) - profit_all_percent = round(mean(profit_all_ratio) * 100, 2) if profit_all_ratio else 0.0 + profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0 + profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, @@ -304,10 +306,18 @@ class RPC: num = float(len(durations) or 1) return { 'profit_closed_coin': profit_closed_coin_sum, - 'profit_closed_percent': profit_closed_percent, + 'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED + 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), + 'profit_closed_ratio_mean': profit_closed_ratio_mean, + 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), + 'profit_closed_ratio_sum': profit_closed_ratio_sum, 'profit_closed_fiat': profit_closed_fiat, 'profit_all_coin': profit_all_coin_sum, - 'profit_all_percent': profit_all_percent, + 'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED + 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), + 'profit_all_ratio_mean': profit_all_ratio_mean, + 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), + 'profit_all_ratio_sum': profit_all_ratio_sum, 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), 'closed_trade_count': len([t for t in trades if not t.is_open]), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8fa973d73..02c580ebd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -315,10 +315,12 @@ class Telegram(RPC): stake_cur, fiat_disp_cur) profit_closed_coin = stats['profit_closed_coin'] - profit_closed_percent = stats['profit_closed_percent'] + profit_closed_percent_mean = stats['profit_closed_percent_mean'] + profit_closed_percent_sum = stats['profit_closed_percent_sum'] profit_closed_fiat = stats['profit_closed_fiat'] profit_all_coin = stats['profit_all_coin'] - profit_all_percent = stats['profit_all_percent'] + profit_all_percent_mean = stats['profit_all_percent_mean'] + profit_all_percent_sum = stats['profit_all_percent_sum'] profit_all_fiat = stats['profit_all_fiat'] trade_count = stats['trade_count'] first_trade_date = stats['first_trade_date'] @@ -333,13 +335,16 @@ class Telegram(RPC): if stats['closed_trade_count'] > 0: markdown_msg = ("*ROI:* Closed trades\n" f"∙ `{profit_closed_coin:.8f} {stake_cur} " - f"({profit_closed_percent:.2f}%)`\n" + f"({profit_closed_percent_mean:.2f}%) " + f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") else: markdown_msg = "`No closed trade` \n" markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" + f"∙ `{profit_all_coin:.8f} {stake_cur} " + f"({profit_all_percent_mean:.2f}%) " + f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" From 6997524a04f27334bf383b3e07a6267b4f8d6923 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jun 2020 19:40:49 +0200 Subject: [PATCH 21/76] Fix tests for additional info --- tests/rpc/test_rpc_apiserver.py | 8 ++++++++ tests/rpc/test_rpc_telegram.py | 9 ++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index cacf49f62..70a06557f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -428,9 +428,17 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'profit_all_coin': 6.217e-05, 'profit_all_fiat': 0, 'profit_all_percent': 6.2, + 'profit_all_percent_mean': 6.2, + 'profit_all_ratio_mean': 0.06201058, + 'profit_all_percent_sum': 6.2, + 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, 'profit_closed_fiat': 0, 'profit_closed_percent': 6.2, + 'profit_closed_ratio_mean': 0.06201058, + 'profit_closed_percent_mean': 6.2, + 'profit_closed_ratio_sum': 0.06201058, + 'profit_closed_percent_sum': 6.2, 'trade_count': 1, 'closed_trade_count': 1, } diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f81127c4c..4895d67e4 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -434,7 +434,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert msg_mock.call_count == 1 assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] - assert '∙ `-0.00000500 BTC (-0.50%)`' in msg_mock.call_args_list[-1][0][0] + assert ('∙ `-0.00000500 BTC (-0.50%) (-0.5 \N{GREEK CAPITAL LETTER SIGMA}%)`' + in msg_mock.call_args_list[-1][0][0]) msg_mock.reset_mock() # Update the ticker with a market going up @@ -447,10 +448,12 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0] - assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] + assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`' + in msg_mock.call_args_list[-1][0][0]) assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] - assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] + assert ('∙ `0.00006217 BTC (6.20%) (6.2 \N{GREEK CAPITAL LETTER SIGMA}%)`' + in msg_mock.call_args_list[-1][0][0]) assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] From 2f07d21629b8aade68a5916303652133bf57d3cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jun 2020 20:20:39 +0200 Subject: [PATCH 22/76] Update documentation with FTX Stoploss on exchange --- docs/configuration.md | 12 ++++++++---- docs/exchanges.md | 5 +++++ docs/stoploss.md | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index da0f015e8..467190463 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -272,7 +272,7 @@ the static list of pairs) if we should buy. ### Understand order_types -The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. +The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoplosses using using market orders. It also allows to set the @@ -288,8 +288,12 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails. The below is the default which is used if this is not configured in either strategy or configuration file. -Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. -`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). +Not all Exchanges support `stoploss_on_exchange`. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. + +If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. +`stoploss` defines the stop-price - and limit should be slightly below this. + +This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). Calculation example: we bought the asset at 100$. Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. @@ -331,7 +335,7 @@ Configuration: refer to [the stoploss documentation](stoploss.md). !!! Note - If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. + If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. diff --git a/docs/exchanges.md b/docs/exchanges.md index 81f017023..cd210eb69 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -64,6 +64,11 @@ print(res) ## FTX +!!! Tip "Stoploss on Exchange" + FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide. + + ### Using subaccounts To use subaccounts with FTX, you need to edit the configuration and add the following: diff --git a/docs/stoploss.md b/docs/stoploss.md index 0e43817ec..cd90a71b4 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. ## Static Stop Loss From 5c5dc6fffe67decf8df285cfc28733c0bd07567a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jun 2020 06:56:30 +0200 Subject: [PATCH 23/76] Update test to reflect real trade after one cycle --- tests/rpc/test_rpc_apiserver.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index cacf49f62..8743ec7ba 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -495,6 +495,10 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert rc.json == [] ftbot.enter_positions() + trades = Trade.get_open_trades() + trades[0].open_order_id = None + ftbot.exit_positions(trades) + rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 @@ -509,25 +513,26 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'close_rate': None, 'current_profit': -0.00408133, 'current_profit_pct': -0.41, + 'current_profit_abs': -4.09e-06, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', 'open_timestamp': ANY, - 'open_order': '(limit buy rem=0.00000000)', + 'open_order': None, 'open_rate': 1.098e-05, 'pair': 'ETH/BTC', 'stake_amount': 0.001, - 'stop_loss': 0.0, - 'stop_loss_abs': 0.0, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, + 'stop_loss': 9.882e-06, + 'stop_loss_abs': 9.882e-06, + 'stop_loss_pct': -10.0, + 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': 0.0, - 'initial_stop_loss_abs': 0.0, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, + 'stoploss_last_update': ANY, + 'stoploss_last_update_timestamp': ANY, + 'initial_stop_loss': 9.882e-06, + 'initial_stop_loss_abs': 9.882e-06, + 'initial_stop_loss_pct': -10.0, + 'initial_stop_loss_ratio': -0.1, 'trade_id': 1, 'close_rate_requested': None, 'current_rate': 1.099e-05, @@ -539,9 +544,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'fee_open_currency': None, 'open_date': ANY, 'is_open': True, - 'max_rate': 0.0, - 'min_rate': None, - 'open_order_id': ANY, + 'max_rate': 1.099e-05, + 'min_rate': 1.098e-05, + 'open_order_id': None, 'open_rate_requested': 1.098e-05, 'open_trade_price': 0.0010025, 'sell_reason': None, From 412b50dac5471e5392395c771298fbb118dcc8bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jun 2020 06:56:59 +0200 Subject: [PATCH 24/76] Add current stoploss calculations --- freqtrade/rpc/rpc.py | 13 +++++++++++++ tests/rpc/test_rpc_apiserver.py | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 91b35e9ad..dd6b084a2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -130,6 +130,14 @@ class RPC: except DependencyException: current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) + current_profit_abs = trade.calc_profit(current_rate) + # Calculate guaranteed profit (in case of trailing stop) + stoploss_entrypoint_dist = trade.open_rate - trade.stop_loss + stoploss_entrypoint_dist_ratio = stoploss_entrypoint_dist / trade.stop_loss + # calculate distance to stoploss + stoploss_current_dist = trade.stop_loss - current_rate + stoploss_current_dist_ratio = stoploss_current_dist / current_rate + fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit is not None else None) trade_dict = trade.to_json() @@ -140,6 +148,11 @@ class RPC: current_rate=current_rate, current_profit=current_profit, current_profit_pct=round(current_profit * 100, 2), + current_profit_abs=current_profit_abs, + stoploss_current_dist=stoploss_current_dist, + stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), + stoploss_entrypoint_dist=stoploss_entrypoint_dist, + stoploss_entrypoint_dist_ratio=round(stoploss_entrypoint_dist_ratio, 8), open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8743ec7ba..906e9f2fe 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -533,6 +533,10 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, + 'stoploss_current_dist': -1.1080000000000002e-06, + 'stoploss_current_dist_ratio': -0.10081893, + 'stoploss_entrypoint_dist': 1.0980000000000003e-06, + 'stoploss_entrypoint_dist_ratio': 0.11111111, 'trade_id': 1, 'close_rate_requested': None, 'current_rate': 1.099e-05, From 7bd55aa2f1c6c60c6b50ffd7d8965146c633f4e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jun 2020 07:04:32 +0200 Subject: [PATCH 25/76] Use correct calcuation for "locked in profit" --- freqtrade/rpc/rpc.py | 8 ++++---- tests/rpc/test_rpc_apiserver.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dd6b084a2..fc774ded2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -132,8 +132,8 @@ class RPC: current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) # Calculate guaranteed profit (in case of trailing stop) - stoploss_entrypoint_dist = trade.open_rate - trade.stop_loss - stoploss_entrypoint_dist_ratio = stoploss_entrypoint_dist / trade.stop_loss + stoploss_entry_dist = trade.calc_profit(trade.stop_loss) + stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss) # calculate distance to stoploss stoploss_current_dist = trade.stop_loss - current_rate stoploss_current_dist_ratio = stoploss_current_dist / current_rate @@ -151,8 +151,8 @@ class RPC: current_profit_abs=current_profit_abs, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), - stoploss_entrypoint_dist=stoploss_entrypoint_dist, - stoploss_entrypoint_dist_ratio=round(stoploss_entrypoint_dist_ratio, 8), + stoploss_entry_dist=stoploss_entry_dist, + stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8), open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 906e9f2fe..3555057a9 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -535,8 +535,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': -1.1080000000000002e-06, 'stoploss_current_dist_ratio': -0.10081893, - 'stoploss_entrypoint_dist': 1.0980000000000003e-06, - 'stoploss_entrypoint_dist_ratio': 0.11111111, + 'stoploss_entry_dist': -0.00010475, + 'stoploss_entry_dist_ratio': -0.10448878, 'trade_id': 1, 'close_rate_requested': None, 'current_rate': 1.099e-05, From 6a88eb603bde0eea17aa18bd318e50345b70baed Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jun 2020 07:20:50 +0200 Subject: [PATCH 26/76] Update failing test --- tests/rpc/test_rpc.py | 73 +++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1de73ada9..ef1c1bc16 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -42,8 +42,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: rpc._rpc_trade_status() freqtradebot.enter_positions() + trades = Trade.get_open_trades() + trades[0].open_order_id = None + freqtradebot.exit_positions(trades) + results = rpc._rpc_trade_status() - assert { + assert results[0] == { 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', @@ -54,11 +58,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'fee_open': ANY, 'fee_open_cost': ANY, 'fee_open_currency': ANY, - 'fee_close': ANY, + 'fee_close': fee.return_value, 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, - 'open_trade_price': ANY, + 'open_trade_price': 0.0010025, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, @@ -80,28 +84,32 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_profit_abs': None, 'current_profit': -0.00408133, 'current_profit_pct': -0.41, - 'stop_loss': 0.0, - 'stop_loss_abs': 0.0, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, + 'current_profit_abs': -4.09e-06, + 'stop_loss': 9.882e-06, + 'stop_loss_abs': 9.882e-06, + 'stop_loss_pct': -10.0, + 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': 0.0, - 'initial_stop_loss_abs': 0.0, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'open_order': '(limit buy rem=0.00000000)', + 'stoploss_last_update': ANY, + 'stoploss_last_update_timestamp': ANY, + 'initial_stop_loss': 9.882e-06, + 'initial_stop_loss_abs': 9.882e-06, + 'initial_stop_loss_pct': -10.0, + 'initial_stop_loss_ratio': -0.1, + 'stoploss_current_dist': -1.1080000000000002e-06, + 'stoploss_current_dist_ratio': -0.10081893, + 'stoploss_entry_dist': -0.00010475, + 'stoploss_entry_dist_ratio': -0.10448878, + 'open_order': None, 'exchange': 'bittrex', - - } == results[0] + } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_rate']) - assert { + assert results[0] == { 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', @@ -112,7 +120,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'fee_open': ANY, 'fee_open_cost': ANY, 'fee_open_currency': ANY, - 'fee_close': ANY, + 'fee_close': fee.return_value, 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, @@ -138,20 +146,25 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_profit_abs': None, 'current_profit': ANY, 'current_profit_pct': ANY, - 'stop_loss': 0.0, - 'stop_loss_abs': 0.0, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, + 'current_profit_abs': ANY, + 'stop_loss': 9.882e-06, + 'stop_loss_abs': 9.882e-06, + 'stop_loss_pct': -10.0, + 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': 0.0, - 'initial_stop_loss_abs': 0.0, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'open_order': '(limit buy rem=0.00000000)', + 'stoploss_last_update': ANY, + 'stoploss_last_update_timestamp': ANY, + 'initial_stop_loss': 9.882e-06, + 'initial_stop_loss_abs': 9.882e-06, + 'initial_stop_loss_pct': -10.0, + 'initial_stop_loss_ratio': -0.1, + 'stoploss_current_dist': ANY, + 'stoploss_current_dist_ratio': ANY, + 'stoploss_entry_dist': -0.00010475, + 'stoploss_entry_dist_ratio': -0.10448878, + 'open_order': None, 'exchange': 'bittrex', - } == results[0] + } def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: From b27348490d3d6883701c0f9dafe9ab0885f29034 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jun 2020 13:59:00 +0200 Subject: [PATCH 27/76] Add rate-limiting note for Kraken datadownload --- docs/exchanges.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 81f017023..075329a07 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -30,6 +30,15 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting. To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data. +Due to the heavy rate-limiting applied by Kraken, the follwoing configuration section should be used to download data: + +``` json + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 3100 + }, +``` + ## Bittrex ### Order types From ff289a71776239e2ac0dd35e2fb40cfa44a83e83 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Jun 2020 19:08:54 +0200 Subject: [PATCH 28/76] Updated tests to work with Telegram emojis --- tests/rpc/test_rpc_telegram.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f81127c4c..2cecc35d1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1221,8 +1221,9 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) + '🔵'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ - == '*Bittrex:* Buying ETH/BTC\n' \ + == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1243,8 +1244,9 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'exchange': 'Bittrex', 'pair': 'ETH/BTC', }) + '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ - == ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC') + == ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC') def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1276,8 +1278,9 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-1), 'close_date': arrow.utcnow(), }) + '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ - == ('*Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1305,7 +1308,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Selling KEY/ETH\n' + == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' @@ -1334,8 +1337,9 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'pair': 'KEY/ETH', 'reason': 'Cancelled on exchange' }) + '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ - == ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange') msg_mock.reset_mock() telegram.send_msg({ @@ -1345,7 +1349,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('*Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') # Reset singleton function to avoid random breaks telegram._fiat_converter.convert_amount = old_convamount @@ -1379,7 +1383,8 @@ def test_warning_notification(default_conf, mocker) -> None: 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' }) - assert msg_mock.call_args[0][0] == '*Warning:* `message`' + '⚠'.encode('ascii', 'namereplace') + assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' def test_custom_notification(default_conf, mocker) -> None: @@ -1437,8 +1442,9 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) + '🔵'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ - == '*Bittrex:* Buying ETH/BTC\n' \ + == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1473,8 +1479,9 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) + '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ - == '*Binance:* Selling KEY/ETH\n' \ + == '\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ From 080efd1102835367f135827c9a7dc4be1623be84 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Jun 2020 19:09:49 +0200 Subject: [PATCH 29/76] Added unicoded emoji's to Telegram messages --- freqtrade/rpc/telegram.py | 61 +++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8fa973d73..e496d3fa6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -19,7 +19,6 @@ logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') - MAX_TELEGRAM_MESSAGE_LENGTH = 4096 @@ -29,6 +28,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: :param command_handler: Telegram CommandHandler :return: decorated function """ + def wrapper(self, *args, **kwargs): """ Decorator logic """ update = kwargs.get('update') or args[0] @@ -126,6 +126,12 @@ class Telegram(RPC): def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ + '🔵'.encode('ascii', 'namereplace') + '🚀'.encode('ascii', 'namereplace') + '✳'.encode('ascii', 'namereplace') + '❌'.encode('ascii', 'namereplace') + '⚠'.encode('ascii', 'namereplace') + if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( @@ -133,7 +139,7 @@ class Telegram(RPC): else: msg['stake_amount_fiat'] = 0 - message = ("*{exchange}:* Buying {pair}\n" + message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{limit:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" @@ -144,7 +150,7 @@ class Telegram(RPC): message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: - message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg) + message = "\N{WARNING SIGN} *{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) @@ -153,33 +159,44 @@ class Telegram(RPC): microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 - message = ("*{exchange}:* Selling {pair}\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) + if float(msg['profit_percent']) >= 5.0: + message = ("\N{ROCKET} *{exchange}:* Selling {pair}\n").format(**msg) + + elif float(msg['profit_percent']) >= 0.0: + message = "\N{EIGHT SPOKED ASTERISK} *{exchange}:* Selling {pair}\n" + + elif msg['sell_reason'] == "stop_loss": + message = ("\N{WARNING SIGN} *{exchange}:* Selling {pair}\n").format(**msg) + + else: + message = ("\N{CROSS MARK} *{exchange}:* Selling {pair}\n").format(**msg) + + message += ("*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._fiat_converter): + and self._fiat_converter): msg['profit_fiat'] = self._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) message += (' `({gain}: {profit_amount:.8f} {stake_currency}' ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: - message = ("*{exchange}:* Cancelling Open Sell Order " + message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair}. Reason: {reason}").format(**msg) elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: - message = '*Warning:* `{status}`'.format(**msg) + message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: message = '{status}'.format(**msg) @@ -222,8 +239,8 @@ class Telegram(RPC): # Adding initial stoploss only if it is different from stoploss "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + ("`({initial_stop_loss_pct:.2f}%)`") if ( - r['stop_loss'] != r['initial_stop_loss'] - and r['initial_stop_loss_pct'] is not None) else "", + r['stop_loss'] != r['initial_stop_loss'] + and r['initial_stop_loss_pct'] is not None) else "", # Adding stoploss and stoploss percentage only if it is not None "*Stoploss:* `{stop_loss:.8f}` " + @@ -363,14 +380,14 @@ class Telegram(RPC): "This mode is still experimental!\n" "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" - ) + ) for currency in result['currencies']: if currency['est_stake'] > 0.0001: curr_output = "*{currency}:*\n" \ - "\t`Available: {free: .8f}`\n" \ - "\t`Balance: {balance: .8f}`\n" \ - "\t`Pending: {used: .8f}`\n" \ - "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency) + "\t`Available: {free: .8f}`\n" \ + "\t`Balance: {balance: .8f}`\n" \ + "\t`Pending: {used: .8f}`\n" \ + "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency) else: curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) @@ -587,7 +604,7 @@ class Telegram(RPC): "*/profit:* `Lists cumulative profit from all finished trades`\n" \ "*/forcesell |all:* `Instantly sells the given trade or all trades, " \ "regardless of profit`\n" \ - f"{forcebuy_text if self._config.get('forcebuy_enable', False) else '' }" \ + f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" \ "*/performance:* `Show performance of each finished trade grouped by pair`\n" \ "*/daily :* `Shows profit or loss per day, over the last n days`\n" \ "*/count:* `Show number of trades running compared to allowed number of trades`" \ From 4c6a7a354dfae8d0a60f8243aefa91f400a01604 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:04:11 +0200 Subject: [PATCH 30/76] Removed '.encode' lines, unessecary --- freqtrade/rpc/telegram.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e496d3fa6..6b1450472 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -126,12 +126,6 @@ class Telegram(RPC): def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ - '🔵'.encode('ascii', 'namereplace') - '🚀'.encode('ascii', 'namereplace') - '✳'.encode('ascii', 'namereplace') - '❌'.encode('ascii', 'namereplace') - '⚠'.encode('ascii', 'namereplace') - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( From 08b9abed3aeb722357ced69fe67489acc05e3eaa Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:05:55 +0200 Subject: [PATCH 31/76] Removed '.encode', unecessary --- tests/rpc/test_rpc_telegram.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2cecc35d1..129bdf9d1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1221,7 +1221,6 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - '🔵'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ @@ -1244,7 +1243,6 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'exchange': 'Bittrex', 'pair': 'ETH/BTC', }) - '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC') @@ -1278,7 +1276,6 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-1), 'close_date': arrow.utcnow(), }) - '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' '*Amount:* `1333.33333333`\n' @@ -1337,7 +1334,6 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'pair': 'KEY/ETH', 'reason': 'Cancelled on exchange' }) - '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange') @@ -1383,7 +1379,6 @@ def test_warning_notification(default_conf, mocker) -> None: 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' }) - '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' @@ -1442,7 +1437,6 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - '🔵'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ @@ -1479,7 +1473,6 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) - '⚠'.encode('ascii', 'namereplace') assert msg_mock.call_args[0][0] \ == '\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' \ '*Amount:* `1333.33333333`\n' \ From 6694ac50779619eec4f761a3f28175833d4080c9 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:10:52 +0200 Subject: [PATCH 32/76] Splitted a line that was too long, resulting in flake8 error --- tests/rpc/test_rpc_telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 129bdf9d1..15fe0eaf9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1335,7 +1335,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. ' + 'Reason: Cancelled on exchange') msg_mock.reset_mock() telegram.send_msg({ From f34bcc5fd3b629acfaeb55cd51854b1e719f44f8 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Fri, 5 Jun 2020 20:15:22 +0200 Subject: [PATCH 33/76] Splitted a line that was too long, resulting in error for flake8 --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6b1450472..7dbd9cd83 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -144,7 +144,8 @@ class Telegram(RPC): message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: - message = "\N{WARNING SIGN} *{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg) + message = "\N{WARNING SIGN} *{exchange}:* " \ + "Cancelling Open Buy Order for {pair}".format(**msg) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) From 5f9994c9ed1d7749412bf8201932f44e4e4b4c69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jun 2020 20:24:21 +0200 Subject: [PATCH 34/76] Reduce verbosity of sell-rate fetching --- freqtrade/freqtradebot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a74b0a5a1..5104e4f95 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -702,11 +702,10 @@ class FreqtradeBot: self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) if config_ask_strategy.get('use_order_book', False): - # logger.debug('Order book %s',orderBook) order_book_min = config_ask_strategy.get('order_book_min', 1) order_book_max = config_ask_strategy.get('order_book_max', 1) - logger.info(f'Using order book between {order_book_min} and {order_book_max} ' - f'for selling {trade.pair}...') + logger.debug(f'Using order book between {order_book_min} and {order_book_max} ' + f'for selling {trade.pair}...') order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s", order_book_min=order_book_min, From 8c32d691c7ee4cee9ff860748b3ebee2c1187388 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 5 Jun 2020 20:31:40 +0200 Subject: [PATCH 35/76] Add information about bid and ask strategy to /showconfig --- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 4 +++- tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 91b35e9ad..a5005f35e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -105,6 +105,8 @@ class RPC: 'exchange': config['exchange']['name'], 'strategy': config['strategy'], 'forcebuy_enabled': config.get('forcebuy_enable', False), + 'ask_strategy': config.get('ask_strategy', {}), + 'bid_strategy': config.get('bid_strategy', {}), 'state': str(self._freqtrade.state) } return val diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8fa973d73..09a99ad85 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -636,13 +636,15 @@ class Telegram(RPC): else: sl_info = f"*Stoploss:* `{val['stoploss']}`\n" - + import json self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Max open Trades:* `{val['max_open_trades']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" + f"*Ask strategy:* ```\n{val['ask_strategy']}```\n" + f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n" f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" f"*Strategy:* `{val['strategy']}`\n" diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index cacf49f62..6a850ecc5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -325,6 +325,8 @@ def test_api_show_config(botclient, mocker): assert rc.json['ticker_interval'] == '5m' assert rc.json['state'] == 'running' assert not rc.json['trailing_stop'] + assert 'bid_strategy' in rc.json + assert 'ask_strategy' in rc.json def test_api_daily(botclient, mocker, ticker, fee, markets): From 1a5cd85900f232ee24022dd82b9ebb2732b88f4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jun 2020 13:15:01 +0200 Subject: [PATCH 36/76] Fix typo Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 075329a07..aef356ea9 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -30,7 +30,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting. To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data. -Due to the heavy rate-limiting applied by Kraken, the follwoing configuration section should be used to download data: +Due to the heavy rate-limiting applied by Kraken, the following configuration section should be used to download data: ``` json "ccxt_async_config": { From b2316cdd00fd1f946a8bd07cb55e39e0a8720859 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jun 2020 13:32:06 +0200 Subject: [PATCH 37/76] Extract sell_smoij logic into it's own function --- freqtrade/rpc/telegram.py | 41 +++++++++++++++++++--------------- tests/rpc/test_rpc_telegram.py | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7dbd9cd83..0dc57e5b9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -154,25 +154,16 @@ class Telegram(RPC): microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 - if float(msg['profit_percent']) >= 5.0: - message = ("\N{ROCKET} *{exchange}:* Selling {pair}\n").format(**msg) + msg['emoij'] = self._get_sell_emoij(msg) - elif float(msg['profit_percent']) >= 0.0: - message = "\N{EIGHT SPOKED ASTERISK} *{exchange}:* Selling {pair}\n" - - elif msg['sell_reason'] == "stop_loss": - message = ("\N{WARNING SIGN} *{exchange}:* Selling {pair}\n").format(**msg) - - else: - message = ("\N{CROSS MARK} *{exchange}:* Selling {pair}\n").format(**msg) - - message += ("*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) + message = ("{emoij} *{exchange}:* Selling {pair}\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. # This might not be the case if the message origin is triggered by /forcesell @@ -201,6 +192,20 @@ class Telegram(RPC): self._send_msg(message) + def _get_sell_emoij(self, msg): + """ + Get emoji for sell-side + """ + + if float(msg['profit_percent']) >= 5.0: + return "\N{ROCKET}" + elif float(msg['profit_percent']) >= 0.0: + return "\N{EIGHT SPOKED ASTERISK}" + elif msg['sell_reason'] == "stop_loss": + return"\N{WARNING SIGN}" + else: + return "\N{CROSS MARK}" + @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 15fe0eaf9..7fea19fa9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1485,6 +1485,29 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Profit:* `-57.41%`' +@pytest.mark.parametrize('msg,expected', [ + ({'profit_percent': 20.1, 'sell_reason': 'roi'}, "\N{ROCKET}"), + ({'profit_percent': 5.1, 'sell_reason': 'roi'}, "\N{ROCKET}"), + ({'profit_percent': 2.56, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_percent': 1.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_percent': 0.0, 'sell_reason': 'roi'}, "\N{EIGHT SPOKED ASTERISK}"), + ({'profit_percent': -5.0, 'sell_reason': 'stop_loss'}, "\N{WARNING SIGN}"), + ({'profit_percent': -2.0, 'sell_reason': 'sell_signal'}, "\N{CROSS MARK}"), +]) +def test__sell_emoji(default_conf, mocker, msg, expected): + del default_conf['fiat_display_currency'] + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + + assert telegram._get_sell_emoij(msg) == expected + + def test__send_msg(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() From 172ca761f2c472bc405e2933f0c4b0b81612e668 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:38:42 +0200 Subject: [PATCH 38/76] Fixed typo 'emoij' --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0dc57e5b9..09be7793e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -154,9 +154,9 @@ class Telegram(RPC): microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 - msg['emoij'] = self._get_sell_emoij(msg) + msg['emoji'] = self._get_sell_emoji(msg) - message = ("{emoij} *{exchange}:* Selling {pair}\n" + message = ("{emoji} *{exchange}:* Selling {pair}\n" "*Amount:* `{amount:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" @@ -192,7 +192,7 @@ class Telegram(RPC): self._send_msg(message) - def _get_sell_emoij(self, msg): + def _get_sell_emoji(self, msg): """ Get emoji for sell-side """ From d20762aa01c9f3e2ab61dca97d260737e93c6c4c Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:46:19 +0200 Subject: [PATCH 39/76] Fixed typo 'emoij' in test file too --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7fea19fa9..423f27441 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1505,7 +1505,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) - assert telegram._get_sell_emoij(msg) == expected + assert telegram._get_sell_emoji(msg) == expected def test__send_msg(default_conf, mocker) -> None: From 3bd38171f80c7b420d259f573861a7fed8a6d031 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jun 2020 17:19:44 +0200 Subject: [PATCH 40/76] DOn't use json.dumps - it's not necessary --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 09a99ad85..2ea697ed4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -636,7 +636,7 @@ class Telegram(RPC): else: sl_info = f"*Stoploss:* `{val['stoploss']}`\n" - import json + self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" @@ -644,7 +644,7 @@ class Telegram(RPC): f"*Max open Trades:* `{val['max_open_trades']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" f"*Ask strategy:* ```\n{val['ask_strategy']}```\n" - f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n" + f"*Bid strategy:* ```\n{val['bid_strategy']}```\n" f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" f"*Strategy:* `{val['strategy']}`\n" From 8d8cf5a2fde3ea20f66991b1eb978e84d514e9d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Jun 2020 17:28:00 +0200 Subject: [PATCH 41/76] Improve code formatting of telegram --- freqtrade/rpc/telegram.py | 72 +++++++++++++++++----------------- tests/rpc/test_rpc_telegram.py | 41 +++++++++---------- 2 files changed, 55 insertions(+), 58 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 965318f64..894955eb5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -144,8 +144,8 @@ class Telegram(RPC): message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: - message = "\N{WARNING SIGN} *{exchange}:* " \ - "Cancelling Open Buy Order for {pair}".format(**msg) + message = ("\N{WARNING SIGN} *{exchange}:* " + "Cancelling Open Buy Order for {pair}".format(**msg)) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) @@ -388,11 +388,11 @@ class Telegram(RPC): ) for currency in result['currencies']: if currency['est_stake'] > 0.0001: - curr_output = "*{currency}:*\n" \ - "\t`Available: {free: .8f}`\n" \ - "\t`Balance: {balance: .8f}`\n" \ - "\t`Pending: {used: .8f}`\n" \ - "\t`Est. {stake}: {est_stake: .8f}`\n".format(**currency) + curr_output = ("*{currency}:*\n" + "\t`Available: {free: .8f}`\n" + "\t`Balance: {balance: .8f}`\n" + "\t`Pending: {used: .8f}`\n" + "\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency) else: curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) @@ -403,9 +403,9 @@ class Telegram(RPC): else: output += curr_output - output += "\n*Estimated Value*:\n" \ - "\t`{stake}: {total: .8f}`\n" \ - "\t`{symbol}: {value: .2f}`\n".format(**result) + output += ("\n*Estimated Value*:\n" + "\t`{stake}: {total: .8f}`\n" + "\t`{symbol}: {value: .2f}`\n").format(**result) self._send_msg(output) except RPCException as e: self._send_msg(str(e)) @@ -598,32 +598,32 @@ class Telegram(RPC): :param update: message update :return: None """ - forcebuy_text = "*/forcebuy []:* `Instantly buys the given pair. " \ - "Optionally takes a rate at which to buy.` \n" - message = "*/start:* `Starts the trader`\n" \ - "*/stop:* `Stops the trader`\n" \ - "*/status [table]:* `Lists all open trades`\n" \ - " *table :* `will display trades in a table`\n" \ - " `pending buy orders are marked with an asterisk (*)`\n" \ - " `pending sell orders are marked with a double asterisk (**)`\n" \ - "*/profit:* `Lists cumulative profit from all finished trades`\n" \ - "*/forcesell |all:* `Instantly sells the given trade or all trades, " \ - "regardless of profit`\n" \ - f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" \ - "*/performance:* `Show performance of each finished trade grouped by pair`\n" \ - "*/daily :* `Shows profit or loss per day, over the last n days`\n" \ - "*/count:* `Show number of trades running compared to allowed number of trades`" \ - "\n" \ - "*/balance:* `Show account balance per currency`\n" \ - "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \ - "*/reload_conf:* `Reload configuration file` \n" \ - "*/show_config:* `Show running configuration` \n" \ - "*/whitelist:* `Show current whitelist` \n" \ - "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \ - "to the blacklist.` \n" \ - "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" \ - "*/help:* `This help message`\n" \ - "*/version:* `Show version`" + forcebuy_text = ("*/forcebuy []:* `Instantly buys the given pair. " + "Optionally takes a rate at which to buy.` \n") + message = ("*/start:* `Starts the trader`\n" + "*/stop:* `Stops the trader`\n" + "*/status [table]:* `Lists all open trades`\n" + " *table :* `will display trades in a table`\n" + " `pending buy orders are marked with an asterisk (*)`\n" + " `pending sell orders are marked with a double asterisk (**)`\n" + "*/profit:* `Lists cumulative profit from all finished trades`\n" + "*/forcesell |all:* `Instantly sells the given trade or all trades, " + "regardless of profit`\n" + f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" + "*/performance:* `Show performance of each finished trade grouped by pair`\n" + "*/daily :* `Shows profit or loss per day, over the last n days`\n" + "*/count:* `Show number of trades running compared to allowed number of trades`" + "\n" + "*/balance:* `Show account balance per currency`\n" + "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" + "*/reload_conf:* `Reload configuration file` \n" + "*/show_config:* `Show running configuration` \n" + "*/whitelist:* `Show current whitelist` \n" + "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " + "to the blacklist.` \n" + "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" + "*/help:* `This help message`\n" + "*/version:* `Show version`") self._send_msg(message) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21b9df81c..b18106ee5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -71,10 +71,10 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.dispatcher.add_handler.call_count > 0 assert start_polling.start_polling.call_count == 1 - message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ - "['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " \ - "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" + message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " + "['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " + "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) @@ -1016,9 +1016,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() telegram._count(update=update, context=MagicMock()) - msg = '
  current    max    total stake\n---------  -----  -------------\n' \
-          '        1      {}          {}
'\ - .format( + msg = ('
  current    max    total stake\n---------  -----  -------------\n'
+           '        1      {}          {}
').format( default_conf['max_open_trades'], default_conf['stake_amount'] ) @@ -1441,12 +1440,11 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ - '*Amount:* `1333.33333333`\n' \ - '*Open Rate:* `0.00001099`\n' \ - '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.001000 BTC)`' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00001099`\n' + '*Current Rate:* `0.00001099`\n' + '*Total:* `(0.001000 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: @@ -1477,15 +1475,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) - assert msg_mock.call_args[0][0] \ - == '\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' \ - '*Amount:* `1333.33333333`\n' \ - '*Open Rate:* `0.00007500`\n' \ - '*Current Rate:* `0.00003201`\n' \ - '*Close Rate:* `0.00003201`\n' \ - '*Sell Reason:* `stop_loss`\n' \ - '*Duration:* `2:35:03 (155.1 min)`\n' \ - '*Profit:* `-57.41%`' + assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Current Rate:* `0.00003201`\n' + '*Close Rate:* `0.00003201`\n' + '*Sell Reason:* `stop_loss`\n' + '*Duration:* `2:35:03 (155.1 min)`\n' + '*Profit:* `-57.41%`') @pytest.mark.parametrize('msg,expected', [ From db4576c50b97e89c524b2de69998e1431d963204 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 10:09:39 +0200 Subject: [PATCH 42/76] Use json for *strategy dump --- freqtrade/rpc/telegram.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 894955eb5..eb53fc68f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +import json import logging from typing import Any, Callable, Dict @@ -665,8 +666,8 @@ class Telegram(RPC): f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Max open Trades:* `{val['max_open_trades']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" - f"*Ask strategy:* ```\n{val['ask_strategy']}```\n" - f"*Bid strategy:* ```\n{val['bid_strategy']}```\n" + f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n" + f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n" f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" f"*Strategy:* `{val['strategy']}`\n" From a6f67247520a9fb52271852cefc80dd05716977d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 11:29:14 +0200 Subject: [PATCH 43/76] Reorder functions in optimize_report --- freqtrade/optimize/optimize_reports.py | 136 +++++++++++++------------ 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c148f0f44..d70a80bb3 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -113,25 +113,6 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t return tabular_data -def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: - """ - Generates and returns a text table for the given backtest data and the results dataframe - :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row - :param stake_currency: stake-currency - used to correctly name headers - :return: pretty printed table with tabulate as string - """ - - headers = _get_line_header('Pair', stake_currency) - floatfmt = _get_line_floatfmt() - output = [[ - t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] - ] for t in pair_results] - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(output, headers=headers, - floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore - - def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results @@ -166,33 +147,6 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List return tabular_data -def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], - stake_currency: str) -> str: - """ - Generate small table outlining Backtest results - :param sell_reason_stats: Sell reason metrics - :param stake_currency: Stakecurrency used - :return: pretty printed table with tabulate as string - """ - headers = [ - 'Sell Reason', - 'Sells', - 'Wins', - 'Draws', - 'Losses', - 'Avg Profit %', - 'Cum Profit %', - f'Tot Profit {stake_currency}', - 'Tot Profit %', - ] - - output = [[ - t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], - t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'], - ] for t in sell_reason_stats] - return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") - - def generate_strategy_metrics(stake_currency: str, max_open_trades: int, all_results: Dict) -> List[Dict]: """ @@ -209,26 +163,6 @@ def generate_strategy_metrics(stake_currency: str, max_open_trades: int, return tabular_data -def generate_text_table_strategy(strategy_results, stake_currency: str) -> str: - """ - Generate summary table per strategy - :param stake_currency: stake-currency - used to correctly name headers - :param max_open_trades: Maximum allowed open trades used for backtest - :param all_results: Dict of containing results for all strategies - :return: pretty printed table with tabulate as string - """ - floatfmt = _get_line_floatfmt() - headers = _get_line_header('Strategy', stake_currency) - - output = [[ - t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] - ] for t in strategy_results] - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(output, headers=headers, - floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore - - def generate_edge_table(results: dict) -> str: floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd') @@ -288,6 +222,76 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], return result +### +# Start output section +### + +def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: + """ + Generates and returns a text table for the given backtest data and the results dataframe + :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row + :param stake_currency: stake-currency - used to correctly name headers + :return: pretty printed table with tabulate as string + """ + + headers = _get_line_header('Pair', stake_currency) + floatfmt = _get_line_floatfmt() + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + ] for t in pair_results] + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(output, headers=headers, + floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") + + +def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], + stake_currency: str) -> str: + """ + Generate small table outlining Backtest results + :param sell_reason_stats: Sell reason metrics + :param stake_currency: Stakecurrency used + :return: pretty printed table with tabulate as string + """ + headers = [ + 'Sell Reason', + 'Sells', + 'Wins', + 'Draws', + 'Losses', + 'Avg Profit %', + 'Cum Profit %', + f'Tot Profit {stake_currency}', + 'Tot Profit %', + ] + + output = [[ + t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], + t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'], + ] for t in sell_reason_stats] + return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") + + +def generate_text_table_strategy(strategy_results, stake_currency: str) -> str: + """ + Generate summary table per strategy + :param stake_currency: stake-currency - used to correctly name headers + :param max_open_trades: Maximum allowed open trades used for backtest + :param all_results: Dict of containing results for all strategies + :return: pretty printed table with tabulate as string + """ + floatfmt = _get_line_floatfmt() + headers = _get_line_header('Strategy', stake_currency) + + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] + ] for t in strategy_results] + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(output, headers=headers, + floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") + + def show_backtest_results(config: Dict, backtest_stats: Dict): stake_currency = config['stake_currency'] From 499c6772d170f30fb16a5410fd32f3c63c3dcfbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 11:31:33 +0200 Subject: [PATCH 44/76] Rename tabulate methods they don't "generate" anything --- freqtrade/optimize/optimize_reports.py | 12 +++++------- tests/optimize/test_backtesting.py | 2 +- tests/optimize/test_optimize_reports.py | 14 +++++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d70a80bb3..789ad7d46 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -245,8 +245,7 @@ def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") -def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], - stake_currency: str) -> str: +def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: """ Generate small table outlining Backtest results :param sell_reason_stats: Sell reason metrics @@ -272,7 +271,7 @@ def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def generate_text_table_strategy(strategy_results, stake_currency: str) -> str: +def text_table_strategy(strategy_results, stake_currency: str) -> str: """ Generate summary table per strategy :param stake_currency: stake-currency - used to correctly name headers @@ -304,9 +303,8 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], - stake_currency=stake_currency, - ) + table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], + stake_currency=stake_currency) if isinstance(table, str): print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) @@ -322,7 +320,7 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): if len(backtest_stats['strategy']) > 1: # Print Strategy summary table - table = generate_text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) + table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) print('=' * len(table.splitlines()[0])) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 40c106975..04c8a4b0e 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -666,7 +666,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): mocker.patch.multiple('freqtrade.optimize.optimize_reports', generate_text_table=gen_table_mock, - generate_text_table_strategy=gen_strattable_mock, + text_table_strategy=gen_strattable_mock, generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, generate_strategy_metrics=gen_strat_summary, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 8bef6e2cc..b4105af7d 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -7,8 +7,8 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( generate_pair_metrics, generate_edge_table, generate_sell_reason_stats, - generate_text_table, generate_text_table_sell_reason, generate_strategy_metrics, - generate_text_table_strategy, store_backtest_result) + generate_text_table, text_table_sell_reason, generate_strategy_metrics, + text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -69,7 +69,7 @@ def test_generate_pair_metrics(default_conf, mocker): pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100) -def test_generate_text_table_sell_reason(default_conf): +def test_text_table_sell_reason(default_conf): results = pd.DataFrame( { @@ -97,8 +97,8 @@ def test_generate_text_table_sell_reason(default_conf): sell_reason_stats = generate_sell_reason_stats(max_open_trades=2, results=results) - assert generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats, - stake_currency='BTC') == result_str + assert text_table_sell_reason(sell_reason_stats=sell_reason_stats, + stake_currency='BTC') == result_str def test_generate_sell_reason_stats(default_conf): @@ -136,7 +136,7 @@ def test_generate_sell_reason_stats(default_conf): assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) -def test_generate_text_table_strategy(default_conf, mocker): +def test_text_table_strategy(default_conf, mocker): results = {} results['TestStrategy1'] = pd.DataFrame( { @@ -178,7 +178,7 @@ def test_generate_text_table_strategy(default_conf, mocker): max_open_trades=2, all_results=results) - assert generate_text_table_strategy(strategy_results, 'BTC') == result_str + assert text_table_strategy(strategy_results, 'BTC') == result_str def test_generate_edge_table(edge_conf, mocker): From 070913f32729c053eb150db1d473c2c5c027e37d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 11:35:02 +0200 Subject: [PATCH 45/76] Rename text_table generation --- freqtrade/optimize/optimize_reports.py | 6 +++--- tests/optimize/test_backtesting.py | 18 +++++++++--------- tests/optimize/test_optimize_reports.py | 7 +++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 789ad7d46..739640bc5 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -226,7 +226,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], # Start output section ### -def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: +def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row @@ -298,7 +298,7 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): # Print results print(f"Result for strategy {strategy}") - table = generate_text_table(results['results_per_pair'], stake_currency=stake_currency) + table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency) if isinstance(table, str): print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) @@ -309,7 +309,7 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table(results['left_open_trades'], stake_currency=stake_currency) + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 04c8a4b0e..1c9810ec9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -659,17 +659,17 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) - gen_table_mock = MagicMock() + text_table_mock = MagicMock() sell_reason_mock = MagicMock() - gen_strattable_mock = MagicMock() - gen_strat_summary = MagicMock() + strattable_mock = MagicMock() + strat_summary = MagicMock() mocker.patch.multiple('freqtrade.optimize.optimize_reports', - generate_text_table=gen_table_mock, - text_table_strategy=gen_strattable_mock, + text_table_bt_results=text_table_mock, + text_table_strategy=strattable_mock, generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, - generate_strategy_metrics=gen_strat_summary, + generate_strategy_metrics=strat_summary, ) patched_configuration_load_config_file(mocker, default_conf) @@ -690,10 +690,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): start_backtesting(args) # 2 backtests, 4 tables assert backtestmock.call_count == 2 - assert gen_table_mock.call_count == 4 - assert gen_strattable_mock.call_count == 1 + assert text_table_mock.call_count == 4 + assert strattable_mock.call_count == 1 assert sell_reason_mock.call_count == 2 - assert gen_strat_summary.call_count == 1 + assert strat_summary.call_count == 1 # check the logs, that will contain the backtest result exists = [ diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b4105af7d..175405e4c 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -7,13 +7,13 @@ from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( generate_pair_metrics, generate_edge_table, generate_sell_reason_stats, - generate_text_table, text_table_sell_reason, generate_strategy_metrics, + text_table_bt_results, text_table_sell_reason, generate_strategy_metrics, text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange -def test_generate_text_table(default_conf, mocker): +def test_text_table_bt_results(default_conf, mocker): results = pd.DataFrame( { @@ -40,8 +40,7 @@ def test_generate_text_table(default_conf, mocker): pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', max_open_trades=2, results=results) - assert generate_text_table(pair_results, - stake_currency='BTC') == result_str + assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str def test_generate_pair_metrics(default_conf, mocker): From 04779411f5066c221ce7fd91a237f0caba9fbd81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 14:32:01 +0200 Subject: [PATCH 46/76] Add docstring to backtest_stats --- freqtrade/optimize/optimize_reports.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 739640bc5..9bcbb4dd7 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -190,7 +190,14 @@ def generate_edge_table(results: dict) -> str: def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], - all_results: Dict[str, DataFrame]): + all_results: Dict[str, DataFrame]) -> Dict[str, Any]: + """ + :param config: Configuration object used for backtest + :param btdata: Backtest data + :param all_results: backtest result - dictionary with { Strategy: results}. + :return: + Dictionary containing results per strategy and a stratgy summary. + """ stake_currency = config['stake_currency'] max_open_trades = config['max_open_trades'] result: Dict[str, Any] = {'strategy': {}} From 3f9ab0846d6b5197094fddb50f15c5fdfe409688 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 15:17:35 +0200 Subject: [PATCH 47/76] Rename profitperc to profit_percent --- freqtrade/data/btanalysis.py | 15 ++++++++------- freqtrade/plot/plotting.py | 14 +++++++------- tests/data/test_btanalysis.py | 2 +- tests/test_plotting.py | 4 ++-- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f98135c27..6c498a470 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -16,7 +16,7 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) # must align with columns in backtest.py -BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration", +BT_DATA_COLUMNS = ["pair", "profit_percent", "open_time", "close_time", "index", "duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] @@ -99,7 +99,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "open_time", "close_time", "profit", "profitperc", + columns = ["pair", "open_time", "close_time", "profit", "profit_percent", "open_rate", "close_rate", "amount", "duration", "sell_reason", "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", "stake_amount", "max_rate", "min_rate", "id", "exchange", @@ -190,7 +190,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index - :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :param trades: DataFrame containing trades (requires columns close_time and profit_percent) :param col_name: Column name that will be assigned the results :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. @@ -201,7 +201,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) # Resample to timeframe to make sure trades match candles - _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum() + _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time' + )[['profit_percent']].sum() df.loc[:, col_name] = _trades_sum.cumsum() # Set first value to 0 df.loc[df.iloc[0].name, col_name] = 0 @@ -211,13 +212,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time', - value_col: str = 'profitperc' + value_col: str = 'profit_percent' ) -> Tuple[float, pd.Timestamp, pd.Timestamp]: """ Calculate max drawdown and the corresponding close dates - :param trades: DataFrame containing trades (requires columns close_time and profitperc) + :param trades: DataFrame containing trades (requires columns close_time and profit_percent) :param date_col: Column in DataFrame to use for dates (defaults to 'close_time') - :param value_col: Column in DataFrame to use for values (defaults to 'profitperc') + :param value_col: Column in DataFrame to use for values (defaults to 'profit_percent') :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time :raise: ValueError if trade-dataframe was found empty. """ diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f1d114e2b..d519c5f4e 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -162,7 +162,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: # Trades can be empty if trades is not None and len(trades) > 0: # Create description for sell summarizing the trade - trades['desc'] = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, " + trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, " f"{row['sell_reason']}, {row['duration']} min", axis=1) trade_buys = go.Scatter( @@ -181,9 +181,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) trade_sells = go.Scatter( - x=trades.loc[trades['profitperc'] > 0, "close_time"], - y=trades.loc[trades['profitperc'] > 0, "close_rate"], - text=trades.loc[trades['profitperc'] > 0, "desc"], + x=trades.loc[trades['profit_percent'] > 0, "close_time"], + y=trades.loc[trades['profit_percent'] > 0, "close_rate"], + text=trades.loc[trades['profit_percent'] > 0, "desc"], mode='markers', name='Sell - Profit', marker=dict( @@ -194,9 +194,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) ) trade_sells_loss = go.Scatter( - x=trades.loc[trades['profitperc'] <= 0, "close_time"], - y=trades.loc[trades['profitperc'] <= 0, "close_rate"], - text=trades.loc[trades['profitperc'] <= 0, "desc"], + x=trades.loc[trades['profit_percent'] <= 0, "close_time"], + y=trades.loc[trades['profit_percent'] <= 0, "close_rate"], + text=trades.loc[trades['profit_percent'] <= 0, "desc"], mode='markers', name='Sell - Loss', marker=dict( diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 50cf9db3d..b65db7fd8 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -47,7 +47,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_time" in trades.columns - assert "profitperc" in trades.columns + assert "profit_percent" in trades.columns for col in BT_DATA_COLUMNS: if col not in ['index', 'open_at_end']: diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 5bb113784..150329c52 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -124,7 +124,7 @@ def test_plot_trades(testdatadir, caplog): trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit') assert isinstance(trade_sell, go.Scatter) assert trade_sell.yaxis == 'y' - assert len(trades.loc[trades['profitperc'] > 0]) == len(trade_sell.x) + assert len(trades.loc[trades['profit_percent'] > 0]) == len(trade_sell.x) assert trade_sell.marker.color == 'green' assert trade_sell.marker.symbol == 'square-open' assert trade_sell.text[0] == '4.0%, roi, 15 min' @@ -132,7 +132,7 @@ def test_plot_trades(testdatadir, caplog): trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss') assert isinstance(trade_sell_loss, go.Scatter) assert trade_sell_loss.yaxis == 'y' - assert len(trades.loc[trades['profitperc'] <= 0]) == len(trade_sell_loss.x) + assert len(trades.loc[trades['profit_percent'] <= 0]) == len(trade_sell_loss.x) assert trade_sell_loss.marker.color == 'red' assert trade_sell_loss.marker.symbol == 'square-open' assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min' From 0f373e6bb9332500f00b735f4965201c8f34b031 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 15:02:56 +0200 Subject: [PATCH 48/76] Update unrelated tests --- tests/test_persistence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index ae639511b..fe2912486 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -298,7 +298,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): fee_close=fee.return_value, exchange='bittrex', ) - trade.open_order_id = 'profit_percent' + trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 # Custom closing rate and regular fee rate @@ -332,7 +332,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): fee_close=fee.return_value, exchange='bittrex', ) - trade.open_order_id = 'profit_percent' + trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 # Get percent of profit with a custom rate (Higher than open rate) From 68395d27454ee307680d381f16652c02ca8d2964 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 15:13:59 +0200 Subject: [PATCH 49/76] Use bracket notation to query results in hyperopt --- docs/advanced-hyperopt.md | 4 ++-- freqtrade/optimize/default_hyperopt_loss.py | 4 ++-- freqtrade/optimize/hyperopt_loss_onlyprofit.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 25b4bd900..5fc674b03 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -63,8 +63,8 @@ class SuperDuperHyperOptLoss(IHyperOptLoss): * 0.25: Avoiding trade loss * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() + total_profit = results['profit_percent'].sum() + trade_duration = results['trade_duration'].mean() trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 4ab9fbe44..9e780d0ea 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -42,8 +42,8 @@ class DefaultHyperOptLoss(IHyperOptLoss): * 0.25: Avoiding trade loss * 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above """ - total_profit = results.profit_percent.sum() - trade_duration = results.trade_duration.mean() + total_profit = results['profit_percent'].sum() + trade_duration = results['trade_duration'].mean() trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) diff --git a/freqtrade/optimize/hyperopt_loss_onlyprofit.py b/freqtrade/optimize/hyperopt_loss_onlyprofit.py index a1c50e727..43176dbad 100644 --- a/freqtrade/optimize/hyperopt_loss_onlyprofit.py +++ b/freqtrade/optimize/hyperopt_loss_onlyprofit.py @@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss): """ Objective function, returns smaller number for better results. """ - total_profit = results.profit_percent.sum() + total_profit = results['profit_percent'].sum() return 1 - total_profit / EXPECTED_MAX_PROFIT From a75b94f1437ad33664de4beb7c55ef7f77b5cd69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 15:30:41 +0200 Subject: [PATCH 50/76] use bracket notation for dataframe access --- freqtrade/optimize/optimize_reports.py | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 9bcbb4dd7..d89860a73 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -65,25 +65,25 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: """ return { 'key': first_column, - 'trades': len(result.index), - 'profit_mean': result.profit_percent.mean(), - 'profit_mean_pct': result.profit_percent.mean() * 100.0, - 'profit_sum': result.profit_percent.sum(), - 'profit_sum_pct': result.profit_percent.sum() * 100.0, - 'profit_total_abs': result.profit_abs.sum(), - 'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades, + 'trades': len(result), + 'profit_mean': result['profit_percent'].mean(), + 'profit_mean_pct': result['profit_percent'].mean() * 100.0, + 'profit_sum': result['profit_percent'].sum(), + 'profit_sum_pct': result['profit_percent'].sum() * 100.0, + 'profit_total_abs': result['profit_abs'].sum(), + 'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades, 'duration_avg': str(timedelta( - minutes=round(result.trade_duration.mean())) + minutes=round(result['trade_duration'].mean())) ) if not result.empty else '0:00', # 'duration_max': str(timedelta( - # minutes=round(result.trade_duration.max())) + # minutes=round(result['trade_duration'].max())) # ) if not result.empty else '0:00', # 'duration_min': str(timedelta( - # minutes=round(result.trade_duration.min())) + # minutes=round(result['trade_duration'].min())) # ) if not result.empty else '0:00', - 'wins': len(result[result.profit_abs > 0]), - 'draws': len(result[result.profit_abs == 0]), - 'losses': len(result[result.profit_abs < 0]), + 'wins': len(result[result['profit_abs'] > 0]), + 'draws': len(result[result['profit_abs'] == 0]), + 'losses': len(result[result['profit_abs'] < 0]), } @@ -102,8 +102,8 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t tabular_data = [] for pair in data: - result = results[results.pair == pair] - if skip_nan and result.profit_abs.isnull().all(): + result = results[results['pair'] == pair] + if skip_nan and result['profit_abs'].isnull().all(): continue tabular_data.append(_generate_result_line(result, max_open_trades, pair)) From bb07746bd5a6a6fcf2843bfe30d85ea5f5e9c16c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:08:19 +0000 Subject: [PATCH 51/76] Bump pytest-mock from 3.1.0 to 3.1.1 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.1.0...v3.1.1) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e05231630..6393d2478 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.770 pytest==5.4.2 pytest-asyncio==0.12.0 pytest-cov==2.9.0 -pytest-mock==3.1.0 +pytest-mock==3.1.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From 9f482d598ef3b66d3b170b09383f07abe0e9fa50 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:09:38 +0000 Subject: [PATCH 52/76] Bump ccxt from 1.29.5 to 1.29.52 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.29.5 to 1.29.52. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.29.5...1.29.52) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 07bc5caa3..dab3a5da4 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.29.5 +ccxt==1.29.52 SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.6 From 1c8243e9b31628c5b3f56f15f10937d61c878fb5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:10:07 +0000 Subject: [PATCH 53/76] Bump numpy from 1.18.4 to 1.18.5 Bumps [numpy](https://github.com/numpy/numpy) from 1.18.4 to 1.18.5. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.18.4...v1.18.5) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f5d09db4d..2c68b8f2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.18.4 +numpy==1.18.5 pandas==1.0.4 From 81ed1d6f358cc6243edcdf2e4144e94917add5ba Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:10:29 +0000 Subject: [PATCH 54/76] Bump mkdocs-material from 5.2.2 to 5.2.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.2.2 to 5.2.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.2.2...5.2.3) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 9997fa854..b8ea338de 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.2.2 +mkdocs-material==5.2.3 mdx_truly_sane_lists==1.2 From 4b5aee7d1ce36d8f3e9c5c567e0eeca271cbcb08 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:21:08 +0000 Subject: [PATCH 55/76] Bump pytest from 5.4.2 to 5.4.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.2 to 5.4.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.4.2...5.4.3) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6393d2478..0bb54679d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.2 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.770 -pytest==5.4.2 +pytest==5.4.3 pytest-asyncio==0.12.0 pytest-cov==2.9.0 pytest-mock==3.1.1 From 6029593c431bd11d9742c01d3468c8dfc24bf880 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2020 09:49:08 +0000 Subject: [PATCH 56/76] Bump mypy from 0.770 to 0.780 Bumps [mypy](https://github.com/python/mypy) from 0.770 to 0.780. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.770...v0.780) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0bb54679d..d62b768c1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==2.0.0 flake8==3.8.2 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.770 +mypy==0.780 pytest==5.4.3 pytest-asyncio==0.12.0 pytest-cov==2.9.0 From 05deb5ba05361df908e22c18ac02c222f9298a29 Mon Sep 17 00:00:00 2001 From: Mister Render Date: Tue, 9 Jun 2020 16:08:20 +0000 Subject: [PATCH 57/76] Fixed typo and missing { This should help with copy pasting the pairlists code block. Also fixed minor typo on line 594 (was: "I selects" and is now: "It selects") --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index da0f015e8..c31af1cc9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -591,7 +591,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis #### Volume Pair List -`VolumePairList` employs sorting/filtering of pairs by their trading volume. I selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). +`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. @@ -609,7 +609,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which "number_assets": 20, "sort_key": "quoteVolume", "refresh_period": 1800, -], +}], ``` #### PrecisionFilter From bd942992ef80bff0c7768ffe7ee57c8965e40265 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 20:47:52 +0200 Subject: [PATCH 58/76] Add documentation about pricing related to market orders --- docs/configuration.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index c31af1cc9..035a65abf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -333,6 +333,9 @@ Configuration: !!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. +!!! Warning "Using market orders" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. + !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. @@ -459,6 +462,9 @@ Prices are always retrieved right before an order is placed, either by querying !!! Note Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details. +!!! Warning "Using market orders" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. + ### Buy price #### Check depth of market @@ -553,6 +559,29 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +### Market order pricing + +When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. +Assuming both buy and sell are using market orders, a configuration similar to the following might be used + +``` jsonc + "order_types": { + "buy": "market", + "sell": "market" + // ... + }, + "bid_strategy": { + "price_side": "ask", + //... + }, + "ask_strategy":{ + "price_side": "bid", + //... + }, +``` + +Obviously, if only one side is using limit orders, different pricing combinations can be used. + ## Pairlists and Pairlist Handlers Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. From 6744f8f052d66e1f7a49eff81c6ff57e65fec0cf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jun 2020 01:22:55 +0300 Subject: [PATCH 59/76] Remove _load_async_markets --- freqtrade/exchange/exchange.py | 17 +++-------------- tests/exchange/test_exchange.py | 28 ---------------------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 038fc22bc..773f7a0a4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -256,32 +256,21 @@ class Exchange: "Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') - def _load_async_markets(self, reload: bool = False) -> None: - try: - if self._api_async: - asyncio.get_event_loop().run_until_complete( - self._api_async.load_markets(reload=reload)) - - except ccxt.BaseError as e: - logger.warning('Could not load async markets. Reason: %s', e) - return - def _load_markets(self) -> None: - """ Initialize markets both sync and async """ + """ Initialize markets """ try: self._api.load_markets() - self._load_async_markets() self._last_markets_refresh = arrow.utcnow().timestamp except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) def _reload_markets(self) -> None: - """Reload markets both sync and async, if refresh interval has passed""" + """Reload markets if refresh interval has passed""" # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval > arrow.utcnow().timestamp): - return None + return logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 32163f696..31efc65ff 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -130,7 +130,6 @@ def test_init_exception(default_conf, mocker): def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -318,19 +317,6 @@ def test_set_sandbox_exception(default_conf, mocker): exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') -def test__load_async_markets(default_conf, mocker, caplog): - exchange = get_patched_exchange(mocker, default_conf) - exchange._api_async.load_markets = get_mock_coro(None) - exchange._load_async_markets() - assert exchange._api_async.load_markets.call_count == 1 - caplog.set_level(logging.DEBUG) - - exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) - exchange._load_async_markets() - - assert log_has('Could not load async markets. Reason: deadbeef', caplog) - - def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() @@ -338,7 +324,6 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) assert log_has('Unable to initialize markets. Reason: SomeError', caplog) @@ -406,7 +391,6 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') Exchange(default_conf) @@ -420,7 +404,6 @@ def test_validate_stake_currency_error(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): @@ -470,7 +453,6 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -483,7 +465,6 @@ def test_validate_pairs_not_available(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -498,7 +479,6 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): Exchange(default_conf) @@ -517,7 +497,6 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -535,7 +514,6 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -551,7 +529,6 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -567,7 +544,6 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): @@ -742,7 +718,6 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -1934,7 +1909,6 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), - _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), validate_stakecurrency=MagicMock() @@ -1968,7 +1942,6 @@ def test_merge_ft_has_dict(default_conf, mocker): def test_get_valid_pair_combination(default_conf, mocker, markets): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), - _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) @@ -2041,7 +2014,6 @@ def test_get_markets(default_conf, mocker, markets, expected_keys): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), - _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) From 7d451638a84e9d709291a4038cb06f760bfa9c80 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jun 2020 01:39:23 +0300 Subject: [PATCH 60/76] Make _reload_markets() public --- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/freqtradebot.py | 4 ++-- tests/exchange/test_exchange.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 773f7a0a4..6802a6b9e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -264,8 +264,8 @@ class Exchange: except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) - def _reload_markets(self) -> None: - """Reload markets if refresh interval has passed""" + def reload_markets(self) -> None: + """Reload markets if refresh interval has passed """ # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5104e4f95..8a66957c3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -139,8 +139,8 @@ class FreqtradeBot: :return: True if one or more trades has been created or closed, False otherwise """ - # Check whether markets have to be reloaded - self.exchange._reload_markets() + # Check whether markets have to be reloaded and reload them when it's needed + self.exchange.reload_markets() # Query trades from persistence layer trades = Trade.get_open_trades() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 31efc65ff..9914188b8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -337,7 +337,7 @@ def test__load_markets(default_conf, mocker, caplog): assert ex.markets == expected_return -def test__reload_markets(default_conf, mocker, caplog): +def test_reload_markets(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) initial_markets = {'ETH/BTC': {}} @@ -356,17 +356,17 @@ def test__reload_markets(default_conf, mocker, caplog): assert exchange.markets == initial_markets # less than 10 minutes have passed, no reload - exchange._reload_markets() + exchange.reload_markets() assert exchange.markets == initial_markets # more than 10 minutes have passed, reload is executed exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 - exchange._reload_markets() + exchange.reload_markets() assert exchange.markets == updated_markets assert log_has('Performing scheduled market reload..', caplog) -def test__reload_markets_exception(default_conf, mocker, caplog): +def test_reload_markets_exception(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) api_mock = MagicMock() @@ -375,7 +375,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") # less than 10 minutes have passed, no reload - exchange._reload_markets() + exchange.reload_markets() assert exchange._last_markets_refresh == 0 assert log_has_re(r"Could not reload markets.*", caplog) From 0067a3ab7ceb929b2ded3a7916a064f759f45c69 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jun 2020 06:30:29 +0300 Subject: [PATCH 61/76] Change logging level --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6802a6b9e..e974655cb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -190,7 +190,7 @@ class Exchange: def markets(self) -> Dict: """exchange ccxt markets""" if not self._api.markets: - logger.warning("Markets were not loaded. Loading them now..") + logger.info("Markets were not loaded. Loading them now..") self._load_markets() return self._api.markets From a198b91b873ca428ccb19dc5244a47104c228a2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 06:36:35 +0200 Subject: [PATCH 62/76] align Spaces in commented config section Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 035a65abf..cb5b6c3ea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -572,11 +572,11 @@ Assuming both buy and sell are using market orders, a configuration similar to t }, "bid_strategy": { "price_side": "ask", - //... + // ... }, "ask_strategy":{ "price_side": "bid", - //... + // ... }, ``` From ac92834693fe5e9a3c165c3529c7dbfc4f1a3798 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Tue, 9 Jun 2020 23:03:55 +0200 Subject: [PATCH 63/76] reload_conf & reload_config now both accepted, code is more consistent now --- docs/rest-api.md | 6 +++--- docs/stoploss.md | 2 +- docs/strategy-customization.md | 2 +- docs/telegram-usage.md | 8 ++++---- freqtrade/rpc/api_server.py | 10 +++++----- freqtrade/rpc/rpc.py | 8 ++++---- freqtrade/rpc/telegram.py | 12 +++++++----- freqtrade/state.py | 2 +- freqtrade/worker.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 13 +++++++------ tests/test_main.py | 4 ++-- 13 files changed, 40 insertions(+), 37 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index ed5f355b4..33f62f884 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -110,7 +110,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `start` | | Starts the trader | `stop` | | Stops the trader | `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `reload_conf` | | Reloads the configuration file +| `reload_config` | | Reloads the configuration file | `show_config` | | Shows part of the current configuration with relevant settings to operation | `status` | | Lists all open trades | `count` | | Displays number of trades used and available @@ -174,7 +174,7 @@ profit Returns the profit summary :returns: json object -reload_conf +reload_config Reload configuration :returns: json object @@ -196,7 +196,7 @@ stop stopbuy Stop buying (but handle sells gracefully). - use reload_conf to reset + use reload_config to reset :returns: json object version diff --git a/docs/stoploss.md b/docs/stoploss.md index 0e43817ec..7ebe98ee6 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -101,7 +101,7 @@ Simplified example: ## Changing stoploss on open trades -A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_conf` command (alternatively, completely stopping and restarting the bot also works). +A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works). The new stoploss value will be applied to open trades (and corresponding log-messages will be generated). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7197b0fba..92e4453d2 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -557,7 +557,7 @@ Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. !!! Note - Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs. + Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs. !!! Warning Locking pairs is not functioning during backtesting. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f683ae8da..f423a9376 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -52,7 +52,7 @@ official commands. You can ask at any moment for help with `/help`. | `/start` | | Starts the trader | `/stop` | | Stops the trader | `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `/reload_conf` | | Reloads the configuration file +| `/reload_config` | | Reloads the configuration file | `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/status` | | Lists all open trades | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) @@ -85,14 +85,14 @@ Below, example of Telegram message you will receive for each command. ### /stopbuy -> **status:** `Setting max_open_trades to 0. Run /reload_conf to reset.` +> **status:** `Setting max_open_trades to 0. Run /reload_config to reset.` Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...). After this, give the bot time to close off open trades (can be checked via `/status table`). Once all positions are sold, run `/stop` to completely stop the bot. -`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. +`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command. !!! Warning The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. @@ -209,7 +209,7 @@ Shows the current whitelist Shows the current blacklist. If Pair is set, then this pair will be added to the pairlist. Also supports multiple pairs, seperated by a space. -Use `/reload_conf` to reset the blacklist. +Use `/reload_config` to reset the blacklist. > Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 9d0899ccd..f424bea92 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -172,8 +172,8 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf', - view_func=self._reload_conf, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config', + view_func=self._reload_config, methods=['POST']) # Info commands self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', view_func=self._balance, methods=['GET']) @@ -304,12 +304,12 @@ class ApiServer(RPC): @require_login @rpc_catch_errors - def _reload_conf(self): + def _reload_config(self): """ - Handler for /reload_conf. + Handler for /reload_config. Triggers a config file reload """ - msg = self._rpc_reload_conf() + msg = self._rpc_reload_config() return self.rest_dump(msg) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 00a170ee3..e4c96cf3b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -418,9 +418,9 @@ class RPC: return {'status': 'already stopped'} - def _rpc_reload_conf(self) -> Dict[str, str]: - """ Handler for reload_conf. """ - self._freqtrade.state = State.RELOAD_CONF + def _rpc_reload_config(self) -> Dict[str, str]: + """ Handler for reload_config. """ + self._freqtrade.state = State.RELOAD_CONFIG return {'status': 'reloading config ...'} def _rpc_stopbuy(self) -> Dict[str, str]: @@ -431,7 +431,7 @@ class RPC: # Set 'max_open_trades' to 0 self._freqtrade.config['max_open_trades'] = 0 - return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index eb53fc68f..e86f35687 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,7 +95,9 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), - CommandHandler('reload_conf', self._reload_conf), + CommandHandler('reload_conf', self._reload_config), + CommandHandler('reload_config', self._reload_config), + CommandHandler('show_conf', self._reload_config), CommandHandler('show_config', self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), @@ -436,15 +438,15 @@ class Telegram(RPC): self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only - def _reload_conf(self, update: Update, context: CallbackContext) -> None: + def _reload_config(self, update: Update, context: CallbackContext) -> None: """ - Handler for /reload_conf. + Handler for /reload_config. Triggers a config file reload :param bot: telegram bot :param update: message update :return: None """ - msg = self._rpc_reload_conf() + msg = self._rpc_reload_config() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -617,7 +619,7 @@ class Telegram(RPC): "\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" - "*/reload_conf:* `Reload configuration file` \n" + "*/reload_config:* `Reload configuration file` \n" "*/show_config:* `Show running configuration` \n" "*/whitelist:* `Show current whitelist` \n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " diff --git a/freqtrade/state.py b/freqtrade/state.py index 38784c6a4..8ddff71d9 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -12,7 +12,7 @@ class State(Enum): """ RUNNING = 1 STOPPED = 2 - RELOAD_CONF = 3 + RELOAD_CONFIG = 3 def __str__(self): return f"{self.name.lower()}" diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 3f5ab734e..5bdb166c2 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -71,7 +71,7 @@ class Worker: state = None while True: state = self._worker(old_state=state) - if state == State.RELOAD_CONF: + if state == State.RELOAD_CONFIG: self._reconfigure() def _worker(self, old_state: Optional[State]) -> State: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ef1c1bc16..88c82c5ea 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -592,7 +592,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: assert freqtradebot.config['max_open_trades'] != 0 result = rpc._rpc_stopbuy() - assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result + assert {'status': 'No more buy will occur from now. Run /reload_config to reset.'} == result assert freqtradebot.config['max_open_trades'] == 0 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 9b247fefc..a6d38cc9f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -251,10 +251,10 @@ def test_api_cleanup(default_conf, mocker, caplog): def test_api_reloadconf(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/reload_conf") + rc = client_post(client, f"{BASE_URI}/reload_config") assert_response(rc) assert rc.json == {'status': 'reloading config ...'} - assert ftbot.state == State.RELOAD_CONF + assert ftbot.state == State.RELOAD_CONFIG def test_api_stopbuy(botclient): @@ -263,7 +263,7 @@ def test_api_stopbuy(botclient): rc = client_post(client, f"{BASE_URI}/stopbuy") assert_response(rc) - assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'} assert ftbot.config['max_open_trades'] == 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b18106ee5..afe007347 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -73,8 +73,9 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " - "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") + "['performance'], ['daily'], ['count'], ['reload_conf'], ['reload_config'], " + "['show_conf'], ['show_config'], ['stopbuy'], ['whitelist'], " + "['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) @@ -666,11 +667,11 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram._stopbuy(update=update, context=MagicMock()) assert freqtradebot.config['max_open_trades'] == 0 assert msg_mock.call_count == 1 - assert 'No more buy will occur from now. Run /reload_conf to reset.' \ + assert 'No more buy will occur from now. Run /reload_config to reset.' \ in msg_mock.call_args_list[0][0][0] -def test_reload_conf_handle(default_conf, update, mocker) -> None: +def test_reload_config_handle(default_conf, update, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -683,8 +684,8 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._reload_conf(update=update, context=MagicMock()) - assert freqtradebot.state == State.RELOAD_CONF + telegram._reload_config(update=update, context=MagicMock()) + assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 assert 'reloading config' in msg_mock.call_args_list[0][0][0] diff --git a/tests/test_main.py b/tests/test_main.py index 11d0ede3a..48cc47104 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -141,12 +141,12 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has_re(r'SIGINT.*', caplog) -def test_main_reload_conf(mocker, default_conf, caplog) -> None: +def test_main_reload_config(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) # Simulate Running, reload, running workflow worker_mock = MagicMock(side_effect=[State.RUNNING, - State.RELOAD_CONF, + State.RELOAD_CONFIG, State.RUNNING, OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) From 04fa59769596506c4ff958b0554aaa0ca42c4a1d Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:28:37 +0200 Subject: [PATCH 64/76] Test with multiple commands in one line --- freqtrade/rpc/telegram.py | 8 ++++---- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e86f35687..51130e653 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,10 +95,10 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), - CommandHandler('reload_conf', self._reload_config), - CommandHandler('reload_config', self._reload_config), - CommandHandler('show_conf', self._reload_config), - CommandHandler('show_config', self._show_config), + #CommandHandler('reload_conf', self._reload_config), + CommandHandler(('reload_config' or 'reload_con'), self._reload_config), + #CommandHandler('show_conf', self._reload_config), + CommandHandler('show_config' or 'show_conf', self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index afe007347..ce0c9d1f0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -73,8 +73,8 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_conf'], ['reload_config'], " - "['show_conf'], ['show_config'], ['stopbuy'], ['whitelist'], " + "['performance'], ['daily'], ['count'], ['reload_config'], " + "['show_config'], ['stopbuy'], ['whitelist'], " "['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From 043397c5d7d70d2e7c3821b892486413a8a68a32 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:34:46 +0200 Subject: [PATCH 65/76] reload_conf & reload_config now both accepted, code is more consistent now --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 51130e653..e7a8971ec 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -96,7 +96,7 @@ class Telegram(RPC): CommandHandler('daily', self._daily), CommandHandler('count', self._count), #CommandHandler('reload_conf', self._reload_config), - CommandHandler(('reload_config' or 'reload_con'), self._reload_config), + CommandHandler(('reload_config' or 'reload_conf'), self._reload_config), #CommandHandler('show_conf', self._reload_config), CommandHandler('show_config' or 'show_conf', self._show_config), CommandHandler('stopbuy', self._stopbuy), From 8c9dea988cf0665744dba54c7b9c0b4c2dfd3cfe Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:55:47 +0200 Subject: [PATCH 66/76] Now supports both commands & fixed test --- freqtrade/rpc/telegram.py | 6 ++---- tests/rpc/test_rpc_telegram.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e7a8971ec..006baee60 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,10 +95,8 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), - #CommandHandler('reload_conf', self._reload_config), - CommandHandler(('reload_config' or 'reload_conf'), self._reload_config), - #CommandHandler('show_conf', self._reload_config), - CommandHandler('show_config' or 'show_conf', self._show_config), + CommandHandler(['reload_config', 'reload_conf'], self._reload_config), + CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ce0c9d1f0..b19c6d9ee 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -72,10 +72,9 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_config'], " - "['show_config'], ['stopbuy'], ['whitelist'], " - "['blacklist'], ['edge'], ['help'], ['version']]") + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['performance'], " + "['daily'], ['count'], ['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From 4f643f8481af5ed983615c9a88b9296cc9a21b92 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 17:12:21 +0200 Subject: [PATCH 67/76] Fix Flake8 error: line too long --- tests/rpc/test_rpc_telegram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b19c6d9ee..0a4352f5b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -72,9 +72,10 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['performance'], " - "['daily'], ['count'], ['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " - "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " + "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " + "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " + "['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From a7cd68121beb33cb40520b94439344879c1b7114 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 19:44:34 +0200 Subject: [PATCH 68/76] Have rest-client use new reload_config endpoint --- scripts/rest_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b26c32479..1f96bcb69 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -80,18 +80,18 @@ class FtRestClient(): return self._post("stop") def stopbuy(self): - """Stop buying (but handle sells gracefully). Use `reload_conf` to reset. + """Stop buying (but handle sells gracefully). Use `reload_config` to reset. :return: json object """ return self._post("stopbuy") - def reload_conf(self): + def reload_config(self): """Reload configuration. :return: json object """ - return self._post("reload_conf") + return self._post("reload_config") def balance(self): """Get the account balance. From c66ca957d9a6c5adc2560042d802c29c966988c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 19:57:47 +0200 Subject: [PATCH 69/76] Add test verifying this behaviour --- tests/pairlist/test_pairlist.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 421f06911..c67f7ae1c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -421,6 +421,23 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist assert log_message in caplog.text +@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) +def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers): + whitelist_conf['pairlists'][0]['method'] = pairlist + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=None), + get_tickers=tickers + ) + # Assign starting whitelist + pairlist_handler = freqtrade.pairlists._pairlist_handlers[0] + with pytest.raises(OperationalException, match=r'Markets not loaded.*'): + pairlist_handler._whitelist_for_active_markets(['ETH/BTC']) + + def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) From 1e7826f3922748449554d01cfb81c7c366c5babb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 19:57:59 +0200 Subject: [PATCH 70/76] Explicitly raise OperationalException if markets are not loaded correctly --- freqtrade/pairlist/IPairList.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index f48a7dcfd..fd25e0766 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -150,6 +150,9 @@ class IPairList(ABC): black_listed """ markets = self._exchange.markets + if not markets: + raise OperationalException( + 'Markets not loaded. Make sure that exchange is initialized correctly.') sanitized_whitelist: List[str] = [] for pair in pairlist: From ab2f5579d8ad638d10a222ff1f84fdff41306c4e Mon Sep 17 00:00:00 2001 From: CoDaXe Date: Thu, 11 Jun 2020 20:34:14 -0400 Subject: [PATCH 71/76] Update strategy-customization.md Fix typo "the an" --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 92e4453d2..be41b196e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -139,7 +139,7 @@ By letting the bot know how much history is needed, backtest trades can start at #### Example -Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above. +Let's try to backtest 1 month (January 2019) of 5m candles using an example strategy with EMA100, as above. ``` bash freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m From 9615614e489d194155a896c6cb8c4ca86522ce8f Mon Sep 17 00:00:00 2001 From: CoDaXe Date: Fri, 12 Jun 2020 13:13:10 -0400 Subject: [PATCH 72/76] Update hyperopt.md Wrong flag name --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8efc51a39..f66534726 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -265,7 +265,7 @@ freqtrade hyperopt --timerange 20180401-20180501 Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. ```bash -freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt +freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt ``` ### Running Hyperopt with Smaller Search Space From 9890e26aeb7d1664088b1dc3cad3db1eba69a633 Mon Sep 17 00:00:00 2001 From: John Duong Date: Fri, 12 Jun 2020 22:10:18 -0700 Subject: [PATCH 73/76] fix SQL cheatsheet query (#3475) --- docs/sql_cheatsheet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 895a0536a..88eb0d3d4 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -101,7 +101,7 @@ SET is_open=0, close_date=, close_rate=, close_profit=close_rate/open_rate-1, - close_profit_abs = (amount * * (1 - fee_close) - (amount * open_rate * 1 - fee_open), + close_profit_abs = (amount * * (1 - fee_close) - (amount * open_rate * 1 - fee_open)), sell_reason= WHERE id=; ``` @@ -114,7 +114,7 @@ SET is_open=0, close_date='2017-12-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)) sell_reason='force_sell' WHERE id=31; ``` From 37bc2d28ad4572234940689fa938a48732d11207 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 13 Jun 2020 13:34:29 +0300 Subject: [PATCH 74/76] Revert "Remove _load_async_markets" This reverts commit 6744f8f052d66e1f7a49eff81c6ff57e65fec0cf. --- freqtrade/exchange/exchange.py | 17 ++++++++++++++--- tests/exchange/test_exchange.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e974655cb..bd44f56f2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -256,21 +256,32 @@ class Exchange: "Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') + def _load_async_markets(self, reload: bool = False) -> None: + try: + if self._api_async: + asyncio.get_event_loop().run_until_complete( + self._api_async.load_markets(reload=reload)) + + except ccxt.BaseError as e: + logger.warning('Could not load async markets. Reason: %s', e) + return + def _load_markets(self) -> None: - """ Initialize markets """ + """ Initialize markets both sync and async """ try: self._api.load_markets() + self._load_async_markets() self._last_markets_refresh = arrow.utcnow().timestamp except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) def reload_markets(self) -> None: - """Reload markets if refresh interval has passed """ + """Reload markets both sync and async if refresh interval has passed """ # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval > arrow.utcnow().timestamp): - return + return None logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9914188b8..2b63eee23 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -130,6 +130,7 @@ def test_init_exception(default_conf, mocker): def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -317,6 +318,19 @@ def test_set_sandbox_exception(default_conf, mocker): exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') +def test__load_async_markets(default_conf, mocker, caplog): + exchange = get_patched_exchange(mocker, default_conf) + exchange._api_async.load_markets = get_mock_coro(None) + exchange._load_async_markets() + assert exchange._api_async.load_markets.call_count == 1 + caplog.set_level(logging.DEBUG) + + exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) + exchange._load_async_markets() + + assert log_has('Could not load async markets. Reason: deadbeef', caplog) + + def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() @@ -324,6 +338,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) assert log_has('Unable to initialize markets. Reason: SomeError', caplog) @@ -391,6 +406,7 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') Exchange(default_conf) @@ -404,6 +420,7 @@ def test_validate_stake_currency_error(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): @@ -453,6 +470,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -465,6 +483,7 @@ def test_validate_pairs_not_available(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -479,6 +498,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): Exchange(default_conf) @@ -497,6 +517,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -514,6 +535,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -529,6 +551,7 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -544,6 +567,7 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): @@ -718,6 +742,7 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -1909,6 +1934,7 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), validate_stakecurrency=MagicMock() @@ -1942,6 +1968,7 @@ def test_merge_ft_has_dict(default_conf, mocker): def test_get_valid_pair_combination(default_conf, mocker, markets): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) @@ -2014,6 +2041,7 @@ def test_get_markets(default_conf, mocker, markets, expected_keys): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) From f6f7c99b9c558a6410b98b4b884a5c952cce074a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 06:31:05 +0200 Subject: [PATCH 75/76] Adjust typography and add missing space Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- freqtrade/exchange/ftx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 73347f1eb..f16db96f5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -60,7 +60,7 @@ class Ftx(Exchange): return order except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: @@ -91,7 +91,7 @@ class Ftx(Exchange): if len(order) == 1: return order[0] else: - raise InvalidOrderException(f"Could not get Stoploss Order for id {order_id}") + raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") except ccxt.InvalidOrder as e: raise InvalidOrderException( From 534c242d1b44f0cda20202da85632936fc1e6ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 06:33:08 +0200 Subject: [PATCH 76/76] Apply typography to test too --- tests/exchange/test_ftx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index bead63096..75e98740c 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -149,7 +149,7 @@ def test_get_stoploss_order(default_conf, mocker): api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - with pytest.raises(InvalidOrderException, match=r"Could not get Stoploss Order for id X"): + with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): exchange.get_stoploss_order('X', 'TKN/BTC')['status'] with pytest.raises(InvalidOrderException):