From feeccfedaadd24b69c458f5625afee511b3fc7f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Oct 2022 20:30:38 +0200 Subject: [PATCH 01/89] Update list-exchanges with watchOHLCV --- freqtrade/exchange/common.py | 2 ++ freqtrade/exchange/exchange_utils.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 99f891836..62221d0cc 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -92,6 +92,8 @@ EXCHANGE_HAS_OPTIONAL = [ # 'fetchMarketLeverageTiers', # Futures initialization # 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... + # ccxt.pro + 'watchOHLCV' ] diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index dcae1ab3b..89b378c08 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -58,7 +58,10 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]: returns: can_use, reason with Reason including both missing and missing_opt """ - ex_mod = getattr(ccxt, exchange.lower())() + try: + ex_mod = getattr(ccxt.pro, exchange.lower())() + except AttributeError: + ex_mod = getattr(ccxt.async_support, exchange.lower())() result = True reason = "" if not ex_mod or not ex_mod.has: From c9b1071baa01618b0b1d4335370a3db991cb2899 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 10 Oct 2022 20:48:35 +0200 Subject: [PATCH 02/89] Use api_async for exchange_has --- 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 9d7c4eabb..5c4745ce5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,7 +793,7 @@ class Exchange: """ if endpoint in self._ft_has.get("exchange_has_overrides", {}): return self._ft_has["exchange_has_overrides"][endpoint] - return endpoint in self._api.has and self._api.has[endpoint] + return endpoint in self._api_async.has and self._api_async.has[endpoint] def get_precision_amount(self, pair: str) -> Optional[float]: """ From ad7b78ec9365014c6ca3336dde46325a01bac709 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Oct 2022 06:17:42 +0200 Subject: [PATCH 03/89] Update exchange init to use .pro if available --- freqtrade/exchange/exchange.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5c4745ce5..c0d34c65e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -15,6 +15,7 @@ from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union import ccxt import ccxt.async_support as ccxt_async +import ccxt.pro as ccxt_pro from cachetools import TTLCache from ccxt import TICK_SIZE from dateutil import parser @@ -218,7 +219,7 @@ class Exchange: ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_sync_config", {}), ccxt_config) - self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config) + self._api = self._init_ccxt(exchange_conf, True, ccxt_config) ccxt_async_config = self._ccxt_config ccxt_async_config = deep_merge_dicts( @@ -227,8 +228,7 @@ class Exchange: ccxt_async_config = deep_merge_dicts( exchange_conf.get("ccxt_async_config", {}), ccxt_async_config ) - self._api_async = self._init_ccxt(exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) - + self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 if validate: @@ -287,18 +287,20 @@ class Exchange: self.validate_pricing(config["entry_pricing"]) def _init_ccxt( - self, - exchange_config: Dict[str, Any], - ccxt_module: CcxtModuleType = ccxt, - *, - ccxt_kwargs: Dict, + self, exchange_config: Dict[str, Any], sync: bool, ccxt_kwargs: Dict[str, Any] ) -> ccxt.Exchange: """ - Initialize ccxt with given config and return valid - ccxt instance. + Initialize ccxt with given config and return valid ccxt instance. """ # Find matching class for the given exchange name name = exchange_config["name"] + if sync: + ccxt_module = ccxt + else: + ccxt_module = ccxt_pro + if not is_exchange_known_ccxt(name, ccxt_module): + # Fall back to async if pro doesn't support this exchange + ccxt_module = ccxt_async if not is_exchange_known_ccxt(name, ccxt_module): raise OperationalException(f"Exchange {name} is not supported by ccxt") From e985c1890b38a122aa5abe9dd751edc5e69d4c05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 12 Oct 2022 21:08:01 +0200 Subject: [PATCH 04/89] Implement basic ccxt.pro to test --- freqtrade/exchange/exchange.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c0d34c65e..39cc9a0b3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,6 +7,7 @@ import asyncio import inspect import logging import signal +import time from copy import deepcopy from datetime import datetime, timedelta, timezone from math import floor, isnan @@ -229,6 +230,8 @@ class Exchange: exchange_conf.get("ccxt_async_config", {}), ccxt_async_config ) self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) + self._has_watch_ohlcv = self.exchange_has('watchOHLCV') + logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 if validate: @@ -2219,6 +2222,14 @@ class Exchange: data = sorted(data, key=lambda x: x[0]) return pair, timeframe, candle_type, data, self._ohlcv_partial_candle + async def _async_watch_ohlcv(self, pair: str, timeframe: str, + candle_type: CandleType) -> Tuple[str, str, str, List]: + start = time.time() + data = await self._api_async.watch_ohlcv(pair, timeframe, ) + + logger.info(f"watch {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") + return pair, timeframe, candle_type, data + def _build_coroutine( self, pair: str, @@ -2231,8 +2242,16 @@ class Exchange: if cache and (pair, timeframe, candle_type) in self._klines: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp() + one_date = date_minus_candles(timeframe, 1).timestamp() + last_refresh = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + if (self._has_watch_ohlcv + and candle_type in (CandleType.SPOT, CandleType.FUTURES) + and one_date <= last_refresh): + logger.info(f"Using watch {pair}, {timeframe}, {candle_type}") + return self._async_watch_ohlcv(pair, timeframe, candle_type) + pass # Check if 1 call can get us updated candles without hole in the data. - if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0): + elif min_date < last_refresh: # Cache can be used - do one-off call. not_all_data = False else: From 34ccada9097dce3bc305ee983cb27bc96b9aaed9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Oct 2022 20:48:40 +0200 Subject: [PATCH 05/89] Tests with seperate thread --- freqtrade/exchange/exchange.py | 43 +++++++++----- freqtrade/exchange/exchange_ws.py | 94 +++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 freqtrade/exchange/exchange_ws.py diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 39cc9a0b3..55c981007 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -58,7 +58,6 @@ from freqtrade.exchange.exchange_utils import ( ROUND, ROUND_DOWN, ROUND_UP, - CcxtModuleType, amount_to_contract_precision, amount_to_contracts, amount_to_precision, @@ -75,6 +74,7 @@ from freqtrade.exchange.exchange_utils_timeframe import ( timeframe_to_prev_date, timeframe_to_seconds, ) +from freqtrade.exchange.exchange_ws import ExchangeWS from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers from freqtrade.misc import ( chunks, @@ -230,7 +230,11 @@ class Exchange: exchange_conf.get("ccxt_async_config", {}), ccxt_async_config ) self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) - self._has_watch_ohlcv = self.exchange_has('watchOHLCV') + self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) + self._has_watch_ohlcv = self.exchange_has("watchOHLCV") + self._exchange_ws: Optional[ExchangeWS] = None + if self._has_watch_ohlcv: + self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 @@ -2222,10 +2226,14 @@ class Exchange: data = sorted(data, key=lambda x: x[0]) return pair, timeframe, candle_type, data, self._ohlcv_partial_candle - async def _async_watch_ohlcv(self, pair: str, timeframe: str, - candle_type: CandleType) -> Tuple[str, str, str, List]: + async def _async_watch_ohlcv( + self, pair: str, timeframe: str, candle_type: CandleType + ) -> Tuple[str, str, str, List]: start = time.time() - data = await self._api_async.watch_ohlcv(pair, timeframe, ) + data = await self._api_async.watch_ohlcv( + pair, + timeframe, + ) logger.info(f"watch {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") return pair, timeframe, candle_type, data @@ -2239,19 +2247,28 @@ class Exchange: cache: bool, ) -> Coroutine[Any, Any, OHLCVResponse]: not_all_data = cache and self.required_candle_call_count > 1 + if cache: + if self._exchange_ws: + # Subscribe to websocket + self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type) if cache and (pair, timeframe, candle_type) in self._klines: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp() - one_date = date_minus_candles(timeframe, 1).timestamp() + date_minus_candles(timeframe, 1).timestamp() last_refresh = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) - if (self._has_watch_ohlcv - and candle_type in (CandleType.SPOT, CandleType.FUTURES) - and one_date <= last_refresh): - logger.info(f"Using watch {pair}, {timeframe}, {candle_type}") - return self._async_watch_ohlcv(pair, timeframe, candle_type) - pass + # if self._exchange_ws: + # self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type) + # if ( + # self._has_watch_ohlcv + # and candle_type in (CandleType.SPOT, CandleType.FUTURES) + # and one_date <= last_refresh + # ): + # logger.info(f"Using watch {pair}, {timeframe}, {candle_type}") + # return self._async_watch_ohlcv(pair, timeframe, candle_type) + # pass # Check if 1 call can get us updated candles without hole in the data. - elif min_date < last_refresh: + # el + if min_date < last_refresh: # Cache can be used - do one-off call. not_all_data = False else: diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py new file mode 100644 index 000000000..55b0fb372 --- /dev/null +++ b/freqtrade/exchange/exchange_ws.py @@ -0,0 +1,94 @@ + +import asyncio +import logging +import time +from threading import Thread +from typing import List, Set, Tuple + +from freqtrade.constants import Config +from freqtrade.enums.candletype import CandleType + + +logger = logging.getLogger(__name__) + + +class ExchangeWS(): + def __init__(self, config: Config, ccxt_object) -> None: + self.config = config + self.ccxt_object = ccxt_object + self._thread = Thread(name="ccxt_ws", target=self.start) + self._background_tasks = set() + self._pairs_watching: Set[Tuple[str, str, CandleType]] = set() + self._pairs_scheduled: Set[Tuple[str, str, CandleType]] = set() + self._thread.start() + + def start(self) -> None: + self._loop = asyncio.new_event_loop() + self._loop.run_forever() + +## One task per Watch + # async def schedule_schedule(self) -> None: + + # for p in self._pairs_watching: + # if p not in self._pairs_scheduled: + # self._pairs_scheduled.add(p) + # await self.schedule_one_task(p[0], p[1], p[2]) + + # async def schedule_one_task(self, pair: str, timeframe: str, candle_type: CandleType) -> None: + # task = asyncio.create_task(self._async_watch_ohlcv(pair, timeframe, candle_type)) + + # # Add task to the set. This creates a strong reference. + # self._background_tasks.add(task) + # task.add_done_callback(self.reschedule_or_stop) + + # async def _async_watch_ohlcv(self, pair: str, timeframe: str, + # candle_type: CandleType) -> Tuple[str, str, str, List]: + # start = time.time() + # data = await self.ccxt_object.watch_ohlcv(pair, timeframe, ) + # logger.info(f"watch done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") + # return pair, timeframe, candle_type, data + + # def reschedule_or_stop(self, task: asyncio.Task): + # # logger.info(f"Task finished {task}") + + # self._background_tasks.discard(task) + # pair, timeframe, candle_type, data = task.result() + + # # reschedule + # asyncio.run_coroutine_threadsafe(self.schedule_one_task( + # pair, timeframe, candle_type), loop=self._loop) + +## End one task epr watch + + async def schedule_while_true(self) -> None: + + for p in self._pairs_watching: + if p not in self._pairs_scheduled: + self._pairs_scheduled.add(p) + pair, timeframe, candle_type = p + task = asyncio.create_task( + self.continuously_async_watch_ohlcv(pair, timeframe, candle_type)) + self._background_tasks.add(task) + task.add_done_callback(self.continuous_stopped) + + def continuous_stopped(self, task: asyncio.Task): + self._background_tasks.discard(task) + pair, timeframe, candle_type, data = task.result() + self._pairs_scheduled.discard(p) + + logger.info(f"Task finished {task}") + + async def continuously_async_watch_ohlcv( + self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: + + while (pair, timeframe, candle_type) in self._pairs_watching: + start = time.time() + data = await self.ccxt_object.watch_ohlcv(pair, timeframe, ) + logger.info( + f"watch1 done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") + + def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: + self._pairs_watching.add((pair, timeframe, candle_type)) + # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) + asyncio.run_coroutine_threadsafe(self.schedule_while_true(), loop=self._loop) + From 51890f80c49b8c2936bf9467748baf6c3834ec19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Oct 2022 06:50:34 +0200 Subject: [PATCH 06/89] Add parameter for ws enablin --- 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 55c981007..8f4dc1bfc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -233,7 +233,7 @@ class Exchange: self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._has_watch_ohlcv = self.exchange_has("watchOHLCV") self._exchange_ws: Optional[ExchangeWS] = None - if self._has_watch_ohlcv: + if exchange_config.get('enable_ws', False) and self._has_watch_ohlcv: self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') From 3468edddf6784f95ed6cc69b0b004752cae135e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 09:15:33 +0200 Subject: [PATCH 07/89] Add enable_ws config setting --- freqtrade/constants.py | 1 + freqtrade/exchange/exchange.py | 2 +- tests/conftest.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f8f1ac7ee..86c1d71cd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -539,6 +539,7 @@ CONF_SCHEMA = { "type": "object", "properties": { "name": {"type": "string"}, + "enable_ws": {"type": "boolean", "default": True}, "key": {"type": "string", "default": ""}, "secret": {"type": "string", "default": ""}, "password": {"type": "string", "default": ""}, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8f4dc1bfc..7d0e3a3b1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -233,7 +233,7 @@ class Exchange: self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._has_watch_ohlcv = self.exchange_has("watchOHLCV") self._exchange_ws: Optional[ExchangeWS] = None - if exchange_config.get('enable_ws', False) and self._has_watch_ohlcv: + if exchange_config.get('enable_ws', True) and self._has_watch_ohlcv: self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') diff --git a/tests/conftest.py b/tests/conftest.py index 3686a548a..0dfb859c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -587,6 +587,7 @@ def get_default_conf(testdatadir): "exchange": { "name": "binance", "key": "key", + "enable_ws": False, "secret": "secret", "pair_whitelist": ["ETH/BTC", "LTC/BTC", "XRP/BTC", "NEO/BTC"], "pair_blacklist": [ @@ -628,6 +629,7 @@ def get_default_conf_usdt(testdatadir): "name": "binance", "enabled": True, "key": "key", + "enable_ws": False, "secret": "secret", "pair_whitelist": [ "ETH/USDT", From bd494ed67a6fe39cda19fc3b1f32159f31e96176 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 09:22:13 +0200 Subject: [PATCH 08/89] Cleanup exchange changes --- 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 7d0e3a3b1..ad66d50e0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -233,7 +233,7 @@ class Exchange: self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._has_watch_ohlcv = self.exchange_has("watchOHLCV") self._exchange_ws: Optional[ExchangeWS] = None - if exchange_config.get('enable_ws', True) and self._has_watch_ohlcv: + if exchange_config.get("enable_ws", True) and self._has_watch_ohlcv: self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') From 1d12985b70e91737f1182e2bebf908483a0a062a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 09:49:50 +0200 Subject: [PATCH 09/89] Update exchange_ws with cleanup function --- freqtrade/exchange/exchange_ws.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 55b0fb372..1805aedef 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -3,7 +3,7 @@ import asyncio import logging import time from threading import Thread -from typing import List, Set, Tuple +from typing import Dict, List, Set, Tuple from freqtrade.constants import Config from freqtrade.enums.candletype import CandleType @@ -20,13 +20,21 @@ class ExchangeWS(): self._background_tasks = set() self._pairs_watching: Set[Tuple[str, str, CandleType]] = set() self._pairs_scheduled: Set[Tuple[str, str, CandleType]] = set() + self.pairs_last_refresh: Dict[Tuple[str, str, CandleType], int] = {} self._thread.start() def start(self) -> None: self._loop = asyncio.new_event_loop() self._loop.run_forever() -## One task per Watch + def cleanup(self) -> None: + logger.debug("Cleanup called - stopping") + self._pairs_watching.clear() + self._loop.stop() + self._thread.join() + logger.debug("Stopped") + +# One task per Watch # async def schedule_schedule(self) -> None: # for p in self._pairs_watching: @@ -58,7 +66,7 @@ class ExchangeWS(): # asyncio.run_coroutine_threadsafe(self.schedule_one_task( # pair, timeframe, candle_type), loop=self._loop) -## End one task epr watch +# End one task epr watch async def schedule_while_true(self) -> None: @@ -73,8 +81,9 @@ class ExchangeWS(): def continuous_stopped(self, task: asyncio.Task): self._background_tasks.discard(task) - pair, timeframe, candle_type, data = task.result() - self._pairs_scheduled.discard(p) + result = task.result() + logger.info(f"Task finished {result}") + # self._pairs_scheduled.discard(pair, timeframe, candle_type) logger.info(f"Task finished {task}") @@ -82,8 +91,10 @@ class ExchangeWS(): self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: while (pair, timeframe, candle_type) in self._pairs_watching: + logger.info(self._pairs_watching) start = time.time() - data = await self.ccxt_object.watch_ohlcv(pair, timeframe, ) + data = await self.ccxt_object.watch_ohlcv(pair, timeframe) + self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() logger.info( f"watch1 done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") From 2fd5b4a6e1541486318c17e45c143094f663a51c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 09:50:13 +0200 Subject: [PATCH 10/89] Use websocket results --- freqtrade/exchange/exchange.py | 45 +++++++++++++++---------------- freqtrade/exchange/exchange_ws.py | 1 - 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ad66d50e0..45fad6aa6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -263,6 +263,8 @@ class Exchange: self.close() def close(self): + if self._exchange_ws: + self._exchange_ws.cleanup() logger.debug("Exchange object destroyed, closing async loop") if ( self._api_async @@ -2229,14 +2231,11 @@ class Exchange: async def _async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType ) -> Tuple[str, str, str, List]: - start = time.time() - data = await self._api_async.watch_ohlcv( - pair, - timeframe, - ) - - logger.info(f"watch {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") - return pair, timeframe, candle_type, data + candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) + # Fake 1 candle - which is then removed again + candles.append([int(datetime.now(timezone.utc).timestamp() * 1000), 0, 0, 0, 0, 0]) + logger.info(f"watch result for {pair}, {timeframe} with length {len(candles)}") + return pair, timeframe, candle_type, candles def _build_coroutine( self, @@ -2247,27 +2246,27 @@ class Exchange: cache: bool, ) -> Coroutine[Any, Any, OHLCVResponse]: not_all_data = cache and self.required_candle_call_count > 1 - if cache: - if self._exchange_ws: + if cache and candle_type in (CandleType.SPOT, CandleType.FUTURES): + if self._has_watch_ohlcv and self._exchange_ws: # Subscribe to websocket self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type) if cache and (pair, timeframe, candle_type) in self._klines: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) - min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp() - date_minus_candles(timeframe, 1).timestamp() + min_date = int(date_minus_candles(timeframe, candle_limit - 5).timestamp()) + last_refresh = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) - # if self._exchange_ws: - # self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type) - # if ( - # self._has_watch_ohlcv - # and candle_type in (CandleType.SPOT, CandleType.FUTURES) - # and one_date <= last_refresh - # ): - # logger.info(f"Using watch {pair}, {timeframe}, {candle_type}") - # return self._async_watch_ohlcv(pair, timeframe, candle_type) - # pass + if self._exchange_ws: + candle_date = int(timeframe_to_prev_date(timeframe).timestamp()) + candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) + x = self._exchange_ws.pairs_last_refresh[(pair, timeframe, candle_type)] + logger.info(f"{candle_date < x}, {candle_date}, {x}") + if candles and candles[-1][0] > min_date and candle_date < x: + # Usable result ... + logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") + + return self._async_watch_ohlcv(pair, timeframe, candle_type) + # Check if 1 call can get us updated candles without hole in the data. - # el if min_date < last_refresh: # Cache can be used - do one-off call. not_all_data = False diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 1805aedef..b304247a3 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -91,7 +91,6 @@ class ExchangeWS(): self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: while (pair, timeframe, candle_type) in self._pairs_watching: - logger.info(self._pairs_watching) start = time.time() data = await self.ccxt_object.watch_ohlcv(pair, timeframe) self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() From 972b932e5d7cd8266ed2f64e68a1f9f0414c463c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Oct 2022 07:22:53 +0200 Subject: [PATCH 11/89] Implement ws cleanup --- freqtrade/exchange/exchange_ws.py | 51 +++++++++++-------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index b304247a3..bded6ec27 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -18,9 +18,11 @@ class ExchangeWS(): self.ccxt_object = ccxt_object self._thread = Thread(name="ccxt_ws", target=self.start) self._background_tasks = set() + self._pairs_watching: Set[Tuple[str, str, CandleType]] = set() self._pairs_scheduled: Set[Tuple[str, str, CandleType]] = set() self.pairs_last_refresh: Dict[Tuple[str, str, CandleType], int] = {} + self.pairs_last_request: Dict[Tuple[str, str, CandleType], int] = {} self._thread.start() def start(self) -> None: @@ -34,39 +36,19 @@ class ExchangeWS(): self._thread.join() logger.debug("Stopped") -# One task per Watch - # async def schedule_schedule(self) -> None: - - # for p in self._pairs_watching: - # if p not in self._pairs_scheduled: - # self._pairs_scheduled.add(p) - # await self.schedule_one_task(p[0], p[1], p[2]) - - # async def schedule_one_task(self, pair: str, timeframe: str, candle_type: CandleType) -> None: - # task = asyncio.create_task(self._async_watch_ohlcv(pair, timeframe, candle_type)) - - # # Add task to the set. This creates a strong reference. - # self._background_tasks.add(task) - # task.add_done_callback(self.reschedule_or_stop) - - # async def _async_watch_ohlcv(self, pair: str, timeframe: str, - # candle_type: CandleType) -> Tuple[str, str, str, List]: - # start = time.time() - # data = await self.ccxt_object.watch_ohlcv(pair, timeframe, ) - # logger.info(f"watch done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") - # return pair, timeframe, candle_type, data - - # def reschedule_or_stop(self, task: asyncio.Task): - # # logger.info(f"Task finished {task}") - - # self._background_tasks.discard(task) - # pair, timeframe, candle_type, data = task.result() - - # # reschedule - # asyncio.run_coroutine_threadsafe(self.schedule_one_task( - # pair, timeframe, candle_type), loop=self._loop) - -# End one task epr watch + def cleanup_expired(self) -> None: + """ + Remove pairs from watchlist if they've not been requested within + the last timeframe (+ offset) + """ + from freqtrade.exchange.exchange import timeframe_to_seconds + for p in list(self._pairs_watching): + _, timeframe, _ = p + timeframe_s = timeframe_to_seconds(timeframe) + last_refresh = self.pairs_last_request.get(p, 0) + if last_refresh > 0 and time.time() - last_refresh > timeframe_s + 20: + logger.info(f"Removing {p} from watchlist") + self._pairs_watching.discard(p) async def schedule_while_true(self) -> None: @@ -99,6 +81,7 @@ class ExchangeWS(): def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: self._pairs_watching.add((pair, timeframe, candle_type)) + self.pairs_last_request[(pair, timeframe, candle_type)] = time.time() # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) asyncio.run_coroutine_threadsafe(self.schedule_while_true(), loop=self._loop) - + self.cleanup_expired() From e2b567165c752a2fa9da4849cb1e238fc7e1bcba Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Oct 2022 07:25:10 +0200 Subject: [PATCH 12/89] remove double log --- freqtrade/exchange/exchange_ws.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index bded6ec27..fb77f8c21 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -67,8 +67,6 @@ class ExchangeWS(): logger.info(f"Task finished {result}") # self._pairs_scheduled.discard(pair, timeframe, candle_type) - logger.info(f"Task finished {task}") - async def continuously_async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: @@ -77,7 +75,7 @@ class ExchangeWS(): data = await self.ccxt_object.watch_ohlcv(pair, timeframe) self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() logger.info( - f"watch1 done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") + f"watch done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: self._pairs_watching.add((pair, timeframe, candle_type)) From 18dabd519a9d5a99fd68a279b4051956179b8d4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Oct 2022 19:19:30 +0200 Subject: [PATCH 13/89] ccxt.pro - move get_klines to ws_exchange --- freqtrade/exchange/exchange.py | 11 +---------- freqtrade/exchange/exchange_ws.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 45fad6aa6..9d1a2d797 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2228,15 +2228,6 @@ class Exchange: data = sorted(data, key=lambda x: x[0]) return pair, timeframe, candle_type, data, self._ohlcv_partial_candle - async def _async_watch_ohlcv( - self, pair: str, timeframe: str, candle_type: CandleType - ) -> Tuple[str, str, str, List]: - candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) - # Fake 1 candle - which is then removed again - candles.append([int(datetime.now(timezone.utc).timestamp() * 1000), 0, 0, 0, 0, 0]) - logger.info(f"watch result for {pair}, {timeframe} with length {len(candles)}") - return pair, timeframe, candle_type, candles - def _build_coroutine( self, pair: str, @@ -2264,7 +2255,7 @@ class Exchange: # Usable result ... logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") - return self._async_watch_ohlcv(pair, timeframe, candle_type) + return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type) # Check if 1 call can get us updated candles without hole in the data. if min_date < last_refresh: diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index fb77f8c21..1500cbb5d 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -2,6 +2,7 @@ import asyncio import logging import time +from datetime import datetime, timezone from threading import Thread from typing import Dict, List, Set, Tuple @@ -17,12 +18,12 @@ class ExchangeWS(): self.config = config self.ccxt_object = ccxt_object self._thread = Thread(name="ccxt_ws", target=self.start) - self._background_tasks = set() + self._background_tasks: Set[asyncio.Task] = set() self._pairs_watching: Set[Tuple[str, str, CandleType]] = set() self._pairs_scheduled: Set[Tuple[str, str, CandleType]] = set() - self.pairs_last_refresh: Dict[Tuple[str, str, CandleType], int] = {} - self.pairs_last_request: Dict[Tuple[str, str, CandleType], int] = {} + self.pairs_last_refresh: Dict[Tuple[str, str, CandleType], float] = {} + self.pairs_last_request: Dict[Tuple[str, str, CandleType], float] = {} self._thread.start() def start(self) -> None: @@ -68,7 +69,7 @@ class ExchangeWS(): # self._pairs_scheduled.discard(pair, timeframe, candle_type) async def continuously_async_watch_ohlcv( - self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: + self, pair: str, timeframe: str, candle_type: CandleType) -> None: while (pair, timeframe, candle_type) in self._pairs_watching: start = time.time() @@ -83,3 +84,14 @@ class ExchangeWS(): # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) asyncio.run_coroutine_threadsafe(self.schedule_while_true(), loop=self._loop) self.cleanup_expired() + + async def get_ohlcv( + self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: + """ + Returns cached klines from ccxt's "watch" cache. + """ + candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) + # Fake 1 candle - which is then removed again + candles.append([int(datetime.now(timezone.utc).timestamp() * 1000), 0, 0, 0, 0, 0]) + logger.info(f"watch result for {pair}, {timeframe} with length {len(candles)}") + return pair, timeframe, candle_type, candles From eda8a767caa5b4c42052cb55bee4ff8cd261d428 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Oct 2022 19:44:27 +0200 Subject: [PATCH 14/89] Improve ws exchange --- freqtrade/exchange/exchange_ws.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 1500cbb5d..70deace53 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -2,12 +2,13 @@ import asyncio import logging import time -from datetime import datetime, timezone +from datetime import datetime from threading import Thread from typing import Dict, List, Set, Tuple from freqtrade.constants import Config from freqtrade.enums.candletype import CandleType +from freqtrade.exchange.exchange import timeframe_to_seconds logger = logging.getLogger(__name__) @@ -42,7 +43,6 @@ class ExchangeWS(): Remove pairs from watchlist if they've not been requested within the last timeframe (+ offset) """ - from freqtrade.exchange.exchange import timeframe_to_seconds for p in list(self._pairs_watching): _, timeframe, _ = p timeframe_s = timeframe_to_seconds(timeframe) @@ -75,8 +75,8 @@ class ExchangeWS(): start = time.time() data = await self.ccxt_object.watch_ohlcv(pair, timeframe) self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() - logger.info( - f"watch done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") + # logger.info( + # f"watch done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: self._pairs_watching.add((pair, timeframe, candle_type)) @@ -92,6 +92,7 @@ class ExchangeWS(): """ candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) # Fake 1 candle - which is then removed again - candles.append([int(datetime.now(timezone.utc).timestamp() * 1000), 0, 0, 0, 0, 0]) + # TODO: is this really a good idea?? + candles.append([candles[-1][0], 0, 0, 0, 0, 0]) logger.info(f"watch result for {pair}, {timeframe} with length {len(candles)}") return pair, timeframe, candle_type, candles From f9524aebe9ec3aa6eb240180220148a7d5a4e29b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Nov 2022 10:35:09 +0100 Subject: [PATCH 15/89] Improve temporary log output for exchange_ws --- freqtrade/exchange/exchange_ws.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 70deace53..afa0b6de3 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -93,6 +93,10 @@ class ExchangeWS(): candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) # Fake 1 candle - which is then removed again # TODO: is this really a good idea?? - candles.append([candles[-1][0], 0, 0, 0, 0, 0]) - logger.info(f"watch result for {pair}, {timeframe} with length {len(candles)}") + refresh_time = int(self.pairs_last_refresh[(pair, timeframe, candle_type)] * 1000) + candles.append([refresh_time, 0, 0, 0, 0, 0]) + logger.info( + f"watch result for {pair}, {timeframe} with length {len(candles)}, " + f"{datetime.fromtimestamp(candles[-1][0] // 1000)}, " + f"lref={datetime.fromtimestamp(self.pairs_last_refresh[(pair, timeframe, candle_type)])}") return pair, timeframe, candle_type, candles From 60cfda5d5236484149ffef5b1dc1d16192f235cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Nov 2022 07:07:15 +0100 Subject: [PATCH 16/89] Add very basic exception handling --- freqtrade/exchange/exchange_ws.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index afa0b6de3..b5bbb663d 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -6,6 +6,8 @@ from datetime import datetime from threading import Thread from typing import Dict, List, Set, Tuple +import ccxt + from freqtrade.constants import Config from freqtrade.enums.candletype import CandleType from freqtrade.exchange.exchange import timeframe_to_seconds @@ -70,13 +72,18 @@ class ExchangeWS(): async def continuously_async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType) -> None: - - while (pair, timeframe, candle_type) in self._pairs_watching: - start = time.time() - data = await self.ccxt_object.watch_ohlcv(pair, timeframe) - self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() - # logger.info( - # f"watch done {pair}, {timeframe}, data {len(data)} in {time.time() - start:.2f}s") + try: + while (pair, timeframe, candle_type) in self._pairs_watching: + start = time.time() + data = await self.ccxt_object.watch_ohlcv(pair, timeframe) + self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() + # logger.info( + # f"watch done {pair}, {timeframe}, data {len(data)} " + # f"in {time.time() - start:.2f}s") + except ccxt.BaseError: + logger.exception("Exception in continuously_async_watch_ohlcv") + finally: + self._pairs_watching.discard((pair, timeframe, candle_type)) def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: self._pairs_watching.add((pair, timeframe, candle_type)) From f4f8b910fe05b824b5bff20dbdabaf64a4377725 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Nov 2022 06:46:14 +0100 Subject: [PATCH 17/89] Improve exchange_ws terminology --- freqtrade/exchange/exchange.py | 2 +- freqtrade/exchange/exchange_ws.py | 43 +++++++++++++++++-------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9d1a2d797..64d048b1d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2249,7 +2249,7 @@ class Exchange: if self._exchange_ws: candle_date = int(timeframe_to_prev_date(timeframe).timestamp()) candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) - x = self._exchange_ws.pairs_last_refresh[(pair, timeframe, candle_type)] + x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type)) logger.info(f"{candle_date < x}, {candle_date}, {x}") if candles and candles[-1][0] > min_date and candle_date < x: # Usable result ... diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index b5bbb663d..1b685b116 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -20,22 +20,22 @@ class ExchangeWS(): def __init__(self, config: Config, ccxt_object) -> None: self.config = config self.ccxt_object = ccxt_object - self._thread = Thread(name="ccxt_ws", target=self.start) + self._thread = Thread(name="ccxt_ws", target=self.__start_forever) self._background_tasks: Set[asyncio.Task] = set() - self._pairs_watching: Set[Tuple[str, str, CandleType]] = set() - self._pairs_scheduled: Set[Tuple[str, str, CandleType]] = set() - self.pairs_last_refresh: Dict[Tuple[str, str, CandleType], float] = {} - self.pairs_last_request: Dict[Tuple[str, str, CandleType], float] = {} + self._klines_watching: Set[Tuple[str, str, CandleType]] = set() + self._klines_scheduled: Set[Tuple[str, str, CandleType]] = set() + self.klines_last_refresh: Dict[Tuple[str, str, CandleType], float] = {} + self.klines_last_request: Dict[Tuple[str, str, CandleType], float] = {} self._thread.start() - def start(self) -> None: + def __start_forever(self) -> None: self._loop = asyncio.new_event_loop() self._loop.run_forever() def cleanup(self) -> None: logger.debug("Cleanup called - stopping") - self._pairs_watching.clear() + self._klines_watching.clear() self._loop.stop() self._thread.join() logger.debug("Stopped") @@ -45,19 +45,19 @@ class ExchangeWS(): Remove pairs from watchlist if they've not been requested within the last timeframe (+ offset) """ - for p in list(self._pairs_watching): + for p in list(self._klines_watching): _, timeframe, _ = p timeframe_s = timeframe_to_seconds(timeframe) - last_refresh = self.pairs_last_request.get(p, 0) + last_refresh = self.klines_last_request.get(p, 0) if last_refresh > 0 and time.time() - last_refresh > timeframe_s + 20: logger.info(f"Removing {p} from watchlist") - self._pairs_watching.discard(p) + self._klines_watching.discard(p) async def schedule_while_true(self) -> None: - for p in self._pairs_watching: - if p not in self._pairs_scheduled: - self._pairs_scheduled.add(p) + for p in self._klines_watching: + if p not in self._klines_scheduled: + self._klines_scheduled.add(p) pair, timeframe, candle_type = p task = asyncio.create_task( self.continuously_async_watch_ohlcv(pair, timeframe, candle_type)) @@ -73,21 +73,24 @@ class ExchangeWS(): async def continuously_async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType) -> None: try: - while (pair, timeframe, candle_type) in self._pairs_watching: + while (pair, timeframe, candle_type) in self._klines_watching: start = time.time() data = await self.ccxt_object.watch_ohlcv(pair, timeframe) - self.pairs_last_refresh[(pair, timeframe, candle_type)] = time.time() + self.klines_last_refresh[(pair, timeframe, candle_type)] = time.time() # logger.info( # f"watch done {pair}, {timeframe}, data {len(data)} " # f"in {time.time() - start:.2f}s") except ccxt.BaseError: logger.exception("Exception in continuously_async_watch_ohlcv") finally: - self._pairs_watching.discard((pair, timeframe, candle_type)) + self._klines_watching.discard((pair, timeframe, candle_type)) def schedule_ohlcv(self, pair: str, timeframe: str, candle_type: CandleType) -> None: - self._pairs_watching.add((pair, timeframe, candle_type)) - self.pairs_last_request[(pair, timeframe, candle_type)] = time.time() + """ + Schedule a pair/timeframe combination to be watched + """ + self._klines_watching.add((pair, timeframe, candle_type)) + self.klines_last_request[(pair, timeframe, candle_type)] = time.time() # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) asyncio.run_coroutine_threadsafe(self.schedule_while_true(), loop=self._loop) self.cleanup_expired() @@ -100,10 +103,10 @@ class ExchangeWS(): candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) # Fake 1 candle - which is then removed again # TODO: is this really a good idea?? - refresh_time = int(self.pairs_last_refresh[(pair, timeframe, candle_type)] * 1000) + refresh_time = int(self.klines_last_refresh[(pair, timeframe, candle_type)] * 1000) candles.append([refresh_time, 0, 0, 0, 0, 0]) logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " f"{datetime.fromtimestamp(candles[-1][0] // 1000)}, " - f"lref={datetime.fromtimestamp(self.pairs_last_refresh[(pair, timeframe, candle_type)])}") + f"lref={datetime.fromtimestamp(self.klines_last_refresh[(pair, timeframe, candle_type)])}") return pair, timeframe, candle_type, candles From aef0324aa7bbce5ca4600c001642442c6f5a5fda Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Nov 2022 19:54:52 +0100 Subject: [PATCH 18/89] set markets for ws exchange on reload --- freqtrade/exchange/exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 64d048b1d..370225152 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -576,6 +576,9 @@ class Exchange: self._markets = self._api.load_markets(reload=True, params={}) # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) + if self._exchange_ws: + # Set markets to avoid reloading on websocket api + self._ws_async.set_markets(self._api.markets, self._api.currencies) self._last_markets_refresh = dt_ts() self.fill_leverage_tiers() except ccxt.BaseError: From 8a00bf3188ad9198d8be8551d5318e01aed69cef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Nov 2022 19:46:50 +0100 Subject: [PATCH 19/89] Use proper typehint --- freqtrade/exchange/exchange.py | 2 +- freqtrade/exchange/exchange_ws.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 370225152..5b6b7a5d6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2292,7 +2292,7 @@ class Exchange: def _build_ohlcv_dl_jobs( self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int], cache: bool - ) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]: + ) -> Tuple[List[Coroutine], List[PairWithTimeframe]]: """ Build Coroutines to execute as part of refresh_latest_ohlcv """ diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 1b685b116..6401c10b9 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -8,7 +8,7 @@ from typing import Dict, List, Set, Tuple import ccxt -from freqtrade.constants import Config +from freqtrade.constants import Config, PairWithTimeframe from freqtrade.enums.candletype import CandleType from freqtrade.exchange.exchange import timeframe_to_seconds @@ -23,10 +23,10 @@ class ExchangeWS(): self._thread = Thread(name="ccxt_ws", target=self.__start_forever) self._background_tasks: Set[asyncio.Task] = set() - self._klines_watching: Set[Tuple[str, str, CandleType]] = set() - self._klines_scheduled: Set[Tuple[str, str, CandleType]] = set() - self.klines_last_refresh: Dict[Tuple[str, str, CandleType], float] = {} - self.klines_last_request: Dict[Tuple[str, str, CandleType], float] = {} + self._klines_watching: Set[PairWithTimeframe] = set() + self._klines_scheduled: Set[PairWithTimeframe] = set() + self.klines_last_refresh: Dict[PairWithTimeframe, float] = {} + self.klines_last_request: Dict[PairWithTimeframe, float] = {} self._thread.start() def __start_forever(self) -> None: From dadc96306fa6a7f1f9f07cc4e5b58d98a20899cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Nov 2022 19:49:01 +0100 Subject: [PATCH 20/89] Better define what interface is external --- freqtrade/exchange/exchange_ws.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 6401c10b9..6144de2a7 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -53,24 +53,24 @@ class ExchangeWS(): logger.info(f"Removing {p} from watchlist") self._klines_watching.discard(p) - async def schedule_while_true(self) -> None: + async def _schedule_while_true(self) -> None: for p in self._klines_watching: if p not in self._klines_scheduled: self._klines_scheduled.add(p) pair, timeframe, candle_type = p task = asyncio.create_task( - self.continuously_async_watch_ohlcv(pair, timeframe, candle_type)) + self._continuously_async_watch_ohlcv(pair, timeframe, candle_type)) self._background_tasks.add(task) - task.add_done_callback(self.continuous_stopped) + task.add_done_callback(self._continuous_stopped) - def continuous_stopped(self, task: asyncio.Task): + def _continuous_stopped(self, task: asyncio.Task): self._background_tasks.discard(task) result = task.result() logger.info(f"Task finished {result}") # self._pairs_scheduled.discard(pair, timeframe, candle_type) - async def continuously_async_watch_ohlcv( + async def _continuously_async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType) -> None: try: while (pair, timeframe, candle_type) in self._klines_watching: @@ -92,7 +92,7 @@ class ExchangeWS(): self._klines_watching.add((pair, timeframe, candle_type)) self.klines_last_request[(pair, timeframe, candle_type)] = time.time() # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) - asyncio.run_coroutine_threadsafe(self.schedule_while_true(), loop=self._loop) + asyncio.run_coroutine_threadsafe(self._schedule_while_true(), loop=self._loop) self.cleanup_expired() async def get_ohlcv( From e8b4bcc65d16ccfa7a84b606d030fc37ff8fe8cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Nov 2022 19:50:29 +0100 Subject: [PATCH 21/89] use default argument --- 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 5b6b7a5d6..41882674c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2252,7 +2252,7 @@ class Exchange: if self._exchange_ws: candle_date = int(timeframe_to_prev_date(timeframe).timestamp()) candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) - x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type)) + x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) logger.info(f"{candle_date < x}, {candle_date}, {x}") if candles and candles[-1][0] > min_date and candle_date < x: # Usable result ... From f223319909cf06ed69d1572edb8a6fe0d57b3aeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Nov 2022 20:54:00 +0100 Subject: [PATCH 22/89] Improve typehint for ohlcv endpoint --- freqtrade/exchange/exchange_ws.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 6144de2a7..afe65765b 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) class ExchangeWS(): - def __init__(self, config: Config, ccxt_object) -> None: + def __init__(self, config: Config, ccxt_object: ccxt.Exchange) -> None: self.config = config self.ccxt_object = ccxt_object self._thread = Thread(name="ccxt_ws", target=self.__start_forever) @@ -96,7 +96,11 @@ class ExchangeWS(): self.cleanup_expired() async def get_ohlcv( - self, pair: str, timeframe: str, candle_type: CandleType) -> Tuple[str, str, str, List]: + self, + pair: str, + timeframe: str, + candle_type: CandleType + ) -> Tuple[str, str, CandleType, List]: """ Returns cached klines from ccxt's "watch" cache. """ From ec6c54367b8e4b6151314a74a5b33579645e1aee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Nov 2022 06:42:48 +0100 Subject: [PATCH 23/89] Add exchange_ws test case --- freqtrade/exchange/exchange_ws.py | 9 +++++---- tests/exchange/test_exchange_ws.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/exchange/test_exchange_ws.py diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index afe65765b..7b4209cb6 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -4,7 +4,7 @@ import logging import time from datetime import datetime from threading import Thread -from typing import Dict, List, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple import ccxt @@ -20,23 +20,24 @@ class ExchangeWS(): def __init__(self, config: Config, ccxt_object: ccxt.Exchange) -> None: self.config = config self.ccxt_object = ccxt_object - self._thread = Thread(name="ccxt_ws", target=self.__start_forever) self._background_tasks: Set[asyncio.Task] = set() self._klines_watching: Set[PairWithTimeframe] = set() self._klines_scheduled: Set[PairWithTimeframe] = set() self.klines_last_refresh: Dict[PairWithTimeframe, float] = {} self.klines_last_request: Dict[PairWithTimeframe, float] = {} + self._thread = Thread(name="ccxt_ws", target=self._start_forever) self._thread.start() - def __start_forever(self) -> None: + def _start_forever(self) -> None: self._loop = asyncio.new_event_loop() self._loop.run_forever() def cleanup(self) -> None: logger.debug("Cleanup called - stopping") self._klines_watching.clear() - self._loop.stop() + if hasattr(self, '_loop'): + self._loop.stop() self._thread.join() logger.debug("Stopped") diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py new file mode 100644 index 000000000..d119230f1 --- /dev/null +++ b/tests/exchange/test_exchange_ws.py @@ -0,0 +1,30 @@ + + +from time import sleep +from unittest.mock import MagicMock + +from freqtrade.exchange.exchange_ws import ExchangeWS + + +def test_exchangews_init(mocker): + + config = MagicMock() + ccxt_object = MagicMock() + mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) + + exchange_ws = ExchangeWS(config, ccxt_object) + + assert exchange_ws.config == config + assert exchange_ws.ccxt_object == ccxt_object + assert exchange_ws._thread.name == "ccxt_ws" + assert exchange_ws._background_tasks == set() + assert exchange_ws._klines_watching == set() + assert exchange_ws._klines_scheduled == set() + assert exchange_ws.klines_last_refresh == {} + assert exchange_ws.klines_last_request == {} + assert exchange_ws._ob_watching == set() + assert exchange_ws._ob_scheduled == set() + assert exchange_ws.ob_last_request == {} + sleep(0.1) + # Cleanup + exchange_ws.cleanup() From 3d6cef3555413a0fde1733c3f4f2158bd07f8c8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 29 Nov 2022 13:51:40 +0100 Subject: [PATCH 24/89] ccxt.pro - first attempt at test --- tests/exchange/test_exchange_ws.py | 48 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index d119230f1..d0c8e182f 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -1,8 +1,11 @@ +import asyncio +import threading from time import sleep -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock +from freqtrade.enums import CandleType from freqtrade.exchange.exchange_ws import ExchangeWS @@ -13,6 +16,7 @@ def test_exchangews_init(mocker): mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) exchange_ws = ExchangeWS(config, ccxt_object) + sleep(0.1) assert exchange_ws.config == config assert exchange_ws.ccxt_object == ccxt_object @@ -25,6 +29,46 @@ def test_exchangews_init(mocker): assert exchange_ws._ob_watching == set() assert exchange_ws._ob_scheduled == set() assert exchange_ws.ob_last_request == {} - sleep(0.1) # Cleanup exchange_ws.cleanup() + + +def patch_eventloop_threading(exchange): + is_init = False + + def thread_fuck(): + nonlocal is_init + exchange._loop = asyncio.new_event_loop() + is_init = True + exchange._loop.run_forever() + x = threading.Thread(target=thread_fuck, daemon=True) + x.start() + while not is_init: + pass + + +async def test_exchangews_ohlcv(mocker): + config = MagicMock() + ccxt_object = MagicMock() + ccxt_object.watch_ohlcv = AsyncMock() + mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) + + exchange_ws = ExchangeWS(config, ccxt_object) + patch_eventloop_threading(exchange_ws) + try: + + assert exchange_ws._klines_watching == set() + assert exchange_ws._klines_scheduled == set() + + exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) + sleep(.5) + + assert exchange_ws._klines_watching == {("ETH/BTC", "1m", CandleType.SPOT)} + assert exchange_ws._klines_scheduled == {("ETH/BTC", "1m", CandleType.SPOT)} + sleep(.1) + assert ccxt_object.watch_ohlcv.call_count == 1 + except Exception as e: + print(e) + finally: + # Cleanup + exchange_ws.cleanup() From fcaee33706b795927a672481189cbb3387c88994 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Dec 2022 20:11:43 +0100 Subject: [PATCH 25/89] Improve log msg --- 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 41882674c..47bd1bdb6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2253,7 +2253,7 @@ class Exchange: candle_date = int(timeframe_to_prev_date(timeframe).timestamp()) candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) - logger.info(f"{candle_date < x}, {candle_date}, {x}") + logger.info(f"{pair}, {candle_date < x}, {candle_date}, {x}") if candles and candles[-1][0] > min_date and candle_date < x: # Usable result ... logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") From 0b620817a2374624c0e588f0042aab7362d482df Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Dec 2022 21:24:21 +0100 Subject: [PATCH 26/89] Don't append fake candle --- freqtrade/exchange/exchange_ws.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 7b4209cb6..175ebdba7 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -106,12 +106,12 @@ class ExchangeWS(): Returns cached klines from ccxt's "watch" cache. """ candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) - # Fake 1 candle - which is then removed again - # TODO: is this really a good idea?? refresh_time = int(self.klines_last_refresh[(pair, timeframe, candle_type)] * 1000) - candles.append([refresh_time, 0, 0, 0, 0, 0]) logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " f"{datetime.fromtimestamp(candles[-1][0] // 1000)}, " f"lref={datetime.fromtimestamp(self.klines_last_refresh[(pair, timeframe, candle_type)])}") + # Fake 1 candle - which is then removed again + # TODO: is this really a good idea?? + # candles.append([refresh_time, 0, 0, 1, 2, 0]) return pair, timeframe, candle_type, candles From f90574abee9f1f5a2f19f2e0396c1568337dbfa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 5 Jan 2023 11:53:37 +0100 Subject: [PATCH 27/89] use OHLCVResponse in ws --- freqtrade/exchange/exchange_ws.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 175ebdba7..e68c2e3c4 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -4,13 +4,14 @@ import logging import time from datetime import datetime from threading import Thread -from typing import Dict, List, Optional, Set, Tuple +from typing import Dict, Set import ccxt from freqtrade.constants import Config, PairWithTimeframe from freqtrade.enums.candletype import CandleType from freqtrade.exchange.exchange import timeframe_to_seconds +from freqtrade.exchange.types import OHLCVResponse logger = logging.getLogger(__name__) @@ -101,7 +102,7 @@ class ExchangeWS(): pair: str, timeframe: str, candle_type: CandleType - ) -> Tuple[str, str, CandleType, List]: + ) -> OHLCVResponse: """ Returns cached klines from ccxt's "watch" cache. """ From 55ed505f94373e97ad5e71ad1b97bb08e6738a14 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Aug 2023 18:11:25 +0200 Subject: [PATCH 28/89] Update exchange_ws get_ohlcv logic --- freqtrade/exchange/exchange.py | 3 +-- freqtrade/exchange/exchange_ws.py | 20 +++++++++++++------- tests/exchange/test_exchange_ws.py | 2 -- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 47bd1bdb6..31d95aaa5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,6 @@ import asyncio import inspect import logging import signal -import time from copy import deepcopy from datetime import datetime, timedelta, timezone from math import floor, isnan @@ -2258,7 +2257,7 @@ class Exchange: # Usable result ... logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") - return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type) + return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) # Check if 1 call can get us updated candles without hole in the data. if min_date < last_refresh: diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index e68c2e3c4..08fd3ae8d 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -101,18 +101,24 @@ class ExchangeWS(): self, pair: str, timeframe: str, - candle_type: CandleType + candle_type: CandleType, + candle_date: int, ) -> OHLCVResponse: """ Returns cached klines from ccxt's "watch" cache. + :param candle_date: timestamp of the end-time of the candle. """ candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) - refresh_time = int(self.klines_last_refresh[(pair, timeframe, candle_type)] * 1000) + refresh_date = self.klines_last_refresh[(pair, timeframe, candle_type)] + drop_hint = False + if refresh_date > candle_date: + # Refreshed after candle was complete. + logger.info(f"{candles[-1][0] // 1000} >= {candle_date}") + drop_hint = (candles[-1][0] // 1000) >= candle_date logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " f"{datetime.fromtimestamp(candles[-1][0] // 1000)}, " - f"lref={datetime.fromtimestamp(self.klines_last_refresh[(pair, timeframe, candle_type)])}") - # Fake 1 candle - which is then removed again - # TODO: is this really a good idea?? - # candles.append([refresh_time, 0, 0, 1, 2, 0]) - return pair, timeframe, candle_type, candles + f"lref={datetime.fromtimestamp(self.klines_last_refresh[(pair, timeframe, candle_type)])}" + f"candle_date={datetime.fromtimestamp(candle_date)}, {drop_hint=}" + ) + return pair, timeframe, candle_type, candles, drop_hint diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index d0c8e182f..f3a6bb0de 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -1,5 +1,3 @@ - - import asyncio import threading from time import sleep From 67a6c11f6d1596b5a285e0fabb7007a426264853 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 06:46:24 +0200 Subject: [PATCH 29/89] No longer import ccxt.async_support --- freqtrade/exchange/exchange.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 31d95aaa5..8b5668eb4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,7 +14,6 @@ from threading import Lock from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union import ccxt -import ccxt.async_support as ccxt_async import ccxt.pro as ccxt_pro from cachetools import TTLCache from ccxt import TICK_SIZE @@ -152,7 +151,7 @@ class Exchange: :return: None """ self._api: ccxt.Exchange - self._api_async: ccxt_async.Exchange = None + self._api_async: ccxt_pro.Exchange = None self._markets: Dict = {} self._trading_fees: Dict[str, Any] = {} self._leverage_tiers: Dict[str, List[Dict]] = {} @@ -232,7 +231,7 @@ class Exchange: self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._has_watch_ohlcv = self.exchange_has("watchOHLCV") self._exchange_ws: Optional[ExchangeWS] = None - if exchange_config.get("enable_ws", True) and self._has_watch_ohlcv: + if exchange_conf.get("enable_ws", True) and self._has_watch_ohlcv: self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') From e0b4e16d195b560c9a2bec67a02852e723844ff8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 06:46:31 +0200 Subject: [PATCH 30/89] Remove ob_test stuff --- tests/exchange/test_exchange_ws.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index f3a6bb0de..c6684b250 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -24,9 +24,6 @@ def test_exchangews_init(mocker): assert exchange_ws._klines_scheduled == set() assert exchange_ws.klines_last_refresh == {} assert exchange_ws.klines_last_request == {} - assert exchange_ws._ob_watching == set() - assert exchange_ws._ob_scheduled == set() - assert exchange_ws.ob_last_request == {} # Cleanup exchange_ws.cleanup() From 4832c10973cd8bf205cec1cfaee1bcc7760392a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 18:12:09 +0200 Subject: [PATCH 31/89] Only import ccxt.async when necessary --- freqtrade/exchange/exchange.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8b5668eb4..2e8ff3ec4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -307,6 +307,7 @@ class Exchange: ccxt_module = ccxt_pro if not is_exchange_known_ccxt(name, ccxt_module): # Fall back to async if pro doesn't support this exchange + import ccxt.async_support as ccxt_async ccxt_module = ccxt_async if not is_exchange_known_ccxt(name, ccxt_module): From c18b6cdb748380abff75aac29691f6daac3bdb18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 20:48:14 +0200 Subject: [PATCH 32/89] Improve stop behavior --- freqtrade/exchange/exchange_ws.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 08fd3ae8d..ef76774dd 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -32,13 +32,18 @@ class ExchangeWS(): def _start_forever(self) -> None: self._loop = asyncio.new_event_loop() - self._loop.run_forever() + try: + self._loop.run_forever() + finally: + if self._loop.is_running(): + self._loop.stop() def cleanup(self) -> None: logger.debug("Cleanup called - stopping") self._klines_watching.clear() if hasattr(self, '_loop'): - self._loop.stop() + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join() logger.debug("Stopped") From 55bd7db022351cc9b5b0884c1c2841abfdc6ec33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 21:23:57 +0200 Subject: [PATCH 33/89] Don't forget to close WS session --- freqtrade/exchange/exchange.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2e8ff3ec4..8fb041c48 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -271,6 +271,11 @@ class Exchange: ): logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + if (self._ws_async and inspect.iscoroutinefunction(self._ws_async.close) + and self._ws_async.session): + logger.debug("Closing ws ccxt session.") + self.loop.run_until_complete(self._ws_async.close()) + if self.loop and not self.loop.is_closed(): self.loop.close() From 8375209a8efe27e5fc414ede999e6e02b9196cc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 21:25:23 +0200 Subject: [PATCH 34/89] Add fixtures for exchange_ws --- freqtrade/exchange/exchange_ws.py | 2 +- tests/exchange_online/conftest.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index ef76774dd..ea3b38763 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -17,7 +17,7 @@ from freqtrade.exchange.types import OHLCVResponse logger = logging.getLogger(__name__) -class ExchangeWS(): +class ExchangeWS: def __init__(self, config: Config, ccxt_object: ccxt.Exchange) -> None: self.config = config self.ccxt_object = ccxt_object diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 8820ce3e7..91adba70e 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -376,7 +376,7 @@ def get_exchange(exchange_name, exchange_conf): exchange_conf, validate=True, load_leverage_tiers=True ) - yield exchange, exchange_name + return exchange, exchange_name def get_futures_exchange(exchange_name, exchange_conf, class_mocker): @@ -398,15 +398,25 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker): class_mocker.patch(f"{EXMS}.load_cached_leverage_tiers", return_value=None) class_mocker.patch(f"{EXMS}.cache_leverage_tiers") - yield from get_exchange(exchange_name, exchange_conf) + return get_exchange(exchange_name, exchange_conf) @pytest.fixture(params=EXCHANGES, scope="class") def exchange(request, exchange_conf, class_mocker): class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") - yield from get_exchange(request.param, exchange_conf) + return get_exchange(request.param, exchange_conf) @pytest.fixture(params=EXCHANGES, scope="class") def exchange_futures(request, exchange_conf, class_mocker): - yield from get_futures_exchange(request.param, exchange_conf, class_mocker) + return get_futures_exchange(request.param, exchange_conf, class_mocker) + + +@pytest.fixture(params=EXCHANGES, scope="class") +def exchange_ws(request, exchange_conf): + exchange_conf["exchange"]["enable_ws"] = True + exchange, name = get_exchange(request.param, exchange_conf) + if not exchange._has_watch_ohlcv: + pytest.skip("Exchange does not support watch_ohlcv.") + yield exchange, name + exchange.close() From a8351775976226738452ff05ea68ca4c53fae313 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Aug 2023 21:25:50 +0200 Subject: [PATCH 35/89] Initial swat at online WS test --- tests/exchange_online/test_ccxt_ws_compat.py | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/exchange_online/test_ccxt_ws_compat.py diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py new file mode 100644 index 000000000..5c1d0845a --- /dev/null +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -0,0 +1,56 @@ +""" +Tests in this file do NOT mock network calls, so they are expected to be fluky at times. + +However, these tests aim to test ccxt compatibility, specifically regarding websockets. +""" + +import logging +from datetime import timedelta + +import pytest + +from freqtrade.enums import CandleType +from freqtrade.exchange.exchange_utils import (timeframe_to_minutes, timeframe_to_next_date, + timeframe_to_prev_date) +from freqtrade.util.datetime_helpers import dt_now +from tests.conftest import log_has_re +from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES + + +@pytest.mark.longrun +class TestCCXTExchangeWs: + + def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_FIXTURE_TYPE, caplog): + exch, exchangename = exchange_ws + + assert exch._ws_async is not None + pair = EXCHANGES[exchangename]['pair'] + timeframe = '1m' + pair_tf = (pair, timeframe, CandleType.SPOT) + + res = exch.refresh_latest_ohlcv([pair_tf]) + now = dt_now() - timedelta(minutes=(timeframe_to_minutes(timeframe) * 1.1)) + # Currently closed candle + curr_candle = timeframe_to_prev_date(timeframe, now) + # Currently open candle + next_candle = timeframe_to_next_date(timeframe, now) + assert pair_tf in exch._exchange_ws._klines_watching + assert pair_tf in exch._exchange_ws._klines_scheduled + assert res[pair_tf] is not None + df1 = res[pair_tf] + caplog.set_level(logging.DEBUG) + assert df1.iloc[-1]['date'] == curr_candle + + # Wait until the next candle (might be up to 1 minute). + while True: + caplog.clear() + res = exch.refresh_latest_ohlcv([pair_tf]) + df2 = res[pair_tf] + assert df2 is not None + if df2.iloc[-1]['date'] == next_candle: + break + assert df2.iloc[-1]['date'] == curr_candle + + assert log_has_re(r"watch result.*", caplog) + + From 35e2e58a5c62271b0d96083f19948084210b244f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Aug 2023 06:47:14 +0200 Subject: [PATCH 36/89] Improve formatting --- freqtrade/exchange/exchange_ws.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index ea3b38763..c108d37d8 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -2,7 +2,6 @@ import asyncio import logging import time -from datetime import datetime from threading import Thread from typing import Dict, Set @@ -12,6 +11,7 @@ from freqtrade.constants import Config, PairWithTimeframe from freqtrade.enums.candletype import CandleType from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.exchange.types import OHLCVResponse +from freqtrade.util.datetime_helpers import dt_from_ts logger = logging.getLogger(__name__) @@ -84,9 +84,9 @@ class ExchangeWS: start = time.time() data = await self.ccxt_object.watch_ohlcv(pair, timeframe) self.klines_last_refresh[(pair, timeframe, candle_type)] = time.time() - # logger.info( - # f"watch done {pair}, {timeframe}, data {len(data)} " - # f"in {time.time() - start:.2f}s") + logger.debug( + f"watch done {pair}, {timeframe}, data {len(data)} " + f"in {time.time() - start:.2f}s") except ccxt.BaseError: logger.exception("Exception in continuously_async_watch_ohlcv") finally: @@ -122,8 +122,8 @@ class ExchangeWS: drop_hint = (candles[-1][0] // 1000) >= candle_date logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " - f"{datetime.fromtimestamp(candles[-1][0] // 1000)}, " - f"lref={datetime.fromtimestamp(self.klines_last_refresh[(pair, timeframe, candle_type)])}" - f"candle_date={datetime.fromtimestamp(candle_date)}, {drop_hint=}" + f"{dt_from_ts(candles[-1][0] // 1000)}, " + f"lref={dt_from_ts(self.klines_last_refresh[(pair, timeframe, candle_type)])}" + f"candle_date={dt_from_ts(candle_date)}, {drop_hint=}" ) return pair, timeframe, candle_type, candles, drop_hint From f9ce0bb9ab11622494249710a627b5b07fe3b4a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Aug 2023 07:30:07 +0200 Subject: [PATCH 37/89] Improve exchange formatting --- freqtrade/exchange/exchange.py | 11 ++++++++--- tests/exchange_online/test_ccxt_ws_compat.py | 1 - 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8fb041c48..368b93a03 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -271,8 +271,11 @@ class Exchange: ): logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) - if (self._ws_async and inspect.iscoroutinefunction(self._ws_async.close) - and self._ws_async.session): + if ( + self._ws_async + and inspect.iscoroutinefunction(self._ws_async.close) + and self._ws_async.session + ): logger.debug("Closing ws ccxt session.") self.loop.run_until_complete(self._ws_async.close()) @@ -313,6 +316,7 @@ class Exchange: if not is_exchange_known_ccxt(name, ccxt_module): # Fall back to async if pro doesn't support this exchange import ccxt.async_support as ccxt_async + ccxt_module = ccxt_async if not is_exchange_known_ccxt(name, ccxt_module): @@ -2248,6 +2252,7 @@ class Exchange: if self._has_watch_ohlcv and self._exchange_ws: # Subscribe to websocket self._exchange_ws.schedule_ohlcv(pair, timeframe, candle_type) + if cache and (pair, timeframe, candle_type) in self._klines: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) min_date = int(date_minus_candles(timeframe, candle_limit - 5).timestamp()) @@ -2259,7 +2264,7 @@ class Exchange: x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) logger.info(f"{pair}, {candle_date < x}, {candle_date}, {x}") if candles and candles[-1][0] > min_date and candle_date < x: - # Usable result ... + # Usable result, update happened after prior candle end date logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index 5c1d0845a..c452338d0 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -53,4 +53,3 @@ class TestCCXTExchangeWs: assert log_has_re(r"watch result.*", caplog) - From bd9ebe4a72303706fb7c731b4147acad5eb456d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Nov 2023 06:23:26 +0100 Subject: [PATCH 38/89] Improve ccxt.ws live test --- tests/exchange_online/test_ccxt_ws_compat.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index c452338d0..0d8f8c7a9 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -6,12 +6,14 @@ However, these tests aim to test ccxt compatibility, specifically regarding webs import logging from datetime import timedelta +from time import sleep import pytest from freqtrade.enums import CandleType from freqtrade.exchange.exchange_utils import (timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date) +from freqtrade.loggers.set_log_levels import set_loggers from freqtrade.util.datetime_helpers import dt_now from tests.conftest import log_has_re from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES @@ -20,25 +22,31 @@ from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES @pytest.mark.longrun class TestCCXTExchangeWs: - def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_FIXTURE_TYPE, caplog): + def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_FIXTURE_TYPE, caplog, mocker): exch, exchangename = exchange_ws assert exch._ws_async is not None pair = EXCHANGES[exchangename]['pair'] timeframe = '1m' pair_tf = (pair, timeframe, CandleType.SPOT) + m_hist = mocker.spy(exch, '_async_get_historic_ohlcv') + m_cand = mocker.spy(exch, '_async_get_candle_history') res = exch.refresh_latest_ohlcv([pair_tf]) - now = dt_now() - timedelta(minutes=(timeframe_to_minutes(timeframe) * 1.1)) + assert m_cand.call_count == 1 + + # Currently open candle + next_candle = timeframe_to_prev_date(timeframe, dt_now()) + now = next_candle - timedelta(seconds=1) # Currently closed candle curr_candle = timeframe_to_prev_date(timeframe, now) - # Currently open candle - next_candle = timeframe_to_next_date(timeframe, now) + assert pair_tf in exch._exchange_ws._klines_watching assert pair_tf in exch._exchange_ws._klines_scheduled assert res[pair_tf] is not None df1 = res[pair_tf] caplog.set_level(logging.DEBUG) + set_loggers(1) assert df1.iloc[-1]['date'] == curr_candle # Wait until the next candle (might be up to 1 minute). @@ -50,6 +58,8 @@ class TestCCXTExchangeWs: if df2.iloc[-1]['date'] == next_candle: break assert df2.iloc[-1]['date'] == curr_candle + sleep(1) + assert m_hist.call_count == 0 + assert m_cand.call_count == 1 assert log_has_re(r"watch result.*", caplog) - From e3887a33b9b677d739290940141fe7611c5a943a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Nov 2023 06:40:58 +0100 Subject: [PATCH 39/89] Add Helping comment to ws_compat_tests --- tests/exchange_online/test_ccxt_ws_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index 0d8f8c7a9..7add576d8 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -61,5 +61,6 @@ class TestCCXTExchangeWs: sleep(1) assert m_hist.call_count == 0 + # shouldn't have tried fetch_ohlcv a second time. assert m_cand.call_count == 1 assert log_has_re(r"watch result.*", caplog) From e31d8313f2dd1032a3317c1bdf01bc8cfafb0380 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Nov 2023 06:55:02 +0100 Subject: [PATCH 40/89] Improve ccxt_ws test setup --- tests/exchange_online/conftest.py | 21 +++++++++++++++++--- tests/exchange_online/test_ccxt_ws_compat.py | 10 ++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 91adba70e..80499c8d9 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -11,6 +11,8 @@ from tests.conftest import EXMS, get_default_conf_usdt EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] +EXCHANGE_WS_FIXTURE_TYPE = Tuple[Exchange, str, str] + # Exchanges that should be tested online EXCHANGES = { @@ -412,11 +414,24 @@ def exchange_futures(request, exchange_conf, class_mocker): return get_futures_exchange(request.param, exchange_conf, class_mocker) +@pytest.fixture(params=["spot", "futures"], scope="class") +def exchange_mode(request): + return request.param + + @pytest.fixture(params=EXCHANGES, scope="class") -def exchange_ws(request, exchange_conf): +def exchange_ws(request, exchange_conf, exchange_mode, class_mocker): exchange_conf["exchange"]["enable_ws"] = True - exchange, name = get_exchange(request.param, exchange_conf) + if exchange_mode == "spot": + exchange, name = get_exchange(request.param, exchange_conf) + pair = EXCHANGES[request.param]["pair"] + else: + exchange, name = get_futures_exchange( + request.param, exchange_conf, class_mocker=class_mocker + ) + pair = EXCHANGES[request.param]["futures_pair"] + if not exchange._has_watch_ohlcv: pytest.skip("Exchange does not support watch_ohlcv.") - yield exchange, name + yield exchange, name, pair exchange.close() diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index 7add576d8..c99c3db1d 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -11,22 +11,20 @@ from time import sleep import pytest from freqtrade.enums import CandleType -from freqtrade.exchange.exchange_utils import (timeframe_to_minutes, timeframe_to_next_date, - timeframe_to_prev_date) +from freqtrade.exchange.exchange_utils import timeframe_to_prev_date from freqtrade.loggers.set_log_levels import set_loggers from freqtrade.util.datetime_helpers import dt_now from tests.conftest import log_has_re -from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES +from tests.exchange_online.conftest import EXCHANGE_WS_FIXTURE_TYPE @pytest.mark.longrun class TestCCXTExchangeWs: - def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_FIXTURE_TYPE, caplog, mocker): - exch, exchangename = exchange_ws + def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_WS_FIXTURE_TYPE, caplog, mocker): + exch, exchangename, pair = exchange_ws assert exch._ws_async is not None - pair = EXCHANGES[exchangename]['pair'] timeframe = '1m' pair_tf = (pair, timeframe, CandleType.SPOT) m_hist = mocker.spy(exch, '_async_get_historic_ohlcv') From 137ddb2ec33b330c0c7f8ff16433f78198693737 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Nov 2023 06:56:32 +0100 Subject: [PATCH 41/89] Require opt-in for ws enablement to allow slow rollout --- freqtrade/exchange/exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 368b93a03..32eb1bf0b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -129,6 +129,7 @@ class Exchange: "marketOrderRequiresPrice": False, "exchange_has_overrides": {}, # Dictionary overriding ccxt's "has". # Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False} + "ws.enabled": False, # Set to true for exchanges with tested websocket support } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -229,7 +230,7 @@ class Exchange: ) self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) - self._has_watch_ohlcv = self.exchange_has("watchOHLCV") + self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws.enabled"] self._exchange_ws: Optional[ExchangeWS] = None if exchange_conf.get("enable_ws", True) and self._has_watch_ohlcv: self._exchange_ws = ExchangeWS(self._config, self._ws_async) From 0ec751826b9bfbcda2d4a102362713cc2e6304d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Nov 2023 06:59:42 +0100 Subject: [PATCH 42/89] Opt in binance to websocket support --- freqtrade/exchange/binance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 07c4f9286..fb6686ea9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -29,6 +29,7 @@ class Binance(Exchange): "trades_pagination": "id", "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], + "ws.enabled": True, } _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop", "market": "stop_market"}, @@ -41,6 +42,7 @@ class Binance(Exchange): PriceType.LAST: "CONTRACT_PRICE", PriceType.MARK: "MARK_PRICE", }, + "ws.enabled": False, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ From c0c775114e66519070f7ee55a168ac00a706bb28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Nov 2023 16:20:53 +0100 Subject: [PATCH 43/89] Slightly improved loggign --- 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 32eb1bf0b..6842c119a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2263,12 +2263,12 @@ class Exchange: candle_date = int(timeframe_to_prev_date(timeframe).timestamp()) candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) - logger.info(f"{pair}, {candle_date < x}, {candle_date}, {x}") if candles and candles[-1][0] > min_date and candle_date < x: # Usable result, update happened after prior candle end date logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) + logger.info(f"Failed to reuse watch {pair}, {candle_date < x}, {candle_date}, {x}") # Check if 1 call can get us updated candles without hole in the data. if min_date < last_refresh: From c61d9e0dece7c0b7308b03a1b86ab1f932f5c2ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Nov 2023 06:34:01 +0100 Subject: [PATCH 44/89] Reduce verbosity --- freqtrade/exchange/exchange_ws.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index c108d37d8..98777271a 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -118,12 +118,12 @@ class ExchangeWS: drop_hint = False if refresh_date > candle_date: # Refreshed after candle was complete. - logger.info(f"{candles[-1][0] // 1000} >= {candle_date}") + # logger.info(f"{candles[-1][0] // 1000} >= {candle_date}") drop_hint = (candles[-1][0] // 1000) >= candle_date logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " f"{dt_from_ts(candles[-1][0] // 1000)}, " - f"lref={dt_from_ts(self.klines_last_refresh[(pair, timeframe, candle_type)])}" + f"lref={dt_from_ts(self.klines_last_refresh[(pair, timeframe, candle_type)])}, " f"candle_date={dt_from_ts(candle_date)}, {drop_hint=}" ) return pair, timeframe, candle_type, candles, drop_hint From b5dc54072e3799d29711716b4712acce1a8d09af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:08:13 +0100 Subject: [PATCH 45/89] Ensure exchange objects are not undefined --- freqtrade/exchange/exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6842c119a..8ab5e2684 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -153,6 +153,8 @@ class Exchange: """ self._api: ccxt.Exchange self._api_async: ccxt_pro.Exchange = None + self._ws_async: ccxt_pro.Exchange = None + self._exchange_ws: Optional[ExchangeWS] = None self._markets: Dict = {} self._trading_fees: Dict[str, Any] = {} self._leverage_tiers: Dict[str, List[Dict]] = {} @@ -231,7 +233,6 @@ class Exchange: self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws.enabled"] - self._exchange_ws: Optional[ExchangeWS] = None if exchange_conf.get("enable_ws", True) and self._has_watch_ohlcv: self._exchange_ws = ExchangeWS(self._config, self._ws_async) From f324af938a93c3b7fc2e639dfb755a9e3c6bfa98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Dec 2023 08:12:55 +0100 Subject: [PATCH 46/89] Improve WS logic to assume a candle is complete if time rolled over --- freqtrade/exchange/exchange.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8ab5e2684..1611f1f31 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2261,15 +2261,25 @@ class Exchange: last_refresh = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) if self._exchange_ws: - candle_date = int(timeframe_to_prev_date(timeframe).timestamp()) + candle_date = int(timeframe_to_prev_date(timeframe).timestamp() * 1000) + prev_candle_date = int(date_minus_candles(timeframe, 1).timestamp() * 1000) candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) - x = self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) - if candles and candles[-1][0] > min_date and candle_date < x: - # Usable result, update happened after prior candle end date - logger.info(f"reuse watch result for {pair}, {timeframe}, {x}") + half_candle = int(candle_date - (candle_date - prev_candle_date) * 0.5) + last_refresh_time = int(self._exchange_ws.klines_last_refresh.get( + (pair, timeframe, candle_type), 0) * 1000) + + if ( + candles and candles[-1][0] >= prev_candle_date + and last_refresh_time >= half_candle + ): + # Usable result, candle contains the previous candle. + # Also, we check if the last refresh time is no more than half the candle ago. + logger.info(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}") return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) - logger.info(f"Failed to reuse watch {pair}, {candle_date < x}, {candle_date}, {x}") + logger.info( + f"Failed to reuse watch {pair}, {candle_date < last_refresh_time}, " + f"{candle_date}, {last_refresh_time}") # Check if 1 call can get us updated candles without hole in the data. if min_date < last_refresh: From 9f2708247ae7d8e6efe9f191a671fcde0bb323d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Dec 2023 08:23:15 +0100 Subject: [PATCH 47/89] Enable ws for bybit --- freqtrade/exchange/bybit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index c8b05d1de..72c224246 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -33,6 +33,7 @@ class Bybit(Exchange): "ohlcv_candle_limit": 1000, "ohlcv_has_history": True, "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], + "ws.enabled": True, } _ft_has_futures: Dict = { "ohlcv_has_history": True, From 4e75e594764f202d1ee1ca66bf7f78368d6935a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Dec 2023 10:14:41 +0100 Subject: [PATCH 48/89] Skip futures tests on exchnages not supporting futures ... --- tests/exchange_online/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 80499c8d9..6d2f2a512 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -425,11 +425,13 @@ def exchange_ws(request, exchange_conf, exchange_mode, class_mocker): if exchange_mode == "spot": exchange, name = get_exchange(request.param, exchange_conf) pair = EXCHANGES[request.param]["pair"] - else: + elif EXCHANGES[request.param].get("futures"): exchange, name = get_futures_exchange( request.param, exchange_conf, class_mocker=class_mocker ) pair = EXCHANGES[request.param]["futures_pair"] + else: + pytest.skip("Exchange does not support futures.") if not exchange._has_watch_ohlcv: pytest.skip("Exchange does not support watch_ohlcv.") From d42e012ec3a15cf25b36df429218f65f07adf992 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Dec 2023 10:59:53 +0100 Subject: [PATCH 49/89] ws - Improve cleanup behavior --- freqtrade/exchange/exchange_ws.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 98777271a..70a373781 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -29,6 +29,7 @@ class ExchangeWS: self.klines_last_request: Dict[PairWithTimeframe, float] = {} self._thread = Thread(name="ccxt_ws", target=self._start_forever) self._thread.start() + self.__cleanup_called = False def _start_forever(self) -> None: self._loop = asyncio.new_event_loop() @@ -41,12 +42,22 @@ class ExchangeWS: def cleanup(self) -> None: logger.debug("Cleanup called - stopping") self._klines_watching.clear() + for task in self._background_tasks: + task.cancel() if hasattr(self, '_loop'): + asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) + while not self.__cleanup_called: + time.sleep(0.1) + self._loop.call_soon_threadsafe(self._loop.stop) self._thread.join() logger.debug("Stopped") + async def _cleanup_async(self) -> None: + await self.ccxt_object.close() + self.__cleanup_called = True + def cleanup_expired(self) -> None: """ Remove pairs from watchlist if they've not been requested within From 2ade5191e6529eba9ff1b78b78e03849da846089 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Feb 2024 16:07:41 +0100 Subject: [PATCH 50/89] Ensure shutdown of async exchange, fix test --- freqtrade/exchange/exchange_ws.py | 8 ++++++-- tests/exchange/test_exchange_ws.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 70a373781..be99cb30f 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -55,8 +55,12 @@ class ExchangeWS: logger.debug("Stopped") async def _cleanup_async(self) -> None: - await self.ccxt_object.close() - self.__cleanup_called = True + try: + await self.ccxt_object.close() + except Exception: + logger.exception("Exception in _cleanup_async") + finally: + self.__cleanup_called = True def cleanup_expired(self) -> None: """ diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index c6684b250..5e93de1f4 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -46,6 +46,7 @@ async def test_exchangews_ohlcv(mocker): config = MagicMock() ccxt_object = MagicMock() ccxt_object.watch_ohlcv = AsyncMock() + ccxt_object.close = AsyncMock() mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) exchange_ws = ExchangeWS(config, ccxt_object) From d3962a7c07375d4622e734c7786ee058d3e75f32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 6 Apr 2024 16:47:43 +0200 Subject: [PATCH 51/89] Remove websocket init for non-trade modes --- freqtrade/exchange/exchange.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1611f1f31..a0713586d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -34,7 +34,15 @@ from freqtrade.constants import ( PairWithTimeframe, ) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode +from freqtrade.enums import ( + OPTIMIZE_MODES, + TRADE_MODES, + CandleType, + MarginMode, + PriceType, + RunMode, + TradingMode, +) from freqtrade.exceptions import ( ConfigurationError, DDosProtection, @@ -231,9 +239,13 @@ class Exchange: exchange_conf.get("ccxt_async_config", {}), ccxt_async_config ) self._api_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) - self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._has_watch_ohlcv = self.exchange_has("watchOHLCV") and self._ft_has["ws.enabled"] - if exchange_conf.get("enable_ws", True) and self._has_watch_ohlcv: + if ( + self._config["runmode"] in TRADE_MODES + and exchange_conf.get("enable_ws", True) + and self._has_watch_ohlcv + ): + self._ws_async = self._init_ccxt(exchange_conf, False, ccxt_async_config) self._exchange_ws = ExchangeWS(self._config, self._ws_async) logger.info(f'Using Exchange "{self.name}"') @@ -2265,11 +2277,14 @@ class Exchange: prev_candle_date = int(date_minus_candles(timeframe, 1).timestamp() * 1000) candles = self._exchange_ws.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) half_candle = int(candle_date - (candle_date - prev_candle_date) * 0.5) - last_refresh_time = int(self._exchange_ws.klines_last_refresh.get( - (pair, timeframe, candle_type), 0) * 1000) + last_refresh_time = int( + self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) + * 1000 + ) if ( - candles and candles[-1][0] >= prev_candle_date + candles + and candles[-1][0] >= prev_candle_date and last_refresh_time >= half_candle ): # Usable result, candle contains the previous candle. @@ -2279,7 +2294,8 @@ class Exchange: return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) logger.info( f"Failed to reuse watch {pair}, {candle_date < last_refresh_time}, " - f"{candle_date}, {last_refresh_time}") + f"{candle_date}, {last_refresh_time}" + ) # Check if 1 call can get us updated candles without hole in the data. if min_date < last_refresh: From 80c7d4eb5fcfdfff264123fdc1f5e829b4dfc356 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Apr 2024 18:27:04 +0200 Subject: [PATCH 52/89] Improve debug logging --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a0713586d..44d3f6256 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2293,8 +2293,8 @@ class Exchange: return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) logger.info( - f"Failed to reuse watch {pair}, {candle_date < last_refresh_time}, " - f"{candle_date}, {last_refresh_time}" + f"Failed to reuse watch {pair}, {timeframe}, {candle_date < last_refresh_time}," + f" {candle_date}, {last_refresh_time}" ) # Check if 1 call can get us updated candles without hole in the data. From c482b7e40fe3dc7777b69048ef39cb0cbe8fe7c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2024 18:19:32 +0200 Subject: [PATCH 53/89] Add log for "removal" tracking --- freqtrade/exchange/exchange_ws.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index be99cb30f..3ad63c3cd 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -67,6 +67,7 @@ class ExchangeWS: Remove pairs from watchlist if they've not been requested within the last timeframe (+ offset) """ + changed = False for p in list(self._klines_watching): _, timeframe, _ = p timeframe_s = timeframe_to_seconds(timeframe) @@ -74,6 +75,9 @@ class ExchangeWS: if last_refresh > 0 and time.time() - last_refresh > timeframe_s + 20: logger.info(f"Removing {p} from watchlist") self._klines_watching.discard(p) + changed = True + if changed: + logger.info(f"Removal done: new watch list: {self._klines_watching}") async def _schedule_while_true(self) -> None: From 85725b54728ff2bc9be451f0963149402833b6bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2024 18:23:05 +0200 Subject: [PATCH 54/89] Improved exception message --- freqtrade/exchange/exchange_ws.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 3ad63c3cd..4de4d5258 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -107,7 +107,8 @@ class ExchangeWS: f"watch done {pair}, {timeframe}, data {len(data)} " f"in {time.time() - start:.2f}s") except ccxt.BaseError: - logger.exception("Exception in continuously_async_watch_ohlcv") + logger.exception( + f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}") finally: self._klines_watching.discard((pair, timeframe, candle_type)) From fc66a12c14c39ae0e0a9c3de4c493c529fbbab39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2024 18:27:47 +0200 Subject: [PATCH 55/89] Improve "stopped" messages --- freqtrade/exchange/exchange_ws.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 4de4d5258..74347d93a 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -2,6 +2,7 @@ import asyncio import logging import time +from functools import partial from threading import Thread from typing import Dict, Set @@ -88,9 +89,11 @@ class ExchangeWS: task = asyncio.create_task( self._continuously_async_watch_ohlcv(pair, timeframe, candle_type)) self._background_tasks.add(task) - task.add_done_callback(self._continuous_stopped) + task.add_done_callback(partial( + self._continuous_stopped, pair=pair, timeframe=timeframe) + ) - def _continuous_stopped(self, task: asyncio.Task): + def _continuous_stopped(self, task: asyncio.Task, pair: str, timeframe: str): self._background_tasks.discard(task) result = task.result() logger.info(f"Task finished {result}") From ce33b031f2e48f531b9f94916253a28b0e73ba80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2024 18:29:53 +0200 Subject: [PATCH 56/89] Show pair for task finished --- freqtrade/exchange/exchange_ws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 74347d93a..4a8cd5e3e 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -96,7 +96,7 @@ class ExchangeWS: def _continuous_stopped(self, task: asyncio.Task, pair: str, timeframe: str): self._background_tasks.discard(task) result = task.result() - logger.info(f"Task finished {result}") + logger.info(f"{pair}, {timeframe} Task finished {result}") # self._pairs_scheduled.discard(pair, timeframe, candle_type) async def _continuously_async_watch_ohlcv( From 45c17f24482bc835ffaad829ababb5e7f51725f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Apr 2024 19:02:01 +0200 Subject: [PATCH 57/89] Reduce excessive log again --- freqtrade/exchange/exchange_ws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 4a8cd5e3e..f683345d5 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -78,7 +78,7 @@ class ExchangeWS: self._klines_watching.discard(p) changed = True if changed: - logger.info(f"Removal done: new watch list: {self._klines_watching}") + logger.info(f"Removal done: new watch list ({len(self._klines_watching)})") async def _schedule_while_true(self) -> None: From 554d4134ffd72211ef21150ba623b09c983a4457 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Apr 2024 08:05:55 +0200 Subject: [PATCH 58/89] Add humanized date to debug log --- freqtrade/exchange/exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 44d3f6256..e8bc2e115 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -91,7 +91,7 @@ from freqtrade.misc import ( ) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.util import dt_from_ts, dt_now -from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts +from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_date, format_ms_time from freqtrade.util.periodic_cache import PeriodicCache @@ -2294,7 +2294,8 @@ class Exchange: return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) logger.info( f"Failed to reuse watch {pair}, {timeframe}, {candle_date < last_refresh_time}," - f" {candle_date}, {last_refresh_time}" + f" {candle_date}, {last_refresh_time}, " + f"{format_ms_time(candle_date)}, {format_ms_time(last_refresh_time)} " ) # Check if 1 call can get us updated candles without hole in the data. From 93cdf1bb543f73350aa5ac6f868b4d3dad780886 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Apr 2024 08:21:21 +0200 Subject: [PATCH 59/89] Simplify logging --- freqtrade/exchange/exchange_ws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index f683345d5..c8f6bb8fc 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -146,7 +146,7 @@ class ExchangeWS: logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " f"{dt_from_ts(candles[-1][0] // 1000)}, " - f"lref={dt_from_ts(self.klines_last_refresh[(pair, timeframe, candle_type)])}, " + f"lref={dt_from_ts(refresh_date)}, " f"candle_date={dt_from_ts(candle_date)}, {drop_hint=}" ) return pair, timeframe, candle_type, candles, drop_hint From 7bc4fdca27c62067de024172e9cf16a196082e16 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Apr 2024 08:21:49 +0200 Subject: [PATCH 60/89] remove pairs from _pairs_schedules when their coroutine stops --- freqtrade/exchange/exchange_ws.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index c8f6bb8fc..ec9d5a29b 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -81,23 +81,30 @@ class ExchangeWS: logger.info(f"Removal done: new watch list ({len(self._klines_watching)})") async def _schedule_while_true(self) -> None: - + # For the ones we should be watching for p in self._klines_watching: + # Check if they're already scheduled if p not in self._klines_scheduled: self._klines_scheduled.add(p) pair, timeframe, candle_type = p task = asyncio.create_task( self._continuously_async_watch_ohlcv(pair, timeframe, candle_type)) self._background_tasks.add(task) - task.add_done_callback(partial( - self._continuous_stopped, pair=pair, timeframe=timeframe) + task.add_done_callback( + partial( + self._continuous_stopped, + pair=pair, + timeframe=timeframe, + candle_type=candle_type + ) ) - def _continuous_stopped(self, task: asyncio.Task, pair: str, timeframe: str): + def _continuous_stopped( + self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType): self._background_tasks.discard(task) result = task.result() logger.info(f"{pair}, {timeframe} Task finished {result}") - # self._pairs_scheduled.discard(pair, timeframe, candle_type) + self._pairs_scheduled.discard(pair, timeframe, candle_type) async def _continuously_async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType) -> None: From b5239f06aefe4f92e9a3801cdd71bccb1193fad3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Apr 2024 08:26:43 +0200 Subject: [PATCH 61/89] Improve log formatting --- freqtrade/exchange/exchange_ws.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index ec9d5a29b..f21d8c7bb 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -12,7 +12,7 @@ from freqtrade.constants import Config, PairWithTimeframe from freqtrade.enums.candletype import CandleType from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.exchange.types import OHLCVResponse -from freqtrade.util.datetime_helpers import dt_from_ts +from freqtrade.util import format_ms_time logger = logging.getLogger(__name__) @@ -152,8 +152,8 @@ class ExchangeWS: drop_hint = (candles[-1][0] // 1000) >= candle_date logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " - f"{dt_from_ts(candles[-1][0] // 1000)}, " - f"lref={dt_from_ts(refresh_date)}, " - f"candle_date={dt_from_ts(candle_date)}, {drop_hint=}" + f"{format_ms_time(candles[-1][0] // 1000)}, " + f"lref={format_ms_time(refresh_date)}, " + f"candle_date={format_ms_time(candle_date)}, {drop_hint=}" ) return pair, timeframe, candle_type, candles, drop_hint From 68c36ce07dcbf63b45667739762495bcf45688dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Apr 2024 08:55:37 +0200 Subject: [PATCH 62/89] Fix typo --- freqtrade/exchange/exchange_ws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index f21d8c7bb..2ec65f33a 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -104,7 +104,7 @@ class ExchangeWS: self._background_tasks.discard(task) result = task.result() logger.info(f"{pair}, {timeframe} Task finished {result}") - self._pairs_scheduled.discard(pair, timeframe, candle_type) + self._klines_scheduled.discard((pair, timeframe, candle_type)) async def _continuously_async_watch_ohlcv( self, pair: str, timeframe: str, candle_type: CandleType) -> None: From ed8b9018c550616d3766995dfeea6e9a1634ab54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Apr 2024 09:38:14 +0200 Subject: [PATCH 63/89] Properly handle shutdown (canceled coroutines) This will imrove shutdown behavior --- freqtrade/exchange/exchange_ws.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 2ec65f33a..d223209bf 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -102,8 +102,12 @@ class ExchangeWS: def _continuous_stopped( self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType): self._background_tasks.discard(task) - result = task.result() - logger.info(f"{pair}, {timeframe} Task finished {result}") + if task.cancelled(): + result = "cancelled" + else: + result = task.result() + + logger.info(f"{pair}, {timeframe} Task finished: {result}") self._klines_scheduled.discard((pair, timeframe, candle_type)) async def _continuously_async_watch_ohlcv( From d5d818be8bf91494f471b17b8c75cab4866aaf22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Apr 2024 10:16:31 +0200 Subject: [PATCH 64/89] Remove unused import --- 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 e8bc2e115..3bbde8749 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -91,7 +91,7 @@ from freqtrade.misc import ( ) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.util import dt_from_ts, dt_now -from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_date, format_ms_time +from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time from freqtrade.util.periodic_cache import PeriodicCache From 765fa06daace1db60a73a09d3fc5a65c011f8e74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2024 18:18:39 +0000 Subject: [PATCH 65/89] Deepcopy ccxt ws result --- freqtrade/exchange/exchange_ws.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index d223209bf..245433551 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -2,6 +2,7 @@ import asyncio import logging import time +from copy import deepcopy from functools import partial from threading import Thread from typing import Dict, Set @@ -147,7 +148,8 @@ class ExchangeWS: Returns cached klines from ccxt's "watch" cache. :param candle_date: timestamp of the end-time of the candle. """ - candles = self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe) + # Deepcopy the response - as it might be modified in the background as new messages arrive + candles = deepcopy(self.ccxt_object.ohlcvs.get(pair, {}).get(timeframe)) refresh_date = self.klines_last_refresh[(pair, timeframe, candle_type)] drop_hint = False if refresh_date > candle_date: From 7ec8b28be30cb84f814b53d1ae71fca0e96d216f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Apr 2024 20:26:18 +0200 Subject: [PATCH 66/89] Re-adjust ts handling to not use time.time() --- freqtrade/exchange/exchange_ws.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 245433551..bce797c68 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -13,7 +13,7 @@ from freqtrade.constants import Config, PairWithTimeframe from freqtrade.enums.candletype import CandleType from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.exchange.types import OHLCVResponse -from freqtrade.util import format_ms_time +from freqtrade.util import dt_ts, format_ms_time logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ class ExchangeWS: _, timeframe, _ = p timeframe_s = timeframe_to_seconds(timeframe) last_refresh = self.klines_last_request.get(p, 0) - if last_refresh > 0 and time.time() - last_refresh > timeframe_s + 20: + if last_refresh > 0 and dt_ts() - last_refresh > timeframe_s + 20: logger.info(f"Removing {p} from watchlist") self._klines_watching.discard(p) changed = True @@ -115,12 +115,12 @@ class ExchangeWS: self, pair: str, timeframe: str, candle_type: CandleType) -> None: try: while (pair, timeframe, candle_type) in self._klines_watching: - start = time.time() + start = dt_ts() data = await self.ccxt_object.watch_ohlcv(pair, timeframe) - self.klines_last_refresh[(pair, timeframe, candle_type)] = time.time() + self.klines_last_refresh[(pair, timeframe, candle_type)] = dt_ts() logger.debug( f"watch done {pair}, {timeframe}, data {len(data)} " - f"in {time.time() - start:.2f}s") + f"in {dt_ts() - start:.2f}s") except ccxt.BaseError: logger.exception( f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}") @@ -132,7 +132,7 @@ class ExchangeWS: Schedule a pair/timeframe combination to be watched """ self._klines_watching.add((pair, timeframe, candle_type)) - self.klines_last_request[(pair, timeframe, candle_type)] = time.time() + self.klines_last_request[(pair, timeframe, candle_type)] = dt_ts() # asyncio.run_coroutine_threadsafe(self.schedule_schedule(), loop=self._loop) asyncio.run_coroutine_threadsafe(self._schedule_while_true(), loop=self._loop) self.cleanup_expired() @@ -154,11 +154,11 @@ class ExchangeWS: drop_hint = False if refresh_date > candle_date: # Refreshed after candle was complete. - # logger.info(f"{candles[-1][0] // 1000} >= {candle_date}") - drop_hint = (candles[-1][0] // 1000) >= candle_date + # logger.info(f"{candles[-1][0]} >= {candle_date}") + drop_hint = candles[-1][0] >= candle_date logger.info( f"watch result for {pair}, {timeframe} with length {len(candles)}, " - f"{format_ms_time(candles[-1][0] // 1000)}, " + f"{format_ms_time(candles[-1][0])}, " f"lref={format_ms_time(refresh_date)}, " f"candle_date={format_ms_time(candle_date)}, {drop_hint=}" ) From 627154cb669c497bc9766b9d4422696be4b96ac8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Apr 2024 07:32:48 +0200 Subject: [PATCH 67/89] improve ws "removing" condition --- freqtrade/exchange/exchange_ws.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index bce797c68..951d8a8d5 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -74,7 +74,10 @@ class ExchangeWS: _, timeframe, _ = p timeframe_s = timeframe_to_seconds(timeframe) last_refresh = self.klines_last_request.get(p, 0) - if last_refresh > 0 and dt_ts() - last_refresh > timeframe_s + 20: + if ( + last_refresh > 0 + and (dt_ts() - last_refresh) > ((timeframe_s + 20) * 1000) + ): logger.info(f"Removing {p} from watchlist") self._klines_watching.discard(p) changed = True From 212ac2073efa2b49de694cfee65eb19e7edc6c76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 May 2024 08:34:44 +0200 Subject: [PATCH 68/89] Don't multiply klines_last_refresh with 1000 - it's already in ms --- freqtrade/exchange/exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3bbde8749..98d172931 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2279,7 +2279,6 @@ class Exchange: half_candle = int(candle_date - (candle_date - prev_candle_date) * 0.5) last_refresh_time = int( self._exchange_ws.klines_last_refresh.get((pair, timeframe, candle_type), 0) - * 1000 ) if ( From cabd36253eb7447705b3a86991fb00db0a252a67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 May 2024 08:30:51 +0200 Subject: [PATCH 69/89] Reduce level of "reuse watch result" . . . --- 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 98d172931..5dac32635 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2288,7 +2288,7 @@ class Exchange: ): # Usable result, candle contains the previous candle. # Also, we check if the last refresh time is no more than half the candle ago. - logger.info(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}") + logger.debug(f"reuse watch result for {pair}, {timeframe}, {last_refresh_time}") return self._exchange_ws.get_ohlcv(pair, timeframe, candle_type, candle_date) logger.info( From f33c4db5722d3566d7c41227dcd9caf9818bd9a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 May 2024 06:35:49 +0200 Subject: [PATCH 70/89] Apply ruff formatting to ws branch --- freqtrade/exchange/common.py | 2 +- freqtrade/exchange/exchange_ws.py | 37 ++++++++++---------- tests/exchange/test_exchange_ws.py | 7 ++-- tests/exchange_online/test_ccxt_ws_compat.py | 13 ++++--- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 62221d0cc..251325a0c 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -93,7 +93,7 @@ EXCHANGE_HAS_OPTIONAL = [ # 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... # ccxt.pro - 'watchOHLCV' + "watchOHLCV", ] diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 951d8a8d5..7cc58978f 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -1,4 +1,3 @@ - import asyncio import logging import time @@ -46,7 +45,7 @@ class ExchangeWS: self._klines_watching.clear() for task in self._background_tasks: task.cancel() - if hasattr(self, '_loop'): + if hasattr(self, "_loop"): asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) while not self.__cleanup_called: time.sleep(0.1) @@ -74,10 +73,7 @@ class ExchangeWS: _, timeframe, _ = p timeframe_s = timeframe_to_seconds(timeframe) last_refresh = self.klines_last_request.get(p, 0) - if ( - last_refresh > 0 - and (dt_ts() - last_refresh) > ((timeframe_s + 20) * 1000) - ): + if last_refresh > 0 and (dt_ts() - last_refresh) > ((timeframe_s + 20) * 1000): logger.info(f"Removing {p} from watchlist") self._klines_watching.discard(p) changed = True @@ -92,19 +88,21 @@ class ExchangeWS: self._klines_scheduled.add(p) pair, timeframe, candle_type = p task = asyncio.create_task( - self._continuously_async_watch_ohlcv(pair, timeframe, candle_type)) + self._continuously_async_watch_ohlcv(pair, timeframe, candle_type) + ) self._background_tasks.add(task) task.add_done_callback( partial( self._continuous_stopped, pair=pair, timeframe=timeframe, - candle_type=candle_type + candle_type=candle_type, ) ) def _continuous_stopped( - self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType): + self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType + ): self._background_tasks.discard(task) if task.cancelled(): result = "cancelled" @@ -115,7 +113,8 @@ class ExchangeWS: self._klines_scheduled.discard((pair, timeframe, candle_type)) async def _continuously_async_watch_ohlcv( - self, pair: str, timeframe: str, candle_type: CandleType) -> None: + self, pair: str, timeframe: str, candle_type: CandleType + ) -> None: try: while (pair, timeframe, candle_type) in self._klines_watching: start = dt_ts() @@ -123,10 +122,10 @@ class ExchangeWS: self.klines_last_refresh[(pair, timeframe, candle_type)] = dt_ts() logger.debug( f"watch done {pair}, {timeframe}, data {len(data)} " - f"in {dt_ts() - start:.2f}s") + f"in {dt_ts() - start:.2f}s" + ) except ccxt.BaseError: - logger.exception( - f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}") + logger.exception(f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}") finally: self._klines_watching.discard((pair, timeframe, candle_type)) @@ -141,11 +140,11 @@ class ExchangeWS: self.cleanup_expired() async def get_ohlcv( - self, - pair: str, - timeframe: str, - candle_type: CandleType, - candle_date: int, + self, + pair: str, + timeframe: str, + candle_type: CandleType, + candle_date: int, ) -> OHLCVResponse: """ Returns cached klines from ccxt's "watch" cache. @@ -164,5 +163,5 @@ class ExchangeWS: f"{format_ms_time(candles[-1][0])}, " f"lref={format_ms_time(refresh_date)}, " f"candle_date={format_ms_time(candle_date)}, {drop_hint=}" - ) + ) return pair, timeframe, candle_type, candles, drop_hint diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index 5e93de1f4..07819fc7a 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -8,7 +8,6 @@ from freqtrade.exchange.exchange_ws import ExchangeWS def test_exchangews_init(mocker): - config = MagicMock() ccxt_object = MagicMock() mocker.patch("freqtrade.exchange.exchange_ws.ExchangeWS._start_forever", MagicMock()) @@ -36,6 +35,7 @@ def patch_eventloop_threading(exchange): exchange._loop = asyncio.new_event_loop() is_init = True exchange._loop.run_forever() + x = threading.Thread(target=thread_fuck, daemon=True) x.start() while not is_init: @@ -52,16 +52,15 @@ async def test_exchangews_ohlcv(mocker): exchange_ws = ExchangeWS(config, ccxt_object) patch_eventloop_threading(exchange_ws) try: - assert exchange_ws._klines_watching == set() assert exchange_ws._klines_scheduled == set() exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) - sleep(.5) + sleep(0.5) assert exchange_ws._klines_watching == {("ETH/BTC", "1m", CandleType.SPOT)} assert exchange_ws._klines_scheduled == {("ETH/BTC", "1m", CandleType.SPOT)} - sleep(.1) + sleep(0.1) assert ccxt_object.watch_ohlcv.call_count == 1 except Exception as e: print(e) diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index c99c3db1d..32398573a 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -20,15 +20,14 @@ from tests.exchange_online.conftest import EXCHANGE_WS_FIXTURE_TYPE @pytest.mark.longrun class TestCCXTExchangeWs: - def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_WS_FIXTURE_TYPE, caplog, mocker): exch, exchangename, pair = exchange_ws assert exch._ws_async is not None - timeframe = '1m' + timeframe = "1m" pair_tf = (pair, timeframe, CandleType.SPOT) - m_hist = mocker.spy(exch, '_async_get_historic_ohlcv') - m_cand = mocker.spy(exch, '_async_get_candle_history') + m_hist = mocker.spy(exch, "_async_get_historic_ohlcv") + m_cand = mocker.spy(exch, "_async_get_candle_history") res = exch.refresh_latest_ohlcv([pair_tf]) assert m_cand.call_count == 1 @@ -45,7 +44,7 @@ class TestCCXTExchangeWs: df1 = res[pair_tf] caplog.set_level(logging.DEBUG) set_loggers(1) - assert df1.iloc[-1]['date'] == curr_candle + assert df1.iloc[-1]["date"] == curr_candle # Wait until the next candle (might be up to 1 minute). while True: @@ -53,9 +52,9 @@ class TestCCXTExchangeWs: res = exch.refresh_latest_ohlcv([pair_tf]) df2 = res[pair_tf] assert df2 is not None - if df2.iloc[-1]['date'] == next_candle: + if df2.iloc[-1]["date"] == next_candle: break - assert df2.iloc[-1]['date'] == curr_candle + assert df2.iloc[-1]["date"] == curr_candle sleep(1) assert m_hist.call_count == 0 From 7e736a34dd46ad19587c11c516e8463dea3211d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 May 2024 19:10:32 +0200 Subject: [PATCH 71/89] Reduce ccxt.pro verbosity --- freqtrade/exchange/exchange.py | 3 +-- freqtrade/exchange/exchange_ws.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5dac32635..c852f53b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2271,7 +2271,6 @@ class Exchange: candle_limit = self.ohlcv_candle_limit(timeframe, candle_type) min_date = int(date_minus_candles(timeframe, candle_limit - 5).timestamp()) - last_refresh = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) if self._exchange_ws: candle_date = int(timeframe_to_prev_date(timeframe).timestamp() * 1000) prev_candle_date = int(date_minus_candles(timeframe, 1).timestamp() * 1000) @@ -2298,7 +2297,7 @@ class Exchange: ) # Check if 1 call can get us updated candles without hole in the data. - if min_date < last_refresh: + if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0): # Cache can be used - do one-off call. not_all_data = False else: diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 7cc58978f..2f46c78f2 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -158,7 +158,7 @@ class ExchangeWS: # Refreshed after candle was complete. # logger.info(f"{candles[-1][0]} >= {candle_date}") drop_hint = candles[-1][0] >= candle_date - logger.info( + logger.debug( f"watch result for {pair}, {timeframe} with length {len(candles)}, " f"{format_ms_time(candles[-1][0])}, " f"lref={format_ms_time(refresh_date)}, " From 33e61b1308abee7144777703611328b524cff308 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2024 06:48:38 +0200 Subject: [PATCH 72/89] Extract connection reset from exchange_ws --- freqtrade/exchange/exchange_ws.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 2f46c78f2..13e689c0b 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -46,15 +46,24 @@ class ExchangeWS: for task in self._background_tasks: task.cancel() if hasattr(self, "_loop"): - asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) - while not self.__cleanup_called: - time.sleep(0.1) + self.reset_connections() self._loop.call_soon_threadsafe(self._loop.stop) self._thread.join() logger.debug("Stopped") + def reset_connections(self) -> None: + """ + Reset all connections - avoids "connection-reset" errors that happen after ~9 days + """ + if hasattr(self, "_loop"): + logger.info("Resetting WS connections.") + asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) + while not self.__cleanup_called: + time.sleep(0.1) + self.__cleanup_called = False + async def _cleanup_async(self) -> None: try: await self.ccxt_object.close() From 12852438a56725770021dc4faa04ded727556c4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2024 06:52:11 +0200 Subject: [PATCH 73/89] Call connection at intervals --- freqtrade/exchange/exchange.py | 7 +++++++ freqtrade/freqtradebot.py | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c852f53b2..384ac43a1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -563,6 +563,13 @@ class Exchange: amount, self.get_precision_amount(pair), self.precisionMode, contract_size ) + def ws_connection_reset(self): + """ + called at regular intervals to reset the websocket connection + """ + if self._exchange_ws: + self._exchange_ws.reset_connections() + def _load_async_markets(self, reload: bool = False) -> None: try: if self._api_async: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8d53097a5..9945296fa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -168,6 +168,8 @@ class FreqtradeBot(LoggingMixin): t = str(time(time_slot, minutes, 2)) self._schedule.every().day.at(t).do(update) + self._schedule.every().day.at("00:02").do(self.exchange.ws_connection_reset) + self.strategy.ft_bot_start() # Initialize protections AFTER bot start - otherwise parameters are not loaded. self.protections = ProtectionManager(self.config, self.strategy.protections) @@ -289,8 +291,7 @@ class FreqtradeBot(LoggingMixin): # Then looking for entry opportunities if self.get_free_open_trades(): self.enter_positions() - if self.trading_mode == TradingMode.FUTURES: - self._schedule.run_pending() + self._schedule.run_pending() Trade.commit() self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.last_process = datetime.now(timezone.utc) From 8b90643f3a9ca37e4cd1bba842de24debf75ecfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2024 07:03:40 +0200 Subject: [PATCH 74/89] Don't show "exchange closed by user" exceptions --- freqtrade/exchange/exchange_ws.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 13e689c0b..ec85cbe29 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -133,6 +133,8 @@ class ExchangeWS: f"watch done {pair}, {timeframe}, data {len(data)} " f"in {dt_ts() - start:.2f}s" ) + except ccxt.ExchangeClosedByUser: + logger.debug("Exchange connection closed by user") except ccxt.BaseError: logger.exception(f"Exception in continuously_async_watch_ohlcv for {pair}, {timeframe}") finally: From 122896f9ab4979f5c28f012c674bfb7ef268a287 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2024 07:12:02 +0200 Subject: [PATCH 75/89] Improved "task done" message --- freqtrade/exchange/exchange_ws.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index ec85cbe29..9083bcba3 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -113,12 +113,14 @@ class ExchangeWS: self, task: asyncio.Task, pair: str, timeframe: str, candle_type: CandleType ): self._background_tasks.discard(task) + result = "done" if task.cancelled(): result = "cancelled" else: - result = task.result() + if (result1 := task.result()) is not None: + result = str(result1) - logger.info(f"{pair}, {timeframe} Task finished: {result}") + logger.info(f"{pair}, {timeframe}, {candle_type} - Task finished - {result}") self._klines_scheduled.discard((pair, timeframe, candle_type)) async def _continuously_async_watch_ohlcv( From 87eda5fc2a3cf25d06c886944575413037d4f4fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 May 2024 20:18:36 +0200 Subject: [PATCH 76/89] Properly mock ccxt_async init --- tests/exchange_online/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 6d2f2a512..6e59c8f82 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -421,6 +421,7 @@ def exchange_mode(request): @pytest.fixture(params=EXCHANGES, scope="class") def exchange_ws(request, exchange_conf, exchange_mode, class_mocker): + class_mocker.patch("freqtrade.exchange.bybit.Bybit.additional_exchange_init") exchange_conf["exchange"]["enable_ws"] = True if exchange_mode == "spot": exchange, name = get_exchange(request.param, exchange_conf) From 0993d12955730f306ec710cfaddeeb5cc43b5119 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Jun 2024 20:57:15 +0200 Subject: [PATCH 77/89] Add timeout to some tests --- requirements-dev.txt | 1 + tests/exchange_online/test_ccxt_ws_compat.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index c9d561b40..03ca030c0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,6 +15,7 @@ pytest-asyncio==0.23.7 pytest-cov==5.0.0 pytest-mock==3.14.0 pytest-random-order==1.1.1 +pytest-timeout==2.3.1 pytest-xdist==3.6.1 isort==5.13.2 # For datetime mocking diff --git a/tests/exchange_online/test_ccxt_ws_compat.py b/tests/exchange_online/test_ccxt_ws_compat.py index 32398573a..49c46f516 100644 --- a/tests/exchange_online/test_ccxt_ws_compat.py +++ b/tests/exchange_online/test_ccxt_ws_compat.py @@ -19,6 +19,7 @@ from tests.exchange_online.conftest import EXCHANGE_WS_FIXTURE_TYPE @pytest.mark.longrun +@pytest.mark.timeout(3 * 60) class TestCCXTExchangeWs: def test_ccxt_ohlcv(self, exchange_ws: EXCHANGE_WS_FIXTURE_TYPE, caplog, mocker): exch, exchangename, pair = exchange_ws From d4ccc7909d4aad5841bcbc7d80d7eddd84c86cab Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Jun 2024 21:00:22 +0200 Subject: [PATCH 78/89] Control pytest log formatting --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e96768c5b..0c9222530 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,9 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*" known_first_party = ["freqtrade_client"] [tool.pytest.ini_options] +log_format = "%(asctime)s %(levelname)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" + asyncio_mode = "auto" addopts = "--dist loadscope" From 9da0437e3d8df29c4a49c8a338d20cf11399f100 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Jun 2024 19:52:19 +0200 Subject: [PATCH 79/89] Improve exchange_ws shutdown --- freqtrade/exchange/exchange_ws.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index 9083bcba3..e84c04d88 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -45,10 +45,13 @@ class ExchangeWS: self._klines_watching.clear() for task in self._background_tasks: task.cancel() - if hasattr(self, "_loop"): + if hasattr(self, "_loop") and not self._loop.is_closed(): self.reset_connections() self._loop.call_soon_threadsafe(self._loop.stop) + time.sleep(0.1) + if not self._loop.is_closed(): + self._loop.close() self._thread.join() logger.debug("Stopped") @@ -57,7 +60,7 @@ class ExchangeWS: """ Reset all connections - avoids "connection-reset" errors that happen after ~9 days """ - if hasattr(self, "_loop"): + if hasattr(self, "_loop") and not self._loop.is_closed(): logger.info("Resetting WS connections.") asyncio.run_coroutine_threadsafe(self._cleanup_async(), loop=self._loop) while not self.__cleanup_called: From 0eeaee21fbdfe37f44f9ebd99f96480e6e49a917 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 7 Jun 2024 19:03:28 +0200 Subject: [PATCH 80/89] Avoid "restart" failure - reset ccxt cache --- freqtrade/exchange/exchange_ws.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/exchange_ws.py b/freqtrade/exchange/exchange_ws.py index e84c04d88..06ac45e21 100644 --- a/freqtrade/exchange/exchange_ws.py +++ b/freqtrade/exchange/exchange_ws.py @@ -70,6 +70,9 @@ class ExchangeWS: async def _cleanup_async(self) -> None: try: await self.ccxt_object.close() + # Clear the cache. + # Not doing this will cause problems on startup with dynamic pairlists + self.ccxt_object.ohlcvs.clear() except Exception: logger.exception("Exception in _cleanup_async") finally: From 094dc18e861b998fb3d40abd4f17973ecda3cfda Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Jun 2024 20:27:40 +0200 Subject: [PATCH 81/89] set wsProxy for exchange calls --- tests/exchange_online/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 6e59c8f82..a2e6c8a74 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -362,6 +362,7 @@ def set_test_proxy(config: Config, use_proxy: bool) -> Config: config1 = deepcopy(config) config1["exchange"]["ccxt_config"] = { "httpsProxy": proxy, + "wsProxy": proxy, } return config1 From 2bc5756326c6d82f52355db69a0a24c2ae2bc25a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 19 Jun 2024 20:27:47 +0200 Subject: [PATCH 82/89] Update proxy documentation --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index e2501cf48..839449e84 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -722,6 +722,7 @@ To use a proxy for exchange connections - you will have to define the proxies as "exchange": { "ccxt_config": { "httpsProxy": "http://addr:port", + "wsProxy": "http://addr:port", } } } From d953226459fd8a3669ce5b98d89741fcea7042d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jul 2024 13:44:36 +0200 Subject: [PATCH 83/89] use correct sleep method in tests --- tests/exchange/test_exchange_ws.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange_ws.py b/tests/exchange/test_exchange_ws.py index 07819fc7a..ec238000a 100644 --- a/tests/exchange/test_exchange_ws.py +++ b/tests/exchange/test_exchange_ws.py @@ -56,11 +56,11 @@ async def test_exchangews_ohlcv(mocker): assert exchange_ws._klines_scheduled == set() exchange_ws.schedule_ohlcv("ETH/BTC", "1m", CandleType.SPOT) - sleep(0.5) + asyncio.sleep(0.5) assert exchange_ws._klines_watching == {("ETH/BTC", "1m", CandleType.SPOT)} assert exchange_ws._klines_scheduled == {("ETH/BTC", "1m", CandleType.SPOT)} - sleep(0.1) + asyncio.sleep(0.1) assert ccxt_object.watch_ohlcv.call_count == 1 except Exception as e: print(e) From db8d4dc9907fd4601f60632a2ff6bd6d2da59f37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 12:06:17 +0200 Subject: [PATCH 84/89] Add exchange.enable_ws setting to doc --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index 9d8611fce..7a304ec9e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -207,6 +207,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict +| `exchange.enable_ws` | Enable the usage of Websockets for the exchange.
*Defaults to `true`.*
**Datatype:** Boolean | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean From 8a246b831b528a2032a9bc4777d59f2afba9552e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 12:07:09 +0200 Subject: [PATCH 85/89] Update non-working ccxt doc links --- docs/configuration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 54329c505..c7cee4a1c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -204,9 +204,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List -| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict -| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict -| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict +| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict +| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation)
**Datatype:** Dict +| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean From 7ab4eecb1899b176a300dbbd22e671d9210aa645 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 12:10:39 +0200 Subject: [PATCH 86/89] Fix header indentation --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c7cee4a1c..135953d0e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -702,7 +702,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d **NEVER** share your private configuration file or your exchange keys with anyone! -### Using proxy with Freqtrade +## Using a proxy with Freqtrade To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values. This will have the proxy settings applied to everything (telegram, coingecko, ...) **except** for exchange requests. @@ -713,7 +713,7 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` -#### Proxy exchange requests +### Proxy exchange requests To use a proxy for exchange connections - you will have to define the proxies as part of the ccxt configuration. From 57316e18752ac9ee93d6acfe5a176a9f5290f9c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 12:39:19 +0200 Subject: [PATCH 87/89] Don't evaluate comment in waves exchange --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 673f98e05..22264ae54 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2176,7 +2176,7 @@ def test_api_exchanges(botclient): "valid": True, "supported": False, "dex": True, - "comment": "", + "comment": ANY, "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], } From b3bcbfa80340928bb196a21e902e62369c2a9f6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 12:47:44 +0200 Subject: [PATCH 88/89] Add extended Websocket documentation --- docs/configuration.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index e3b15f4f6..b47964b69 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -207,7 +207,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation)
**Datatype:** Dict -| `exchange.enable_ws` | Enable the usage of Websockets for the exchange.
*Defaults to `true`.*
**Datatype:** Boolean +| `exchange.enable_ws` | Enable the usage of Websockets for the exchange.
[More information](#consuming-exchange-websockets).
*Defaults to `true`.*
**Datatype:** Boolean | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean @@ -615,6 +615,30 @@ Freqtrade supports both Demo and Pro coingecko API keys. The Coingecko API key is NOT required for the bot to function correctly. It is only used for the conversion of coin to fiat in the Telegram reports, which usually also work without API key. +## Consuming exchange Websockets + +Freqtrade can consume websockets through ccxt.pro. + +Freqtrade aims ensure data is available at all times. +Should the websocket connection fail (or be disabled), the bot will fall back to REST API calls. + +Should you experience problems you suspect are caused by websockets, you can disable these via the setting `exchange.enable_ws`, which defaults to true. + +```jsonc +"exchange": { + // ... + "enable_ws": false, + // ... +} +``` + +Should you be required to use a proxy, please refer to the [proxy section](#using-proxy-with-freqtrade) for more information. + +!!! Info "Rollout" + We're implementing this out slowly, ensuring stability of your bots. + Currently, usage is limited to ohlcv data streams. + It's also limited to a few exchanges, with new exchanges being added on an ongoing basis. + ## Using Dry-run mode We recommend starting the bot in the Dry-run mode to see how your bot will From 0e51baeb109ab77f761d521646fdb86aa9a7b5c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Jul 2024 12:49:51 +0200 Subject: [PATCH 89/89] Better structure for config docs --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 58d744c24..ec8134281 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -410,6 +410,8 @@ Or another example if your position adjustment assumes it can do 1 additional bu --8<-- "includes/pricing.md" +## Further Configuration details + ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration