From 72cf3147b865063000100fee974a5e4ae4a7ef04 Mon Sep 17 00:00:00 2001 From: apneamona <65978183+apneamona@users.noreply.github.com> Date: Tue, 6 Oct 2020 20:17:05 +0200 Subject: [PATCH 01/63] Update configuration.md --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d6e26f80e..44736e0ba 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -59,8 +59,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean -| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer +| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
*Defaults to `bid`.*
**Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
**Datatype:** Boolean From ecddaa663b3c8be6af8f305efd5f48548e52d2b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Oct 2020 19:58:04 +0200 Subject: [PATCH 02/63] Convert timestamp to int_timestamp for all arrow occurances --- freqtrade/configuration/timerange.py | 8 ++++---- freqtrade/edge/edge_positioning.py | 4 ++-- freqtrade/exchange/exchange.py | 12 ++++++------ freqtrade/optimize/optimize_reports.py | 4 ++-- freqtrade/wallets.py | 4 ++-- tests/commands/test_commands.py | 5 +++-- tests/conftest.py | 12 ++++++------ tests/data/test_history.py | 4 ++-- tests/edge/test_edge.py | 12 ++++++------ tests/exchange/test_exchange.py | 23 ++++++++++++----------- 10 files changed, 45 insertions(+), 43 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 151003999..32bbd02a0 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -52,11 +52,11 @@ class TimeRange: :return: None (Modifies the object in place) """ if (not self.starttype or (startup_candles - and min_date.timestamp >= self.startts)): + and min_date.int_timestamp >= self.startts)): # If no startts was defined, or backtest-data starts at the defined backtest-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) - self.startts = (min_date.timestamp + timeframe_secs * startup_candles) + self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles) self.starttype = 'date' @staticmethod @@ -89,7 +89,7 @@ class TimeRange: if stype[0]: starts = rvals[index] if stype[0] == 'date' and len(starts) == 8: - start = arrow.get(starts, 'YYYYMMDD').timestamp + start = arrow.get(starts, 'YYYYMMDD').int_timestamp elif len(starts) == 13: start = int(starts) // 1000 else: @@ -98,7 +98,7 @@ class TimeRange: if stype[1]: stops = rvals[index] if stype[1] == 'date' and len(stops) == 8: - stop = arrow.get(stops, 'YYYYMMDD').timestamp + stop = arrow.get(stops, 'YYYYMMDD').int_timestamp elif len(stops) == 13: stop = int(stops) // 1000 else: diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index a40b63d67..037717c68 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -87,7 +87,7 @@ class Edge: heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( - self._last_updated + heartbeat > arrow.utcnow().timestamp): + self._last_updated + heartbeat > arrow.utcnow().int_timestamp): return False data: Dict[str, Any] = {} @@ -146,7 +146,7 @@ class Edge: # Fill missing, calculable columns, profit, duration , abs etc. trades_df = self._fill_calculable_fields(DataFrame(trades)) self._cached_pairs = self._process_expectancy(trades_df) - self._last_updated = arrow.utcnow().timestamp + self._last_updated = arrow.utcnow().int_timestamp return True diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bbb94e61f..d6836ee73 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -291,7 +291,7 @@ class Exchange: try: self._api.load_markets() self._load_async_markets() - self._last_markets_refresh = arrow.utcnow().timestamp + self._last_markets_refresh = arrow.utcnow().int_timestamp except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) @@ -300,14 +300,14 @@ class Exchange: # 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): + > arrow.utcnow().int_timestamp): return None logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) - self._last_markets_refresh = arrow.utcnow().timestamp + self._last_markets_refresh = arrow.utcnow().int_timestamp except ccxt.BaseError: logger.exception("Could not reload markets.") @@ -501,7 +501,7 @@ class Exchange: 'side': side, 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), - 'timestamp': int(arrow.utcnow().timestamp * 1000), + 'timestamp': int(arrow.utcnow().int_timestamp * 1000), 'status': "closed" if ordertype == "market" else "open", 'fee': None, 'info': {} @@ -696,7 +696,7 @@ class Exchange: ) input_coroutines = [self._async_get_candle_history( pair, timeframe, since) for since in - range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] + range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)] results = await asyncio.gather(*input_coroutines, return_exceptions=True) @@ -759,7 +759,7 @@ class Exchange: interval_in_sec = timeframe_to_seconds(timeframe) return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0) - + interval_in_sec) >= arrow.utcnow().timestamp) + + interval_in_sec) >= arrow.utcnow().int_timestamp) @retrier_async async def _async_get_candle_history(self, pair: str, timeframe: str, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3db9a312a..c977a991b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -268,9 +268,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'profit_total': results['profit_percent'].sum(), 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.timestamp * 1000, + 'backtest_start_ts': min_date.int_timestamp * 1000, 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.timestamp * 1000, + 'backtest_end_ts': max_date.int_timestamp * 1000, 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 21a9466e1..3680dd416 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -108,13 +108,13 @@ class Wallets: for trading operations, the latest balance is needed. :param require_update: Allow skipping an update if balances were recently refreshed """ - if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().timestamp)): + if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): if self._config['dry_run']: self._update_dry() else: self._update_live() logger.info('Wallets synced.') - self._last_wallet_refresh = arrow.utcnow().timestamp + self._last_wallet_refresh = arrow.utcnow().int_timestamp def get_all_balances(self) -> Dict[str, Any]: return self._wallets diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 5b125697c..bf845a2e1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -579,7 +579,7 @@ def test_download_data_timerange(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 # 20days ago - days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).timestamp + days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago dl_mock.reset_mock() @@ -592,7 +592,8 @@ def test_download_data_timerange(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 - assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(2020, 1, 1).timestamp + assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow( + 2020, 1, 1).int_timestamp def test_download_data_no_markets(mocker, caplog): diff --git a/tests/conftest.py b/tests/conftest.py index 2153fd327..a99404ac2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -792,7 +792,7 @@ def limit_buy_order_open(): 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().timestamp, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -911,7 +911,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -932,7 +932,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': 'AZNPFF-4AC4N-7MKTAT', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'status': 'canceled', @@ -953,7 +953,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -974,7 +974,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1000,7 +1000,7 @@ def limit_sell_order_open(): 'side': 'sell', 'pair': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().timestamp, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001173, 'amount': 90.99181073, 'filled': 0.0, diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c8324cf0b..bbc6e55b4 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -323,7 +323,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, - timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp)) assert log_has( 'Using indicator startup period: 20 ...', caplog ) @@ -339,7 +339,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: start = arrow.get('2018-01-10T00:00:00') end = arrow.get('2018-02-20T00:00:00') data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp)) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(data['UNITTEST/BTC']) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index a4bfa1085..f25dad35b 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -50,7 +50,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): 'date': tests_start_time.shift( minutes=( ohlc[0] * - timeframe_in_minute)).timestamp * + timeframe_in_minute)).int_timestamp * 1000, 'buy': ohlc[1], 'open': ohlc[2], @@ -71,7 +71,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): def _time_on_candle(number): return np.datetime64(tests_start_time.shift( - minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms') + minutes=(number * timeframe_in_minute)).int_timestamp * 1000, 'ms') # End helper functions @@ -251,7 +251,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): heartbeat = edge_conf['edge']['process_throttle_secs'] # should not recalculate if heartbeat not reached - edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1 + edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1 assert edge.calculate() is False @@ -263,7 +263,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', NEOBTC = [ [ - tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -275,7 +275,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', base = 0.002 LTCBTC = [ [ - tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -299,7 +299,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): assert edge.calculate() assert len(edge._cached_pairs) == 2 - assert edge._last_updated <= arrow.utcnow().timestamp + 2 + assert edge._last_updated <= arrow.utcnow().int_timestamp + 2 def test_edge_process_no_data(mocker, edge_conf, caplog): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7be9c77ac..7df596098 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -385,7 +385,7 @@ def test_reload_markets(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) exchange._load_async_markets = MagicMock() - exchange._last_markets_refresh = arrow.utcnow().timestamp + exchange._last_markets_refresh = arrow.utcnow().int_timestamp updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} assert exchange.markets == initial_markets @@ -396,7 +396,7 @@ def test_reload_markets(default_conf, mocker, caplog): assert exchange._load_async_markets.call_count == 0 # more than 10 minutes have passed, reload is executed - exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 + exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 exchange.reload_markets() assert exchange.markets == updated_markets assert exchange._load_async_markets.call_count == 1 @@ -1264,7 +1264,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) ohlcv = [ [ - arrow.utcnow().timestamp * 1000, # unix timestamp ms + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -1281,7 +1281,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): # one_call calculation * 1.8 should do 2 calls since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 - ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + ret = exchange.get_historic_ohlcv(pair, "5m", int(( + arrow.utcnow().int_timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 # Returns twice the above OHLCV data @@ -1291,7 +1292,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ [ - (arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms + (arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -1299,7 +1300,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: 5, # volume (in quote currency) ], [ - arrow.utcnow().timestamp * 1000, # unix timestamp ms + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms 3, # open 1, # high 4, # low @@ -1345,7 +1346,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ [ - arrow.utcnow().timestamp * 1000, # unix timestamp ms + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -1380,14 +1381,14 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", - (arrow.utcnow().timestamp - 2000) * 1000) + (arrow.utcnow().int_timestamp - 2000) * 1000) with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical candle \(OHLCV\) data\..*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", - (arrow.utcnow().timestamp - 2000) * 1000) + (arrow.utcnow().int_timestamp - 2000) * 1000) @pytest.mark.asyncio @@ -1599,13 +1600,13 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, with pytest.raises(OperationalException, match=r'Could not fetch trade data*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000) + await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical trade data\..*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000) + await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) @pytest.mark.asyncio From 685d18940a52eff4534d1a87bdb440cabb413bf1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Oct 2020 08:13:31 +0200 Subject: [PATCH 03/63] specify min-version for arrow int_timestamp was introduced in this version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9b57e8d2c..b47427709 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup(name='freqtrade', 'ccxt>=1.24.96', 'SQLAlchemy', 'python-telegram-bot', - 'arrow', + 'arrow>=0.17.0', 'cachetools', 'requests', 'urllib3', From adffd402ea6aff03719d21bf725448f8da0d8b27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Oct 2020 20:11:38 +0200 Subject: [PATCH 04/63] Replace some pointless occurances of arrow --- freqtrade/commands/data_commands.py | 4 ++-- freqtrade/data/dataprovider.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 7102eee38..c12c20ddd 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -1,9 +1,9 @@ import logging import sys from collections import defaultdict +from datetime import datetime, timedelta from typing import Any, Dict, List -import arrow from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format @@ -29,7 +29,7 @@ def start_download_data(args: Dict[str, Any]) -> None: "You can only specify one or the other.") timerange = TimeRange() if 'days' in config: - time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d") + time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") timerange = TimeRange.parse_timerange(f'{time_since}-') if 'timerange' in config: diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 07dd94fc1..ba43044a1 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -8,7 +8,6 @@ import logging from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from arrow import Arrow from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe @@ -38,7 +37,7 @@ class DataProvider: :param timeframe: Timeframe to get data for :param dataframe: analyzed dataframe """ - self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime) + self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc)) def add_pairlisthandler(self, pairlists) -> None: """ From fd6018f67aaa9620c237098f8139bcfb06832d62 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Oct 2020 06:21:13 +0200 Subject: [PATCH 05/63] Fix dependency sorting --- freqtrade/commands/data_commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index c12c20ddd..b28e0c357 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -4,7 +4,6 @@ from collections import defaultdict from datetime import datetime, timedelta from typing import Any, Dict, List - from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, From b8c12f657670ecc52bcb15bb9c600e63c4408314 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 07:45:11 +0200 Subject: [PATCH 06/63] Test if return value is an exception when downloading historic data --- freqtrade/exchange/exchange.py | 17 ++++++++++++----- tests/exchange/test_exchange.py | 9 +++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c0d737f26..da3c83b0c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -687,6 +687,9 @@ class Exchange: async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: + """ + Download historic ohlcv + """ one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit logger.debug( @@ -702,9 +705,14 @@ class Exchange: # Combine gathered results data: List = [] - for p, timeframe, res in results: + for res in results: + if isinstance(res, Exception): + logger.warning("Async code raised an exception: %s", res.__class__.__name__) + continue + # Deconstruct tuple if it's not an exception + p, _, new_data = res if p == pair: - data.extend(res) + data.extend(new_data) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) logger.info("Downloaded data for %s with length %s.", pair, len(data)) @@ -741,9 +749,8 @@ class Exchange: if isinstance(res, Exception): logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue - pair = res[0] - timeframe = res[1] - ticks = res[2] + # Deconstruct tuple (has 3 elements) + pair, timeframe, ticks = res # keeping last candle time as last refreshed time of the pair if ticks: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 19f2c7239..b23c18bb3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1295,6 +1295,15 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): # Returns twice the above OHLCV data assert len(ret) == 2 + caplog.clear() + + async def mock_get_candle_hist_error(pair, *args, **kwargs): + raise TimeoutError() + + exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error) + ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + assert log_has_re(r"Async code raised an exception: .*", caplog) + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ From 3439e6c5c42693f5c75e2eb76557405e53806983 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:13 +0000 Subject: [PATCH 07/63] Bump urllib3 from 1.25.10 to 1.25.11 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.10 to 1.25.11. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.10...1.25.11) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..c03f852c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 requests==2.24.0 -urllib3==1.25.10 +urllib3==1.25.11 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From df5e6aa58b9d4e049a10e62b5c756da035d5bda5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:32 +0000 Subject: [PATCH 08/63] Bump aiohttp from 3.6.3 to 3.7.1 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.6.3 to 3.7.1. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.6.3...v3.7.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..bf597c256 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.1.3 ccxt==1.36.66 multidict==4.7.6 -aiohttp==3.6.3 +aiohttp==3.7.1 SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 From 2831a78d0e0bf51bbd9388f6b3506658617cac2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:33 +0000 Subject: [PATCH 09/63] Bump ccxt from 1.36.66 to 1.36.85 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.36.66 to 1.36.85. - [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.36.66...1.36.85) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..dcd02c752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.3 -ccxt==1.36.66 +ccxt==1.36.85 multidict==4.7.6 aiohttp==3.6.3 SQLAlchemy==1.3.20 From 95d11bd0d24318bb9846bf0a00b7b688ebccba72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:34 +0000 Subject: [PATCH 10/63] Bump python-rapidjson from 0.9.1 to 0.9.3 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.9.1 to 0.9.3. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v0.9.1...v0.9.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..fb961e5d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.1 +python-rapidjson==0.9.3 # Notify systemd sdnotify==0.3.2 From 066ea45ce0e6ac37ca39261354cfcc82c4ac5030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:39 +0000 Subject: [PATCH 11/63] Bump plotly from 4.11.0 to 4.12.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.11.0 to 4.12.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.11.0...v4.12.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 7c3e04723..bd40bc0b5 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.11.0 +plotly==4.12.0 From 442e9d20e12061e94e652070818414e724a9535a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Oct 2020 16:25:46 +0100 Subject: [PATCH 12/63] Remove pinned dependency of multidict --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b97965f6..baf368adf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ numpy==1.19.2 pandas==1.1.3 ccxt==1.36.85 -multidict==4.7.6 aiohttp==3.7.1 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 69e8da30e53ee6a69183b9ce645b632eda37b0e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Oct 2020 10:13:54 +0100 Subject: [PATCH 13/63] Ensure times that fall on a candle are also shifted --- tests/exchange/test_exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b23c18bb3..a01700e5d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,6 +1,6 @@ import copy import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch @@ -2300,6 +2300,9 @@ def test_timeframe_to_next_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_next_date("5m") > date + date = datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) + assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5) + @pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [ ("BTC/USDT", 'BTC', 'USDT', "binance", {}, True), From e602ac3406ae2d23210811bc2b21503b39303fa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Oct 2020 10:54:30 +0100 Subject: [PATCH 14/63] Introduce Pairlocks middleware --- freqtrade/freqtradebot.py | 32 +++---- freqtrade/persistence/__init__.py | 4 +- freqtrade/persistence/models.py | 50 +--------- freqtrade/persistence/pairlock_middleware.py | 97 ++++++++++++++++++++ freqtrade/rpc/rpc.py | 4 +- freqtrade/strategy/interface.py | 10 +- tests/pairlist/test_pairlocks.py | 81 ++++++++++++++++ tests/rpc/test_rpc_apiserver.py | 6 +- tests/rpc/test_rpc_telegram.py | 6 +- tests/strategy/test_interface.py | 4 +- tests/test_freqtradebot.py | 3 +- tests/test_persistence.py | 49 +--------- 12 files changed, 216 insertions(+), 130 deletions(-) create mode 100644 freqtrade/persistence/pairlock_middleware.py create mode 100644 tests/pairlist/test_pairlocks.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6112a599e..5a399801a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -345,23 +345,23 @@ class FreqtradeBot: whitelist = copy.deepcopy(self.active_pair_whitelist) if not whitelist: logger.info("Active pair whitelist is empty.") - else: - # Remove pairs for currently opened trades from the whitelist - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + return trades_created + # Remove pairs for currently opened trades from the whitelist + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) - if not whitelist: - logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") - else: - # Create entity and execute trade for each pair from whitelist - for pair in whitelist: - try: - trades_created += self.create_trade(pair) - except DependencyException as exception: - logger.warning('Unable to create trade for %s: %s', pair, exception) + if not whitelist: + logger.info("No currency pair in active pair whitelist, " + "but checking to sell open trades.") + return trades_created + # Create entity and execute trade for each pair from whitelist + for pair in whitelist: + try: + trades_created += self.create_trade(pair) + except DependencyException as exception: + logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: logger.debug("Found no buy signals for whitelisted currencies. " diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index e184e7d9a..35f2bc406 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import (Order, PairLock, Trade, clean_dry_run_db, cleanup_db, - init_db) +from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 22efed78d..62b033bdf 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -684,19 +684,7 @@ class PairLock(_DECL_BASE): f'lock_end_time={lock_end_time})') @staticmethod - def lock_pair(pair: str, until: datetime, reason: str = None) -> None: - lock = PairLock( - pair=pair, - lock_time=datetime.now(timezone.utc), - lock_end_time=until, - reason=reason, - active=True - ) - PairLock.session.add(lock) - PairLock.session.flush() - - @staticmethod - def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List['PairLock']: + def query_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> Query: """ Get all locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -713,41 +701,7 @@ class PairLock(_DECL_BASE): filters.append(PairLock.pair == pair) return PairLock.query.filter( *filters - ).all() - - @staticmethod - def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: - """ - Release all locks for this pair. - :param pair: Pair to unlock - :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() - """ - if not now: - now = datetime.now(timezone.utc) - - logger.info(f"Releasing all locks for {pair}.") - locks = PairLock.get_pair_locks(pair, now) - for lock in locks: - lock.active = False - PairLock.session.flush() - - @staticmethod - def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: - """ - :param pair: Pair to check for - :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() - """ - if not now: - now = datetime.now(timezone.utc) - - return PairLock.query.filter( - PairLock.pair == pair, - func.datetime(PairLock.lock_end_time) >= now, - # Only active locks - PairLock.active.is_(True), - ).first() is not None + ) def to_json(self) -> Dict[str, Any]: return { diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py new file mode 100644 index 000000000..ca2c31e36 --- /dev/null +++ b/freqtrade/persistence/pairlock_middleware.py @@ -0,0 +1,97 @@ + + +import logging +from datetime import datetime, timezone +from typing import List, Optional + +from freqtrade.persistence.models import PairLock + + +logger = logging.getLogger(__name__) + + +class PairLocks(): + """ + Pairlocks intermediate class + + """ + + use_db = True + locks: List[PairLock] = [] + + @staticmethod + def lock_pair(pair: str, until: datetime, reason: str = None) -> None: + lock = PairLock( + pair=pair, + lock_time=datetime.now(timezone.utc), + lock_end_time=until, + reason=reason, + active=True + ) + if PairLocks.use_db: + PairLock.session.add(lock) + PairLock.session.flush() + else: + PairLocks.locks.append(lock) + + @staticmethod + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: + """ + Get all currently active locks for this pair + :param pair: Pair to check for. Returns all current locks if pair is empty + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() + """ + if not now: + now = datetime.now(timezone.utc) + + if PairLocks.use_db: + return PairLock.query_pair_locks(pair, now).all() + else: + locks = [lock for lock in PairLocks.locks if ( + lock.lock_end_time > now + and lock.active is True + and (pair is None or lock.pair == pair) + )] + return locks + + @staticmethod + def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this pair. + :param pair: Pair to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + logger.info(f"Releasing all locks for {pair}.") + locks = PairLocks.get_pair_locks(pair, now) + for lock in locks: + lock.active = False + if PairLocks.use_db: + PairLock.session.flush() + + @staticmethod + def is_global_lock(now: Optional[datetime] = None) -> bool: + """ + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + return len(PairLocks.get_pair_locks('*', now)) > 0 + + @staticmethod + def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: + """ + :param pair: Pair to check for + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index de8bcaefb..10aaf56fa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +19,7 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import shorten_date -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State from freqtrade.strategy.interface import SellType @@ -604,7 +604,7 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - locks = PairLock.get_pair_locks(None) + locks = PairLocks.get_pair_locks(None) return { 'lock_count': len(locks), 'locks': [lock.to_json() for lock in locks] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e6256cafb..1c6aa535d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_next_date -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -288,7 +288,7 @@ class IStrategy(ABC): Needs to be timezone aware `datetime.now(timezone.utc)` :param reason: Optional string explaining why the pair was locked. """ - PairLock.lock_pair(pair, until, reason) + PairLocks.lock_pair(pair, until, reason) def unlock_pair(self, pair: str) -> None: """ @@ -297,7 +297,7 @@ class IStrategy(ABC): manually from within the strategy, to allow an easy way to unlock pairs. :param pair: Unlock pair to allow trading again """ - PairLock.unlock_pair(pair, datetime.now(timezone.utc)) + PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ @@ -312,10 +312,10 @@ class IStrategy(ABC): if not candle_date: # Simple call ... - return PairLock.is_pair_locked(pair, candle_date) + return PairLocks.is_pair_locked(pair, candle_date) else: lock_time = timeframe_to_next_date(self.timeframe, candle_date) - return PairLock.is_pair_locked(pair, lock_time) + return PairLocks.is_pair_locked(pair, lock_time) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/tests/pairlist/test_pairlocks.py b/tests/pairlist/test_pairlocks.py new file mode 100644 index 000000000..3ed7d643e --- /dev/null +++ b/tests/pairlist/test_pairlocks.py @@ -0,0 +1,81 @@ +from datetime import datetime, timedelta, timezone + +import arrow +import pytest + +from freqtrade.persistence import PairLocks +from freqtrade.persistence.models import PairLock + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks(use_db): + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + else: + PairLocks.use_db = False + + assert PairLocks.use_db == use_db + + pair = 'ETH/BTC' + assert not PairLocks.is_pair_locked(pair) + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLocks.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not PairLocks.is_pair_locked(pair) + # Unlocking a pair that's not locked should not raise an error + PairLocks.unlock_pair(pair) + + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + assert PairLocks.is_pair_locked(pair) + + # Get both locks from above + locks = PairLocks.get_pair_locks(None) + assert len(locks) == 2 + + # Unlock original pair + pair = 'ETH/BTC' + PairLocks.unlock_pair(pair) + assert not PairLocks.is_pair_locked(pair) + assert not PairLocks.is_global_lock() + + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + PairLocks.lock_pair(pair, lock_time) + + assert not PairLocks.is_pair_locked(pair) + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-10)) + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + + # Should not be locked after time expired + assert not PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + locks = PairLocks.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) + assert len(locks) == 1 + assert 'PairLock' in str(locks[0]) + + # Unlock all + PairLocks.unlock_pair(pair, lock_time + timedelta(minutes=-2)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + + # Global lock + PairLocks.lock_pair('*', lock_time) + assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + # Global lock also locks every pair seperately + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) + + if use_db: + assert len(PairLock.query.all()) > 0 + else: + # Nothing was pushed to the database + assert len(PairLock.query.all()) == 0 + # Reset use-db variable + PairLocks.use_db = True diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 34e959875..0dd15a777 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -12,7 +12,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -339,8 +339,8 @@ def test_api_locks(botclient): assert rc.json['lock_count'] == 0 assert rc.json['lock_count'] == len(rc.json['locks']) - PairLock.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') - PairLock.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') + PairLocks.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') + PairLocks.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c412313ad..f1246005f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -18,7 +18,7 @@ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State @@ -1047,8 +1047,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None msg_mock.reset_mock() freqtradebot.state = State.RUNNING - PairLock.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') - PairLock.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') + PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') + PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') telegram._locks(update=update, context=MagicMock()) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dc5cd47e7..96d4882da 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,7 +11,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -364,7 +364,7 @@ def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) # No lock should be present - assert len(PairLock.query.all()) == 0 + assert len(PairLocks.get_pair_locks(None)) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2a1b0c3cc..29df9c012 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,8 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, PairLock, Trade +from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 243da3396..4216565ac 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 import logging -from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import arrow @@ -9,7 +8,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, PairLock, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1159,49 +1158,3 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None - - -@pytest.mark.usefixtures("init_persistence") -def test_PairLock(default_conf): - # No lock should be present - assert len(PairLock.query.all()) == 0 - - pair = 'ETH/BTC' - assert not PairLock.is_pair_locked(pair) - PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) - # ETH/BTC locked for 4 minutes - assert PairLock.is_pair_locked(pair) - - # XRP/BTC should not be locked now - pair = 'XRP/BTC' - assert not PairLock.is_pair_locked(pair) - # Unlocking a pair that's not locked should not raise an error - PairLock.unlock_pair(pair) - - PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) - assert PairLock.is_pair_locked(pair) - - # Get both locks from above - locks = PairLock.get_pair_locks(None) - assert len(locks) == 2 - - # Unlock original pair - pair = 'ETH/BTC' - PairLock.unlock_pair(pair) - assert not PairLock.is_pair_locked(pair) - - pair = 'BTC/USDT' - # Lock until 14:30 - lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) - PairLock.lock_pair(pair, lock_time) - - assert not PairLock.is_pair_locked(pair) - assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) - assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) - - # Should not be locked after time expired - assert not PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=10)) - - locks = PairLock.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) - assert len(locks) == 1 - assert 'PairLock' in str(locks[0]) From 9c54c9a2bfdec85369c02d93d649f45e130ba72c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Oct 2020 07:36:25 +0100 Subject: [PATCH 15/63] Use correct timezone for tests --- tests/rpc/test_rpc_apiserver.py | 6 +++--- tests/strategy/test_interface.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dd15a777..7b4e2e153 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2,7 +2,7 @@ Unit test file for rpc/api_server.py """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock @@ -339,8 +339,8 @@ def test_api_locks(botclient): assert rc.json['lock_count'] == 0 assert rc.json['lock_count'] == len(rc.json['locks']) - PairLocks.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') - PairLocks.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') + PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') + PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 96d4882da..e87fb7182 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -362,13 +362,14 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> @pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) # No lock should be present assert len(PairLocks.get_pair_locks(None)) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) - strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime) # ETH/BTC locked for 4 minutes assert strategy.is_pair_locked(pair) From 6c913fa6179e93ba922a6a7ef62fec839391d485 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Oct 2020 07:37:07 +0100 Subject: [PATCH 16/63] Fix locking - should round before storing to have a consistent picture --- docs/strategy-customization.md | 2 +- freqtrade/freqtradebot.py | 18 ++++++++++-------- freqtrade/persistence/models.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 7 +++++-- tests/strategy/test_interface.py | 3 ++- tests/test_freqtradebot.py | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index c0506203f..6c7d78864 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -704,7 +704,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning - Locking pairs is not functioning during backtesting. + Locking pairs is not available during backtesting. #### Pair locking example diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5a399801a..ae46d335b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime +from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -19,10 +19,10 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade, cleanup_db, init_db +from freqtrade.persistence import Order, Trade, cleanup_db, init_db, PairLocks from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -72,6 +72,8 @@ class FreqtradeBot: self.wallets = Wallets(self.config, self.exchange) + PairLocks.timeframe = self.config['timeframe'] + self.pairlists = PairListManager(self.exchange, self.config) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) @@ -363,9 +365,9 @@ class FreqtradeBot: except DependencyException as exception: logger.warning('Unable to create trade for %s: %s', pair, exception) - if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. " - "Trying again...") + if not trades_created: + logger.debug("Found no buy signals for whitelisted currencies. " + "Trying again...") return trades_created @@ -937,7 +939,7 @@ class FreqtradeBot: self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') self._notify_sell(trade, "stoploss") return True @@ -1264,7 +1266,7 @@ class FreqtradeBot: Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') self._notify_sell(trade, order_type) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 62b033bdf..3c62a7268 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -694,7 +694,7 @@ class PairLock(_DECL_BASE): if not now: now = datetime.now(timezone.utc) - filters = [func.datetime(PairLock.lock_end_time) >= now, + filters = [PairLock.lock_end_time > now, # Only active locks PairLock.active.is_(True), ] if pair: diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index ca2c31e36..c1acc2423 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone from typing import List, Optional from freqtrade.persistence.models import PairLock +from freqtrade.exchange import timeframe_to_next_date logger = logging.getLogger(__name__) @@ -19,12 +20,14 @@ class PairLocks(): use_db = True locks: List[PairLock] = [] + timeframe: str = '' + @staticmethod def lock_pair(pair: str, until: datetime, reason: str = None) -> None: lock = PairLock( pair=pair, lock_time=datetime.now(timezone.utc), - lock_end_time=until, + lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until), reason=reason, active=True ) @@ -49,7 +52,7 @@ class PairLocks(): return PairLock.query_pair_locks(pair, now).all() else: locks = [lock for lock in PairLocks.locks if ( - lock.lock_end_time > now + lock.lock_end_time >= now and lock.active is True and (pair is None or lock.pair == pair) )] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e87fb7182..7cf9a0624 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -388,7 +388,8 @@ def test_is_pair_locked(default_conf): pair = 'BTC/USDT' # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) - strategy.lock_pair(pair, lock_time) + # Subtract 2 seconds, as locking rounds up to the next candle. + strategy.lock_pair(pair, lock_time - timedelta(seconds=2)) assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 29df9c012..1f5b3ecaa 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence import Order, Trade from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State From 5c8779b1550ab2cd8ad4649420833e5c7fb592e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Oct 2020 08:09:18 +0100 Subject: [PATCH 17/63] Sort imports --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ae46d335b..7416d8236 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade, cleanup_db, init_db, PairLocks +from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index c1acc2423..44fc228f6 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -1,11 +1,9 @@ - - import logging from datetime import datetime, timezone from typing import List, Optional -from freqtrade.persistence.models import PairLock from freqtrade.exchange import timeframe_to_next_date +from freqtrade.persistence.models import PairLock logger = logging.getLogger(__name__) @@ -13,8 +11,9 @@ logger = logging.getLogger(__name__) class PairLocks(): """ - Pairlocks intermediate class - + Pairlocks middleware class + Abstracts the database layer away so it becomes optional - which will be necessary to support + backtesting and hyperopt in the future. """ use_db = True @@ -43,7 +42,7 @@ class PairLocks(): Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() + defaults to datetime.now(timezone.utc) """ if not now: now = datetime.now(timezone.utc) From 72f61f4682e926a33018e6767b005a1bd78236c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Oct 2020 10:08:24 +0100 Subject: [PATCH 18/63] Remove optional, now is not optional --- freqtrade/persistence/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3c62a7268..7e6d967c1 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -684,15 +684,12 @@ class PairLock(_DECL_BASE): f'lock_end_time={lock_end_time})') @staticmethod - def query_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> Query: + def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ Get all locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() """ - if not now: - now = datetime.now(timezone.utc) filters = [PairLock.lock_end_time > now, # Only active locks From 28d6c3419b90c2dad1105f6631a42903cba41edc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Oct 2020 20:01:23 +0100 Subject: [PATCH 19/63] Fix random test failure in pairlocks --- tests/pairlist/test_pairlocks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pairlist/test_pairlocks.py b/tests/pairlist/test_pairlocks.py index 3ed7d643e..0b6b89717 100644 --- a/tests/pairlist/test_pairlocks.py +++ b/tests/pairlist/test_pairlocks.py @@ -10,6 +10,7 @@ from freqtrade.persistence.models import PairLock @pytest.mark.parametrize('use_db', (False, True)) @pytest.mark.usefixtures("init_persistence") def test_PairLocks(use_db): + PairLocks.timeframe = '5m' # No lock should be present if use_db: assert len(PairLock.query.all()) == 0 From 5cb3735a57448c6fd83d9ffcb290f44d0e8616b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 07:58:55 +0100 Subject: [PATCH 20/63] Improve error when hyperopt-loss-function is missing --- freqtrade/commands/cli_options.py | 4 ++-- freqtrade/constants.py | 3 +++ freqtrade/resolvers/hyperopt_resolver.py | 9 ++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 8ea945ae7..4769bccde 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -4,6 +4,7 @@ Definition of cli arguments used in arguments.py from argparse import ArgumentTypeError from freqtrade import __version__, constants +from freqtrade.constants import HYPEROPT_LOSS_BUILTIN def check_int_positive(value: str) -> int: @@ -257,8 +258,7 @@ AVAILABLE_CLI_OPTIONS = { help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' - 'ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, ' - 'SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily.', + f'{", ".join(HYPEROPT_LOSS_BUILTIN)}', metavar='NAME', ), "hyperoptexportfilename": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8e92d3ed8..dc5384f6f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -20,6 +20,9 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERBOOK_SIDES = ['ask', 'bid'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] +HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', + 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', + 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 328dc488b..8327a4d13 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,7 +7,7 @@ import logging from pathlib import Path from typing import Dict -from freqtrade.constants import USERPATH_HYPEROPTS +from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss @@ -72,8 +72,11 @@ class HyperOptLossResolver(IResolver): hyperoptloss_name = config.get('hyperopt_loss') if not hyperoptloss_name: - raise OperationalException("No Hyperopt loss set. Please use `--hyperopt-loss` to " - "specify the Hyperopt-Loss class to use.") + raise OperationalException( + "No Hyperopt loss set. Please use `--hyperopt-loss` to " + "specify the Hyperopt-Loss class to use.\n" + f"Built-in Hyperopt-loss-functions are: {', '.join(HYPEROPT_LOSS_BUILTIN)}" + ) hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name, config, kwargs={}, extra_dir=config.get('hyperopt_path')) From e1e2829ef3b5b1cbd77cac106039ceddda8ab621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 14:36:19 +0100 Subject: [PATCH 21/63] Improve and refactor hyperopt tests --- tests/optimize/conftest.py | 51 +++++++ tests/optimize/test_hyperopt.py | 206 +--------------------------- tests/optimize/test_hyperoptloss.py | 165 ++++++++++++++++++++++ 3 files changed, 219 insertions(+), 203 deletions(-) create mode 100644 tests/optimize/conftest.py create mode 100644 tests/optimize/test_hyperoptloss.py diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py new file mode 100644 index 000000000..f06b0ecd3 --- /dev/null +++ b/tests/optimize/conftest.py @@ -0,0 +1,51 @@ +from copy import deepcopy +from datetime import datetime +from pathlib import Path + +import pandas as pd +import pytest + +from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.strategy.interface import SellType +from tests.conftest import patch_exchange + + +@pytest.fixture(scope='function') +def hyperopt_conf(default_conf): + hyperconf = deepcopy(default_conf) + hyperconf.update({ + 'hyperopt': 'DefaultHyperOpt', + 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', + 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), + 'epochs': 1, + 'timerange': None, + 'spaces': ['default'], + 'hyperopt_jobs': 1, + }) + return hyperconf + + +@pytest.fixture(scope='function') +def hyperopt(hyperopt_conf, mocker): + + patch_exchange(mocker) + return Hyperopt(hyperopt_conf) + + +@pytest.fixture(scope='function') +def hyperopt_results(): + return pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [-0.1, 0.2, 0.3], + 'profit_abs': [-0.2, 0.4, 0.6], + 'trade_duration': [10, 30, 10], + 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], + 'close_date': + [ + datetime(2019, 1, 1, 9, 26, 3, 478039), + datetime(2019, 2, 1, 9, 26, 3, 478039), + datetime(2019, 3, 1, 9, 26, 3, 478039) + ] + } + ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 41ad6f5de..82be894d3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -2,7 +2,6 @@ import locale import logging import re -from copy import deepcopy from datetime import datetime from pathlib import Path from typing import Dict, List @@ -17,58 +16,15 @@ from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt -from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from .hyperopts.default_hyperopt import DefaultHyperOpt -@pytest.fixture(scope='function') -def hyperopt_conf(default_conf): - hyperconf = deepcopy(default_conf) - hyperconf.update({ - 'hyperopt': 'DefaultHyperOpt', - 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', - 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), - 'epochs': 1, - 'timerange': None, - 'spaces': ['default'], - 'hyperopt_jobs': 1, - }) - return hyperconf - - -@pytest.fixture(scope='function') -def hyperopt(hyperopt_conf, mocker): - - patch_exchange(mocker) - return Hyperopt(hyperopt_conf) - - -@pytest.fixture(scope='function') -def hyperopt_results(): - return pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [-0.1, 0.2, 0.3], - 'profit_abs': [-0.2, 0.4, 0.6], - 'trade_duration': [10, 30, 10], - 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], - 'close_date': - [ - datetime(2019, 1, 1, 9, 26, 3, 478039), - datetime(2019, 2, 1, 9, 26, 3, 478039), - datetime(2019, 3, 1, 9, 26, 3, 478039) - ] - } - ) - - # Functions for recurrent object patching def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: """ @@ -230,32 +186,6 @@ def test_hyperoptresolver_noname(default_conf): HyperOptResolver.load_hyperopt(default_conf) -def test_hyperoptlossresolver_noname(default_conf): - with pytest.raises(OperationalException, - match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " - "the Hyperopt-Loss class to use."): - HyperOptLossResolver.load_hyperoptloss(default_conf) - - -def test_hyperoptlossresolver(mocker, default_conf) -> None: - - hl = ShortTradeDurHyperOptLoss - mocker.patch( - 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', - MagicMock(return_value=hl) - ) - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - x = HyperOptLossResolver.load_hyperoptloss(default_conf) - assert hasattr(x, "hyperopt_loss_function") - - -def test_hyperoptlossresolver_wrongname(default_conf) -> None: - default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) - - with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): - HyperOptLossResolver.load_hyperoptloss(default_conf) - - def test_start_not_installed(mocker, default_conf, import_fails) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) @@ -269,7 +199,8 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None: '--hyperopt', 'DefaultHyperOpt', '--hyperopt-path', str(Path(__file__).parent / "hyperopts"), - '--epochs', '5' + '--epochs', '5', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', ] pargs = get_args(args) @@ -337,137 +268,6 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog) -def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: - hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over > correct - assert under > correct - - -def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: - resultsb = hyperopt_results.copy() - resultsb.loc[1, 'trade_duration'] = 20 - - hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) - longer = hl.hyperopt_loss_function(hyperopt_results, 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - shorter = hl.hyperopt_loss_function(resultsb, 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert shorter < longer - - -def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.total_epochs = 2 diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py new file mode 100644 index 000000000..63012ee48 --- /dev/null +++ b/tests/optimize/test_hyperoptloss.py @@ -0,0 +1,165 @@ +from datetime import datetime +from unittest.mock import MagicMock + +import pytest + +from freqtrade.exceptions import OperationalException +from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver + + +def test_hyperoptlossresolver_noname(default_conf): + with pytest.raises(OperationalException, + match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " + "the Hyperopt-Loss class to use."): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + +def test_hyperoptlossresolver(mocker, default_conf) -> None: + + hl = ShortTradeDurHyperOptLoss + mocker.patch( + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', + MagicMock(return_value=hl) + ) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) + x = HyperOptLossResolver.load_hyperoptloss(default_conf) + assert hasattr(x, "hyperopt_loss_function") + + +def test_hyperoptlossresolver_wrongname(default_conf) -> None: + default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + +def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over > correct + assert under > correct + + +def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: + resultsb = hyperopt_results.copy() + resultsb.loc[1, 'trade_duration'] = 20 + + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + longer = hl.hyperopt_loss_function(hyperopt_results, 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + shorter = hl.hyperopt_loss_function(resultsb, 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert shorter < longer + + +def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct From ffa67979586447479e4fdf6ccdd03559f0e7f2c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 16:29:08 +0100 Subject: [PATCH 22/63] Improve test coverage --- tests/test_arguments.py | 3 +++ tests/test_wallets.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 2af36277b..315d47876 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -249,6 +249,9 @@ def test_check_int_positive() -> None: with pytest.raises(argparse.ArgumentTypeError): check_int_positive('0') + with pytest.raises(argparse.ArgumentTypeError): + check_int_positive(0) + with pytest.raises(argparse.ArgumentTypeError): check_int_positive('3.5') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 450dabc4d..b7aead0c4 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -74,6 +74,10 @@ def test_sync_wallet_at_boot(mocker, default_conf): freqtrade.wallets.update() assert update_mock.call_count == 1 + assert freqtrade.wallets.get_free('NOCURRENCY') == 0 + assert freqtrade.wallets.get_used('NOCURRENCY') == 0 + assert freqtrade.wallets.get_total('NOCURRENCY') == 0 + def test_sync_wallet_missing_data(mocker, default_conf): default_conf['dry_run'] = False From 86725847edb3ea10ec6922cd87498de1d992b729 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 16:58:39 +0100 Subject: [PATCH 23/63] Add explicit test for check_int_nonzero --- tests/test_arguments.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 315d47876..e2a1ae53c 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.commands import Arguments -from freqtrade.commands.cli_options import check_int_positive +from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive # Parse common command-line-arguments. Used for all tools @@ -257,3 +257,23 @@ def test_check_int_positive() -> None: with pytest.raises(argparse.ArgumentTypeError): check_int_positive('DeadBeef') + + +def test_check_int_nonzero() -> None: + assert check_int_nonzero('3') == 3 + assert check_int_nonzero('1') == 1 + assert check_int_nonzero('100') == 100 + + assert check_int_nonzero('-2') == -2 + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('0') + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero(0) + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('3.5') + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('DeadBeef') From 19fcbc92a7d0fe4570f34c9ed5cdb9394373aa15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 07:43:40 +0100 Subject: [PATCH 24/63] Remove stake-currency for download-data - it's not needed --- freqtrade/commands/data_commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 7102eee38..df4c52de0 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -35,6 +35,9 @@ def start_download_data(args: Dict[str, Any]) -> None: if 'timerange' in config: timerange = timerange.parse_timerange(config['timerange']) + # Remove stake-currency to skip checks which are not relevant for datadownload + config['stake_currency'] = '' + if 'pairs' not in config: raise OperationalException( "Downloading data requires a list of pairs. " From f4d39f2a12538a3e75d921f737b961ee0ee29b38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 07:44:03 +0100 Subject: [PATCH 25/63] Improve test coverage of deploy_commands --- freqtrade/commands/deploy_commands.py | 2 +- tests/commands/test_commands.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 0a49c55de..a0105e140 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -133,7 +133,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " - "Please choose another Strategy Name.") + "Please choose another Hyperopt Name.") deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 713386a8e..6861e0cd9 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -476,6 +476,12 @@ def test_start_new_strategy(mocker, caplog): assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0] assert log_has_re("Writing strategy to .*", caplog) + mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r".* already exists. Please choose another Strategy Name\."): + start_new_strategy(get_args(args)) + def test_start_new_strategy_DefaultStrat(mocker, caplog): args = [ @@ -512,6 +518,12 @@ def test_start_new_hyperopt(mocker, caplog): assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] assert log_has_re("Writing hyperopt to .*", caplog) + mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r".* already exists. Please choose another Hyperopt Name\."): + start_new_hyperopt(get_args(args)) + def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): args = [ From d8ff79a2faa69ac714d5898ca1b1386cf70afacf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 07:54:42 +0100 Subject: [PATCH 26/63] Improve tests of list commands --- tests/commands/test_commands.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 6861e0cd9..305b6b376 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -435,6 +435,16 @@ def test_list_markets(mocker, markets, capsys): assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError)) + # Test --one-column + args = [ + "list-markets", + '--config', 'config.json.example', + "--one-column" + ] + with pytest.raises(OperationalException, match=r"Cannot get markets.*"): + start_list_markets(get_args(args), False) + def test_create_datadir_failed(caplog): @@ -707,6 +717,7 @@ def test_start_list_strategies(mocker, caplog, capsys): "list-strategies", "--strategy-path", str(Path(__file__).parent.parent / "strategy" / "strats"), + '--no-color', ] pargs = get_args(args) # pargs['config'] = None From 3ca97223f21c820363386aa0a68570388660b31c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 08:09:50 +0100 Subject: [PATCH 27/63] Improve test for test_pairlist --- tests/commands/test_commands.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 305b6b376..a392b74cf 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -792,6 +792,25 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): assert re.match(r"Pairs for .*", captured.out) assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out) + args = [ + 'test-pairlist', + '-c', 'config.json.example', + '--one-column', + ] + start_test_pairlist(get_args(args)) + captured = capsys.readouterr() + assert re.match(r"ETH/BTC\nTKN/BTC\nBLK/BTC\nLTC/BTC\nXRP/BTC\n", captured.out) + + args = [ + 'test-pairlist', + '-c', 'config.json.example', + '--print-json', + ] + start_test_pairlist(get_args(args)) + captured = capsys.readouterr() + assert re.match(r'Pairs for BTC: \n\["ETH/BTC","TKN/BTC","BLK/BTC","LTC/BTC","XRP/BTC"\]\n', + captured.out) + def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): mocker.patch( From 38fc5d680b937c7aff9ed148ecab34804dd9dfa0 Mon Sep 17 00:00:00 2001 From: Matthias Spiller Date: Sat, 31 Oct 2020 10:31:58 +0000 Subject: [PATCH 28/63] Enable usage of devcontainer for macOS users --- .devcontainer/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 7b5e64609..20ec247d1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -7,8 +7,8 @@ services: dockerfile: ".devcontainer/Dockerfile" volumes: # Allow git usage within container - - "/home/${USER}/.ssh:/home/ftuser/.ssh:ro" - - "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro" + - "${HOME}/.ssh:/home/ftuser/.ssh:ro" + - "${HOME}/.gitconfig:/home/ftuser/.gitconfig:ro" - ..:/freqtrade:cached # Persist bash-history - freqtrade-vscode-server:/home/ftuser/.vscode-server From 78874fa86573f810d0c57e248651fc8aa1d80286 Mon Sep 17 00:00:00 2001 From: Matthias Spiller Date: Sat, 31 Oct 2020 10:53:51 +0000 Subject: [PATCH 29/63] informative_pairs does not honor dataformat --- freqtrade/data/dataprovider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 07dd94fc1..3ca38e865 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -88,7 +88,8 @@ class DataProvider: """ return load_pair_history(pair=pair, timeframe=timeframe or self._config['timeframe'], - datadir=self._config['datadir'] + datadir=self._config['datadir'], + data_format=self._config.get('dataformat_ohlcv', 'json') ) def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: From 0d11f0bd75da23e2c285778a435cb7cf2f65bff4 Mon Sep 17 00:00:00 2001 From: Matthias Spiller Date: Sat, 31 Oct 2020 11:45:46 +0000 Subject: [PATCH 30/63] Add unit test for hdf5 dataformat for informative pairs --- tests/data/test_dataprovider.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index a64dce908..a3c57a77b 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -52,6 +52,31 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history): assert historymock.call_args_list[0][1]["timeframe"] == "5m" +def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history): + hdf5loadmock = MagicMock(return_value=ohlcv_history) + jsonloadmock = MagicMock(return_value=ohlcv_history) + mocker.patch("freqtrade.data.history.hdf5datahandler.HDF5DataHandler._ohlcv_load", hdf5loadmock) + mocker.patch("freqtrade.data.history.jsondatahandler.JsonDataHandler._ohlcv_load", jsonloadmock) + + default_conf["runmode"] = RunMode.BACKTEST + exchange = get_patched_exchange(mocker, default_conf) + dp = DataProvider(default_conf, exchange) + data = dp.historic_ohlcv("UNITTEST/BTC", "5m") + assert isinstance(data, DataFrame) + hdf5loadmock.assert_not_called() + jsonloadmock.assert_called_once() + + # Swiching to dataformat hdf5 + hdf5loadmock.reset_mock() + jsonloadmock.reset_mock() + default_conf["dataformat_ohlcv"] = "hdf5" + dp = DataProvider(default_conf, exchange) + data = dp.historic_ohlcv("UNITTEST/BTC", "5m") + assert isinstance(data, DataFrame) + hdf5loadmock.assert_called_once() + jsonloadmock.assert_not_called() + + def test_get_pair_dataframe(mocker, default_conf, ohlcv_history): default_conf["runmode"] = RunMode.DRY_RUN timeframe = default_conf["timeframe"] From e73203acb85b16ef79bfebac6b0275ca72ddf8f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 1 Nov 2020 10:51:07 +0100 Subject: [PATCH 31/63] FIx bug with dmmp --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 47bb9edd9..883f7338c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -340,7 +340,7 @@ class Backtesting: # max_open_trades must be respected # don't open on the last row if ((position_stacking or len(open_trades[pair]) == 0) - and max_open_trades > 0 and open_trade_count_start < max_open_trades + and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1): # Enter trade From 81fb0c572614805a393fa5a10ad4389877f65498 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:54:57 +0000 Subject: [PATCH 32/63] Bump numpy from 1.19.2 to 1.19.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.2 to 1.19.3. - [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.19.2...v1.19.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d2017beb..e2687539e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.2 +numpy==1.19.3 pandas==1.1.3 ccxt==1.36.85 From 6c3753ac7f64c1d61e8b4ecfcace66db9c5d5946 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:55:07 +0000 Subject: [PATCH 33/63] Bump mkdocs-material from 6.1.0 to 6.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.0 to 6.1.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.0...6.1.2) Signed-off-by: dependabot[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 f30710a1f..47f0eff1c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.0 +mkdocs-material==6.1.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 21b22760a7087409cf3f91ea860f29329d831745 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:55:12 +0000 Subject: [PATCH 34/63] Bump pytest from 6.1.1 to 6.1.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.1 to 6.1.2. - [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/6.1.1...6.1.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 916bb2ec2..1c96a880a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.790 -pytest==6.1.1 +pytest==6.1.2 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From aed44ef6b36f071d784bda57c9f910e90c4d953a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 07:46:28 +0000 Subject: [PATCH 35/63] Bump pandas from 1.1.3 to 1.1.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.3 to 1.1.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.3...v1.1.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e2687539e..05520d4d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.3 -pandas==1.1.3 +pandas==1.1.4 ccxt==1.36.85 aiohttp==3.7.1 From 74d8a985e22d0b6d2881b4d3dcd989186f3ad7b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 08:44:26 +0000 Subject: [PATCH 36/63] Bump aiohttp from 3.7.1 to 3.7.2 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.1 to 3.7.2. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.1...v3.7.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05520d4d9..3df54ad06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.19.3 pandas==1.1.4 ccxt==1.36.85 -aiohttp==3.7.1 +aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 From d56da41679f022365ab6ac913989f09d88ff920d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 13:50:07 +0000 Subject: [PATCH 37/63] Bump ccxt from 1.36.85 to 1.37.14 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.36.85 to 1.37.14. - [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.36.85...1.37.14) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3df54ad06..afe48171c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.3 pandas==1.1.4 -ccxt==1.36.85 +ccxt==1.37.14 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From cf89a773da498915cb7f08e590d1d061c3d46bc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 07:34:21 +0100 Subject: [PATCH 38/63] Standardize trade api outputs there should be no difference between current_profit and close_profit it's always profit, and the information if it's a closed trade is available elsewhere --- freqtrade/persistence/models.py | 9 +++++++-- freqtrade/rpc/rpc.py | 13 +++++++------ tests/rpc/test_rpc.py | 8 +++++++- tests/rpc/test_rpc_apiserver.py | 7 +++++++ tests/test_persistence.py | 8 ++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7e6d967c1..3019d3d5f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -295,8 +295,13 @@ class Trade(_DECL_BASE): tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, - 'close_profit': self.close_profit, - 'close_profit_abs': self.close_profit_abs, + 'close_profit': self.close_profit, # Deprecated + 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, + 'close_profit_abs': self.close_profit_abs, # Deprecated + + 'profit_ratio': self.close_profit, + 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, + 'profit_abs': self.close_profit_abs, 'sell_reason': self.sell_reason, 'sell_order_status': self.sell_order_status, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 10aaf56fa..4370fe897 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -152,17 +152,18 @@ class RPC: 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() trade_dict.update(dict( base_currency=self._freqtrade.config['stake_currency'], close_profit=trade.close_profit if trade.close_profit is not None else None, - close_profit_pct=fmt_close_profit, current_rate=current_rate, - current_profit=current_profit, - current_profit_pct=round(current_profit * 100, 2), - current_profit_abs=current_profit_abs, + current_profit=current_profit, # Deprectated + current_profit_pct=round(current_profit * 100, 2), # Deprectated + current_profit_abs=current_profit_abs, # Deprectated + profit_ratio=current_profit, + profit_pct=round(current_profit * 100, 2), + profit_abs=current_profit_abs, + stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 977dfbc20..4a4f3053e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -70,7 +70,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'max_rate': ANY, 'strategy': ANY, 'ticker_interval': ANY, - 'timeframe': ANY, + 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, 'close_date_hum': None, @@ -87,6 +87,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_profit_abs': -4.09e-06, + 'profit_ratio': -0.00408133, + 'profit_pct': -0.41, + 'profit_abs': -4.09e-06, 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, @@ -152,6 +155,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_profit': ANY, 'current_profit_pct': ANY, 'current_profit_abs': ANY, + 'profit_ratio': ANY, + 'profit_pct': ANY, + 'profit_abs': ANY, 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7b4e2e153..fd5d2fce2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -639,6 +639,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_profit_abs': -4.09e-06, + 'profit_ratio': -0.00408133, + 'profit_pct': -0.41, + 'profit_abs': -4.09e-06, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', @@ -791,8 +794,12 @@ def test_api_forcebuy(botclient, mocker, fee): 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'close_profit': None, + 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate_requested': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4216565ac..b2d2b716c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -816,7 +816,11 @@ def test_to_json(default_conf, fee): 'amount_requested': 123.0, 'stake_amount': 0.001, 'close_profit': None, + 'close_profit_pct': None, 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, 'sell_reason': None, 'sell_order_status': None, 'stop_loss': None, @@ -880,7 +884,11 @@ def test_to_json(default_conf, fee): 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'close_profit': None, + 'close_profit_pct': None, 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, 'close_rate_requested': None, 'fee_close': 0.0025, 'fee_close_cost': None, From d1dab2328379446b502287fdbba0deb842fcb68a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 08:25:47 +0100 Subject: [PATCH 39/63] Remove deprecated api fields --- freqtrade/persistence/models.py | 3 --- freqtrade/rpc/rpc.py | 1 - freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc.py | 6 ------ tests/rpc/test_rpc_apiserver.py | 7 ------- tests/test_persistence.py | 6 ------ 6 files changed, 2 insertions(+), 25 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3019d3d5f..8160ffbbf 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -270,7 +270,6 @@ class Trade(_DECL_BASE): 'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None, 'stake_amount': round(self.stake_amount, 8), 'strategy': self.strategy, - 'ticker_interval': self.timeframe, # DEPRECATED 'timeframe': self.timeframe, 'fee_open': self.fee_open, @@ -305,7 +304,6 @@ class Trade(_DECL_BASE): 'sell_reason': self.sell_reason, 'sell_order_status': self.sell_order_status, - 'stop_loss': self.stop_loss, # Deprecated - should not be used 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, @@ -314,7 +312,6 @@ class Trade(_DECL_BASE): if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, - 'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used 'initial_stop_loss_abs': self.initial_stop_loss, 'initial_stop_loss_ratio': (self.initial_stop_loss_pct if self.initial_stop_loss_pct else None), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4370fe897..efeb361ae 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -110,7 +110,6 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), - 'ticker_interval': config['timeframe'], # DEPRECATED 'timeframe': config['timeframe'], 'timeframe_ms': timeframe_to_msecs(config['timeframe']), 'timeframe_min': timeframe_to_minutes(config['timeframe']), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3dcb7ab72..9bbae871d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -251,14 +251,14 @@ class Telegram(RPC): if r['close_profit_pct'] is not None else ""), "*Current Profit:* `{current_profit_pct:.2f}%`", ] - if (r['stop_loss'] != r['initial_stop_loss'] + if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_pct'] is not None): # Adding initial stoploss only if it is different from stoploss lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` " "`({initial_stop_loss_pct:.2f}%)`") # Adding stoploss and stoploss percentage only if it is not None - lines.append("*Stoploss:* `{stop_loss:.8f}` " + + lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else "")) lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " "`({stoploss_current_dist_pct:.2f}%)`") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4a4f3053e..23ca53e53 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -69,7 +69,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, - 'ticker_interval': ANY, 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, @@ -90,14 +89,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': -0.00408133, 'profit_pct': -0.41, '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': 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, @@ -137,7 +134,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, - 'ticker_interval': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, @@ -158,14 +154,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, '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': 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, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index fd5d2fce2..80de839e4 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -360,7 +360,6 @@ def test_api_show_config(botclient, mocker): assert_response(rc) assert 'dry_run' in rc.json assert rc.json['exchange'] == 'bittrex' - assert rc.json['ticker_interval'] == '5m' assert rc.json['timeframe'] == '5m' assert rc.json['timeframe_ms'] == 300000 assert rc.json['timeframe_min'] == 5 @@ -650,14 +649,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'open_rate': 1.098e-05, 'pair': 'ETH/BTC', 'stake_amount': 0.001, - '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': 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, @@ -685,7 +682,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', - 'ticker_interval': 5, 'timeframe': 5, 'exchange': 'bittrex', }] @@ -782,14 +778,12 @@ def test_api_forcebuy(botclient, mocker, fee): 'open_rate': 0.245441, 'pair': 'ETH/ETH', 'stake_amount': 1, - 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, @@ -815,7 +809,6 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b2d2b716c..41b99b34f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -823,21 +823,18 @@ def test_to_json(default_conf, fee): 'profit_abs': None, 'sell_reason': None, 'sell_order_status': None, - 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_ratio': None, 'stop_loss_pct': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'min_rate': None, 'max_rate': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } @@ -872,14 +869,12 @@ def test_to_json(default_conf, fee): 'amount': 100.0, 'amount_requested': 101.0, 'stake_amount': 0.001, - 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, @@ -905,7 +900,6 @@ def test_to_json(default_conf, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } From b58d6d38b5e080d7bfcf06d9813f55f55bd55e69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 08:34:12 +0100 Subject: [PATCH 40/63] Use correct fields in telegram --- freqtrade/rpc/telegram.py | 7 +++---- tests/rpc/test_rpc_telegram.py | 11 ++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9bbae871d..31ec33b63 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -247,14 +247,13 @@ class Telegram(RPC): "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", - ("*Close Profit:* `{close_profit_pct}`" - if r['close_profit_pct'] is not None else ""), - "*Current Profit:* `{current_profit_pct:.2f}%`", + ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") + + "`{profit_pct:.2f}%`", ] if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_pct'] is not None): # Adding initial stoploss only if it is different from stoploss - lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` " + lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " "`({initial_stop_loss_pct:.2f}%)`") # Adding stoploss and stoploss percentage only if it is not None diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f1246005f..8e81db106 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -163,16 +163,17 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'amount': 90.99181074, 'stake_amount': 90.99181074, 'close_profit_pct': None, - 'current_profit': -0.0059, - 'current_profit_pct': -0.59, - 'initial_stop_loss': 1.098e-05, - 'stop_loss': 1.099e-05, + 'profit': -0.0059, + 'profit_pct': -0.59, + 'initial_stop_loss_abs': 1.098e-05, + 'stop_loss_abs': 1.099e-05, 'sell_order_status': None, 'initial_stop_loss_pct': -0.05, 'stoploss_current_dist': 1e-08, 'stoploss_current_dist_pct': -0.02, 'stop_loss_pct': -0.01, - 'open_order': '(limit buy rem=0.00000000)' + 'open_order': '(limit buy rem=0.00000000)', + 'is_open': True }]), _status_table=status_table, _send_msg=msg_mock From 7d2bd00f0ca421857ba475838269397bc64ec536 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 09:23:07 +0100 Subject: [PATCH 41/63] Update forgotten arrow.timestamp occurance --- tests/exchange/test_exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index af08d753e..e4452a83c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1302,7 +1302,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): raise TimeoutError() exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error) - ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + ret = exchange.get_historic_ohlcv(pair, "5m", int( + (arrow.utcnow().int_timestamp - since) * 1000)) assert log_has_re(r"Async code raised an exception: .*", caplog) From 2af1c80fd536a94aca53f45e5cd0f3c2f8a69781 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Nov 2020 11:26:02 +0100 Subject: [PATCH 42/63] Convert _rpc_show_config to static method --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 20 ++++++++++++-------- freqtrade/rpc/telegram.py | 3 ++- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 3 ++- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be21179ad..7f4773d57 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -329,7 +329,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return jsonify(self._rpc_show_config(self._config)) + return jsonify(RPC._rpc_show_config(self._config, self._freqtrade.state)) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index efeb361ae..888dc11ec 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -93,7 +93,8 @@ class RPC: def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_show_config(self, config) -> Dict[str, Any]: + @staticmethod + def _rpc_show_config(config, botstate: State) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive @@ -104,21 +105,24 @@ class RPC: 'stake_currency': config['stake_currency'], 'stake_amount': config['stake_amount'], 'max_open_trades': config['max_open_trades'], - 'minimal_roi': config['minimal_roi'].copy(), - 'stoploss': config['stoploss'], - 'trailing_stop': config['trailing_stop'], + 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, + 'stoploss': config.get('stoploss'), + 'trailing_stop': config.get('trailing_stop'), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), - 'timeframe': config['timeframe'], - 'timeframe_ms': timeframe_to_msecs(config['timeframe']), - 'timeframe_min': timeframe_to_minutes(config['timeframe']), + 'timeframe': config.get('timeframe'), + 'timeframe_ms': timeframe_to_msecs(config['timeframe'] + ) if 'timeframe' in config else '', + 'timeframe_min': timeframe_to_minutes(config['timeframe'] + ) if 'timeframe' in config else '', '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) if self._freqtrade else '', + 'state': str(botstate), + 'runmode': config['runmode'].value } return val diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 31ec33b63..31d5bbfbd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -775,7 +775,8 @@ class Telegram(RPC): :param update: message update :return: None """ - val = self._rpc_show_config(self._freqtrade.config) + val = RPC._rpc_show_config(self._freqtrade.config, self._freqtrade.state) + if val['trailing_stop']: sl_info = ( f"*Initial Stoploss:* `{val['stoploss']}`\n" diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 80de839e4..0dc43474f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -14,7 +14,7 @@ from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer -from freqtrade.state import State +from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -26,7 +26,7 @@ _TEST_PASS = "SuperSecurePassword1!" def botclient(default_conf, mocker): setup_logging_pre() setup_logging(default_conf) - + default_conf['runmode'] = RunMode.DRY_RUN default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": 8080, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8e81db106..7885a251d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -21,7 +21,7 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only -from freqtrade.state import State +from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellType from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, patch_get_signal, patch_whitelist) @@ -1309,6 +1309,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) + default_conf['runmode'] = RunMode.DRY_RUN freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) From 5243214a36a880a1ee830e9fe1b89bf1fcece92f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:29 +0000 Subject: [PATCH 43/63] Bump mkdocs-material from 6.1.2 to 6.1.4 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.2 to 6.1.4. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.2...6.1.4) Signed-off-by: dependabot[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 47f0eff1c..f034b0b36 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.2 +mkdocs-material==6.1.4 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 42d9e3a28f788e2fd7f59f42f620d6a991dc6ed2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:40 +0000 Subject: [PATCH 44/63] Bump numpy from 1.19.3 to 1.19.4 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.3 to 1.19.4. - [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.19.3...v1.19.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index afe48171c..818741207 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.3 +numpy==1.19.4 pandas==1.1.4 ccxt==1.37.14 From 6063f2f91f319593085fe0bab300df638fd08faf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:49 +0000 Subject: [PATCH 45/63] Bump questionary from 1.7.0 to 1.8.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.7.0 to 1.8.0. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.7.0...1.8.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index afe48171c..2789ca16a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.7.0 +questionary==1.8.0 prompt-toolkit==3.0.8 From 88b2f3f0d1b914f76e1a9921b3547a81d2040ea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:51 +0000 Subject: [PATCH 46/63] Bump scipy from 1.5.3 to 1.5.4 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.3 to 1.5.4. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.3...v1.5.4) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 5b68c1ea1..7e480b8c9 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.3 +scipy==1.5.4 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 From 59e846d554f36d1eb126f5ae0d0e52ad22a0a6af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 07:48:14 +0000 Subject: [PATCH 47/63] Bump ccxt from 1.37.14 to 1.37.41 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.37.14 to 1.37.41. - [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.37.14...1.37.41) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37a57e3f5..4622afa15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.14 +ccxt==1.37.41 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 4eb96cfc4f24bb2a7e5c3b93f4374b2d8a14c088 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Nov 2020 06:51:45 +0100 Subject: [PATCH 48/63] Allow locks to be gathered even when the bot is stopped --- freqtrade/rpc/rpc.py | 2 -- tests/rpc/test_rpc_telegram.py | 7 ------- 2 files changed, 9 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 888dc11ec..90564a19d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -605,8 +605,6 @@ class RPC: def _rpc_locks(self) -> Dict[str, Any]: """ Returns the current locks""" - if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') locks = PairLocks.get_pair_locks(None) return { diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7885a251d..ace44a34a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1041,13 +1041,6 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED - telegram._locks(update=update, context=MagicMock()) - assert msg_mock.call_count == 1 - assert 'not running' in msg_mock.call_args_list[0][0][0] - msg_mock.reset_mock() - freqtradebot.state = State.RUNNING - PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') From 08b52926c816331033087b47c2afd1cb047c4452 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Nov 2020 10:43:48 +0100 Subject: [PATCH 49/63] Catch asyncio.TimeoutError when reloading async markets --- 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 659ff59bc..e74f5668c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -282,7 +282,7 @@ class Exchange: asyncio.get_event_loop().run_until_complete( self._api_async.load_markets(reload=reload)) - except ccxt.BaseError as e: + except (asyncio.TimeoutError, ccxt.BaseError) as e: logger.warning('Could not load async markets. Reason: %s', e) return From 164105acf24503a7349621add1c3b178b74ec5ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Nov 2020 08:25:57 +0100 Subject: [PATCH 50/63] Adjust startup_candle_count of sample strategies --- freqtrade/templates/base_strategy.py.j2 | 2 +- freqtrade/templates/sample_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index ce2c6d5c0..4a1b43e36 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -63,7 +63,7 @@ class {{ strategy }}(IStrategy): ignore_roi_if_buy_signal = False # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 + startup_candle_count: int = 30 # Optional order type mapping. order_types = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 103f68a43..44590dbbe 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -64,7 +64,7 @@ class SampleStrategy(IStrategy): ignore_roi_if_buy_signal = False # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 + startup_candle_count: int = 30 # Optional order type mapping. order_types = { From 05f0cc787c432235fb31255725645fb42bb32fd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Nov 2020 09:28:00 +0100 Subject: [PATCH 51/63] Plotting should use startup_candles too closes #3943 --- freqtrade/plot/plotting.py | 18 ++++++++++++++---- tests/test_plotting.py | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a89732df5..f7d300593 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -9,9 +9,9 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframe create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.data.history import load_data +from freqtrade.data.history import get_timerange, load_data from freqtrade.exceptions import OperationalException -from freqtrade.exchange import timeframe_to_prev_date +from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds from freqtrade.misc import pair_to_filename from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy import IStrategy @@ -29,7 +29,7 @@ except ImportError: exit(1) -def init_plotscript(config): +def init_plotscript(config, startup_candles: int = 0): """ Initialize objects needed for plotting :return: Dict with candle (OHLCV) data, trades and pairs @@ -48,9 +48,16 @@ def init_plotscript(config): pairs=pairs, timeframe=config.get('timeframe', '5m'), timerange=timerange, + startup_candles=startup_candles, data_format=config.get('dataformat_ohlcv', 'json'), ) + if startup_candles: + min_date, max_date = get_timerange(data) + logger.info(f"Loading data from {min_date} to {max_date}") + timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), + startup_candles, min_date) + no_trades = False filename = config.get('exportfilename') if config.get('no_trades', False): @@ -72,6 +79,7 @@ def init_plotscript(config): return {"ohlcv": data, "trades": trades, "pairs": pairs, + "timerange": timerange, } @@ -474,7 +482,8 @@ def load_and_plot_trades(config: Dict[str, Any]): exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) IStrategy.dp = DataProvider(config, exchange) - plot_elements = init_plotscript(config) + plot_elements = init_plotscript(config, strategy.startup_candle_count) + timerange = plot_elements['timerange'] trades = plot_elements['trades'] pair_counter = 0 for pair, data in plot_elements["ohlcv"].items(): @@ -482,6 +491,7 @@ def load_and_plot_trades(config: Dict[str, Any]): logger.info("analyse pair %s", pair) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) + df_analyzed = trim_dataframe(df_analyzed, timerange) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 401f66b60..d3f97013d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -51,9 +51,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir): assert "ohlcv" in ret assert "trades" in ret assert "pairs" in ret + assert 'timerange' in ret default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] - ret = init_plotscript(default_conf) + ret = init_plotscript(default_conf, 20) assert "ohlcv" in ret assert "TRX/BTC" in ret["ohlcv"] assert "ADA/BTC" in ret["ohlcv"] From 7243c8ee56852d1fd649ffcbc5bff28fd57e7e8a Mon Sep 17 00:00:00 2001 From: SamVerhaegen Date: Sun, 15 Nov 2020 13:06:05 +0100 Subject: [PATCH 52/63] Fix typo in windows installation docs. --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 0ef0f131f..924459a6d 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -32,7 +32,7 @@ python -m venv .env .env\Scripts\activate.ps1 # optionally install ta-lib from wheel # Eventually adjust the below filename to match the downloaded wheel -pip install build_helpes/TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl +pip install build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl pip install -r requirements.txt pip install -e . freqtrade From da16474b2561a12636e0440d2100b0bb691a8f2d Mon Sep 17 00:00:00 2001 From: Aleksey Savin Date: Sun, 15 Nov 2020 15:13:44 +0300 Subject: [PATCH 53/63] Update telegram-usage.md --- docs/telegram-usage.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index ce2d715a0..5def6efd2 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -35,12 +35,16 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo Don't forget to start the conversation with your bot, by clicking `/START` button -### 2. Get your user id +### 2. Telegram user_id +#### Get your user id Talk to the [userinfobot](https://telegram.me/userinfobot) Get your "Id", you will use it for the config parameter `chat_id`. +#### Use Group id +You can use bots in telegram groups just adding them to it. You can find the group id by adding a [RawDataBot](https://telegram.me/rawdatabot) to it, group id is the `"chat":{"id":-1001332619709}` in the [RawDataBot](https://telegram.me/rawdatabot) message. Dont forget about "-" (minus symbol) in start of value if it is and use string type in config, for example: `"chat_id":"-1001332619709"`. + ## Control telegram noise Freqtrade provides means to control the verbosity of your telegram bot. From 7b4c1ec3ced1ebcc2f8be06e2602f7223d964d5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Nov 2020 15:40:40 +0100 Subject: [PATCH 54/63] Small wording changes --- docs/telegram-usage.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5def6efd2..09cf21223 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -36,6 +36,7 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo Don't forget to start the conversation with your bot, by clicking `/START` button ### 2. Telegram user_id + #### Get your user id Talk to the [userinfobot](https://telegram.me/userinfobot) @@ -43,7 +44,20 @@ Talk to the [userinfobot](https://telegram.me/userinfobot) Get your "Id", you will use it for the config parameter `chat_id`. #### Use Group id -You can use bots in telegram groups just adding them to it. You can find the group id by adding a [RawDataBot](https://telegram.me/rawdatabot) to it, group id is the `"chat":{"id":-1001332619709}` in the [RawDataBot](https://telegram.me/rawdatabot) message. Dont forget about "-" (minus symbol) in start of value if it is and use string type in config, for example: `"chat_id":"-1001332619709"`. + +You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you: + +``` json +"chat":{ + "id":-1001332619709 +} +``` + +For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string: + +```json + "chat_id": "-1001332619709" +``` ## Control telegram noise From 6ebc2f38974eff34140d63b2955f1e1b54f82b3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 05:40:25 +0000 Subject: [PATCH 55/63] Bump mkdocs-material from 6.1.4 to 6.1.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.4 to 6.1.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.4...6.1.5) Signed-off-by: dependabot[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 f034b0b36..cd294bef6 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.4 +mkdocs-material==6.1.5 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From e52c181a2a2a8a369a69fdff2f758767bb42b969 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 05:40:35 +0000 Subject: [PATCH 56/63] Bump flask-jwt-extended from 3.24.1 to 3.25.0 Bumps [flask-jwt-extended](https://github.com/vimalloc/flask-jwt-extended) from 3.24.1 to 3.25.0. - [Release notes](https://github.com/vimalloc/flask-jwt-extended/releases) - [Commits](https://github.com/vimalloc/flask-jwt-extended/compare/3.24.1...3.25.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4622afa15..ea33cdf3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ sdnotify==0.3.2 # Api server flask==1.1.2 -flask-jwt-extended==3.24.1 +flask-jwt-extended==3.25.0 flask-cors==3.0.9 # Support for colorized terminal output From 23947cf30b0dcfc7aab066d48237e0dcd229fb6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 05:40:50 +0000 Subject: [PATCH 57/63] Bump requests from 2.24.0 to 2.25.0 Bumps [requests](https://github.com/psf/requests) from 2.24.0 to 2.25.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.24.0...v2.25.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4622afa15..e56dcf4d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 -requests==2.24.0 +requests==2.25.0 urllib3==1.25.11 wrapt==1.12.1 jsonschema==3.2.0 From f092a923990515976cfccabc59010d8ccddfd5ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 07:51:10 +0000 Subject: [PATCH 58/63] Bump urllib3 from 1.25.11 to 1.26.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.11 to 1.26.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.11...1.26.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5ce060819..d60a0ebba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 requests==2.25.0 -urllib3==1.25.11 +urllib3==1.26.2 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From 3f2addb729f0d462ec11a40197eba4f00c17706a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 09:17:22 +0000 Subject: [PATCH 59/63] Bump ccxt from 1.37.41 to 1.37.69 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.37.41 to 1.37.69. - [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.37.41...1.37.69) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d60a0ebba..67b69a5dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.41 +ccxt==1.37.69 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 9621734adc37eb6f6edbd24907e42d35628d70ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Nov 2020 06:53:38 +0100 Subject: [PATCH 60/63] Allow setting datafromat via configuration closes #3953 --- freqtrade/commands/cli_options.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4769bccde..619a300ae 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -354,13 +354,11 @@ AVAILABLE_CLI_OPTIONS = { '--data-format-ohlcv', help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).', choices=constants.AVAILABLE_DATAHANDLERS, - default='json' ), "dataformat_trades": Arg( '--data-format-trades', help='Storage format for downloaded trades data. (default: `%(default)s`).', choices=constants.AVAILABLE_DATAHANDLERS, - default='jsongz' ), "exchange": Arg( '--exchange', From 4a215821cdc2c3017c15fff2082a2b5e58773c61 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 17 Nov 2020 14:07:24 +0100 Subject: [PATCH 61/63] Fix typo in windows installation docs --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 924459a6d..0c1eeef64 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -50,7 +50,7 @@ freqtrade error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools ``` -Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. +Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. From 854d0c481f0b7569f4e28418a64e088aeb6c0bd4 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 17 Nov 2020 14:14:42 +0100 Subject: [PATCH 62/63] Update windows_installation.md --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 0c1eeef64..5341ce96b 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -52,6 +52,6 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. --- From 52c9a2c37f62196d43f09b41fb0a90dc187a37e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 07:30:28 +0100 Subject: [PATCH 63/63] Convert np to None when loading hdf5 trades to allow duplicate detection --- freqtrade/data/history/hdf5datahandler.py | 4 +++- tests/data/test_history.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index f6cf9e0d9..00e41673d 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -3,6 +3,7 @@ import re from pathlib import Path from typing import List, Optional +import numpy as np import pandas as pd from freqtrade import misc @@ -175,7 +176,8 @@ class HDF5DataHandler(IDataHandler): if timerange.stoptype == 'date': where.append(f"timestamp < {timerange.stopts * 1e3}") - trades = pd.read_hdf(filename, key=key, mode="r", where=where) + trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where) + trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None}) return trades.values.tolist() def trades_purge(self, pair: str) -> bool: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index bbc6e55b4..538a0840f 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -724,6 +724,8 @@ def test_hdf5datahandler_trades_load(testdatadir): trades2 = dh._trades_load('XRP/ETH', timerange) assert len(trades) > len(trades2) + # Check that ID is None (If it's nan, it's wrong) + assert trades2[0][2] is None # unfiltered load has trades before starttime assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0