From 20fc52177128ee24b3812b5cf9235dd941a23611 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Sat, 15 Oct 2022 23:30:12 +0200 Subject: [PATCH 01/41] Fix constant PCA --- freqtrade/freqai/data_kitchen.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 52261d341..7f3e7cb01 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -206,11 +206,15 @@ class FreqaiDataKitchen: drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. + ft_params = self.freqai_config["feature_parameters"] if (training_filter): - const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) - if const_cols: - filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) - logger.warning(f"Removed features {const_cols} with constant values.") + if not ft_params.get( + "principal_component_analysis", False + ): + const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) + if const_cols: + filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) + logger.warning(f"Removed features {const_cols} with constant values.") # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here @@ -241,7 +245,10 @@ class FreqaiDataKitchen: self.data["filter_drop_index_training"] = drop_index else: - filtered_df = self.check_pred_labels(filtered_df) + if not ft_params.get( + "principal_component_analysis", False + ): + filtered_df = self.check_pred_labels(filtered_df) # we are backtesting so we need to preserve row number to send back to strategy, # so now we use do_predict to avoid any prediction based on a NaN drop_index = pd.isnull(filtered_df).any(axis=1) From 033c5bd441b941a9b7de45b236f0fe66568f18ba Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Tue, 18 Oct 2022 12:55:47 +0200 Subject: [PATCH 02/41] Make check constant pred labels agnostic --- freqtrade/freqai/data_kitchen.py | 37 +++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 7f3e7cb01..edf6b366d 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -71,6 +71,7 @@ class FreqaiDataKitchen: self.data_path = Path() self.label_list: List = [] self.training_features_list: List = [] + self.constant_features_list: List = [] self.model_filename: str = "" self.backtesting_results_path = Path() self.backtest_predictions_folder: str = "backtesting_predictions" @@ -206,15 +207,14 @@ class FreqaiDataKitchen: drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. - ft_params = self.freqai_config["feature_parameters"] if (training_filter): - if not ft_params.get( - "principal_component_analysis", False - ): - const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) - if const_cols: - filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) - logger.warning(f"Removed features {const_cols} with constant values.") + const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) + if const_cols: + filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) + self.constant_features_list = const_cols + logger.warning(f"Removed features {const_cols} with constant values.") + else: + self.constant_features_list = [] # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here @@ -245,9 +245,7 @@ class FreqaiDataKitchen: self.data["filter_drop_index_training"] = drop_index else: - if not ft_params.get( - "principal_component_analysis", False - ): + if len(self.constant_features_list): filtered_df = self.check_pred_labels(filtered_df) # we are backtesting so we need to preserve row number to send back to strategy, # so now we use do_predict to avoid any prediction based on a NaN @@ -474,15 +472,14 @@ class FreqaiDataKitchen: :params: :df_predictions: incoming predictions """ - train_labels = self.data_dictionary["train_features"].columns - pred_labels = df_predictions.columns - num_diffs = len(pred_labels.difference(train_labels)) - if num_diffs != 0: - df_predictions = df_predictions[train_labels] - logger.warning( - f"Removed {num_diffs} features from prediction features, " - f"these were likely considered constant values during most recent training." - ) + constant_labels = self.constant_features_list + df_predictions = df_predictions.filter( + df_predictions.columns.difference(constant_labels) + ) + logger.warning( + f"Removed {len(constant_labels)} features from prediction features, " + f"these were considered constant values during most recent training." + ) return df_predictions From 60cb11a44d30c92b87117e21b12ac73f2cbb349d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Oct 2022 19:36:28 +0200 Subject: [PATCH 03/41] Add price jump warning --- docs/faq.md | 6 ++++++ freqtrade/data/converter.py | 8 ++++++++ tests/data/test_converter.py | 9 +++++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index a72268ef9..bcceaf898 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -102,6 +102,12 @@ If this happens for all pairs in the pairlist, this might indicate a recent exch Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. +### I'm getting "Price jump between 2 candles detected" + +This message is a warning that the candles had a price jump of > 30%. +This might be a sign that the pair stopped trading, and some token exchange took place (e.g. COCOS in 2021 - where price jumped from 0.0000154 to 0.01621). +This message is often accompanied by ["Missing data fillup"](#im-getting-missing-data-fillup-messages-in-the-log) - as trading on such pairs is often stopped for some time. + ### I'm getting "Outdated history for pair xxx" in the log The bot is trying to tell you that it got an outdated last candle (not the last complete candle). diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 98ed15489..7f7e79445 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -119,6 +119,14 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) else: # Don't be verbose if only a small amount is missing logger.debug(message) + candle_price_gap = 0 + if not df.empty and 'close' in df.columns: + returns = df['close'].pct_change().dropna() + if len(returns): + candle_price_gap = max(abs(returns)) + if candle_price_gap > 0.3: + logger.info(f"Price jump in {pair} between two candles of {candle_price_gap:.2%} detected.") + return df diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index f74383d15..83429bb38 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -80,10 +80,10 @@ def test_ohlcv_fill_up_missing_data2(caplog): ticks = [ [ 1511686200000, # 8:50:00 - 8.794e-05, # open - 8.948e-05, # high - 8.794e-05, # low - 8.88e-05, # close + 8.794e-07, # open + 8.948e-07, # high + 8.794e-07, # low + 8.88e-07, # close 2255, # volume (in quote currency) ], [ @@ -118,6 +118,7 @@ def test_ohlcv_fill_up_missing_data2(caplog): assert len(data) == 3 caplog.set_level(logging.DEBUG) data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC") + assert log_has_re(r"Price jump in .* between two candles .* detected\.", caplog) assert len(data2) == 4 # 3rd candle has been filled row = data2.loc[2, :] From 0ff7a0771d1b5ca038a37b7e07368005abadae72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Oct 2022 08:37:30 +0200 Subject: [PATCH 04/41] Move price_jump_warn to dataloading it's not relevant for live data, and should only run when loading data from disk. --- freqtrade/data/converter.py | 8 --- freqtrade/data/history/idatahandler.py | 14 +++++ tests/data/test_converter.py | 9 ++- tests/data/test_datahandler.py | 81 +++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 14 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 7f7e79445..98ed15489 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -119,14 +119,6 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) else: # Don't be verbose if only a small amount is missing logger.debug(message) - candle_price_gap = 0 - if not df.empty and 'close' in df.columns: - returns = df['close'].pct_change().dropna() - if len(returns): - candle_price_gap = max(abs(returns)) - if candle_price_gap > 0.3: - logger.info(f"Price jump in {pair} between two candles of {candle_price_gap:.2%} detected.") - return df diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 80e29f4c0..19d753043 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -335,6 +335,20 @@ class IDataHandler(ABC): "Use `freqtrade download-data` to download the data" ) return True + else: + candle_price_gap = 0 + if (candle_type in (CandleType.SPOT, CandleType.FUTURES) and + not pairdf.empty + and 'close' in pairdf.columns and 'open' in pairdf.columns): + # Detect gaps between prior close and open + gaps = ((pairdf['open'] - pairdf['close'].shift(1)) / pairdf['close'].shift(1)) + gaps = gaps.dropna() + if len(gaps): + candle_price_gap = max(abs(gaps)) + if candle_price_gap > 0.1: + logger.info(f"Price jump in {pair} between two candles of " + f"{candle_price_gap:.2%} detected.") + return False def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str, diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 83429bb38..f74383d15 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -80,10 +80,10 @@ def test_ohlcv_fill_up_missing_data2(caplog): ticks = [ [ 1511686200000, # 8:50:00 - 8.794e-07, # open - 8.948e-07, # high - 8.794e-07, # low - 8.88e-07, # close + 8.794e-05, # open + 8.948e-05, # high + 8.794e-05, # low + 8.88e-05, # close 2255, # volume (in quote currency) ], [ @@ -118,7 +118,6 @@ def test_ohlcv_fill_up_missing_data2(caplog): assert len(data) == 3 caplog.set_level(logging.DEBUG) data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC") - assert log_has_re(r"Price jump in .* between two candles .* detected\.", caplog) assert len(data2) == 4 # 3rd candle has been filled row = data2.loc[2, :] diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 5d6d60f84..4b1f9634c 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -15,7 +15,7 @@ from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler, g from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler from freqtrade.data.history.parquetdatahandler import ParquetDataHandler from freqtrade.enums import CandleType, TradingMode -from tests.conftest import log_has +from tests.conftest import log_has, log_has_re def test_datahandler_ohlcv_get_pairs(testdatadir): @@ -154,6 +154,85 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog): assert df.columns.equals(df1.columns) +def test_datahandler__check_empty_df(testdatadir, caplog): + dh = JsonDataHandler(testdatadir) + expected_text = r"Price jump in UNITTEST/USDT between" + df = DataFrame([ + [ + 1511686200000, # 8:50:00 + 8.794, # open + 8.948, # high + 8.794, # low + 8.88, # close + 2255, # volume (in quote currency) + ], + [ + 1511686500000, # 8:55:00 + 8.88, + 8.942, + 8.88, + 8.893, + 9911, + ], + [ + 1511687100000, # 9:05:00 + 8.891, + 8.893, + 8.875, + 8.877, + 2251 + ], + [ + 1511687400000, # 9:10:00 + 8.877, + 8.883, + 8.895, + 8.817, + 123551 + ] + ], columns=['date', 'open', 'high', 'low', 'close', 'volume']) + + dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True) + assert not log_has_re(expected_text, caplog) + df = DataFrame([ + [ + 1511686200000, # 8:50:00 + 8.794, # open + 8.948, # high + 8.794, # low + 8.88, # close + 2255, # volume (in quote currency) + ], + [ + 1511686500000, # 8:55:00 + 8.88, + 8.942, + 8.88, + 8.893, + 9911, + ], + [ + 1511687100000, # 9:05:00 + 889.1, # Price jump by several decimals + 889.3, + 887.5, + 887.7, + 2251 + ], + [ + 1511687400000, # 9:10:00 + 8.877, + 8.883, + 8.895, + 8.817, + 123551 + ] + ], columns=['date', 'open', 'high', 'low', 'close', 'volume']) + + dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True) + assert log_has_re(expected_text, caplog) + + @pytest.mark.parametrize('datahandler', ['feather', 'parquet']) def test_datahandler_trades_not_supported(datahandler, testdatadir, ): dh = get_datahandler(testdatadir, datahandler) From 547fd288115caddeb15632a2d2943bb3a0f6c67b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Oct 2022 08:43:37 +0200 Subject: [PATCH 05/41] Price-jump detection should only run once --- freqtrade/data/history/idatahandler.py | 13 +++++++------ tests/data/test_datahandler.py | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 19d753043..cbc3f1a34 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -303,7 +303,7 @@ class IDataHandler(ABC): timerange=timerange_startup, candle_type=candle_type ) - if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True): return pairdf else: enddate = pairdf.iloc[-1]['date'] @@ -323,8 +323,9 @@ class IDataHandler(ABC): self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data) return pairdf - def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, - candle_type: CandleType, warn_no_data: bool): + def _check_empty_df( + self, pairdf: DataFrame, pair: str, timeframe: str, candle_type: CandleType, + warn_no_data: bool, warn_price: bool = False) -> bool: """ Warn on empty dataframe """ @@ -335,7 +336,7 @@ class IDataHandler(ABC): "Use `freqtrade download-data` to download the data" ) return True - else: + elif warn_price: candle_price_gap = 0 if (candle_type in (CandleType.SPOT, CandleType.FUTURES) and not pairdf.empty @@ -346,8 +347,8 @@ class IDataHandler(ABC): if len(gaps): candle_price_gap = max(abs(gaps)) if candle_price_gap > 0.1: - logger.info(f"Price jump in {pair} between two candles of " - f"{candle_price_gap:.2%} detected.") + logger.info(f"Price jump in {pair}, {timeframe}, {candle_type} between two candles " + f"of {candle_price_gap:.2%} detected.") return False diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 4b1f9634c..67eeda7d0 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -156,7 +156,7 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog): def test_datahandler__check_empty_df(testdatadir, caplog): dh = JsonDataHandler(testdatadir) - expected_text = r"Price jump in UNITTEST/USDT between" + expected_text = r"Price jump in UNITTEST/USDT, 1h, spot between" df = DataFrame([ [ 1511686200000, # 8:50:00 @@ -192,7 +192,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog): ] ], columns=['date', 'open', 'high', 'low', 'close', 'volume']) - dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True) + dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True, True) assert not log_has_re(expected_text, caplog) df = DataFrame([ [ @@ -229,7 +229,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog): ] ], columns=['date', 'open', 'high', 'low', 'close', 'volume']) - dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True) + dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True, True) assert log_has_re(expected_text, caplog) From 3a9853db101d48978dbd7930889b19e0f702e2ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Oct 2022 12:52:13 +0200 Subject: [PATCH 06/41] use high/low for custom stoploss evaluation in backtesting --- freqtrade/strategy/interface.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 05744e845..681c5fcbb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1072,26 +1072,26 @@ class IStrategy(ABC, HyperStrategyMixin): trade.stop_loss > (high or current_rate) ) + # Make sure current_profit is calculated using high for backtesting. + bound = (low if trade.is_short else high) + bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound) if self.use_custom_stoploss and dir_correct: stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, - current_rate=current_rate, - current_profit=current_profit) + current_rate=(bound or current_rate), + current_profit=bound_profit) # Sanity check - error cases will return None if stop_loss_value: - # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") - trade.adjust_stop_loss(current_rate, stop_loss_value) + # logger.info(f"{trade.pair} {stop_loss_value=} {bound_profit=}") + trade.adjust_stop_loss(bound or current_rate, stop_loss_value) else: logger.warning("CustomStoploss function did not return valid stoploss") if self.trailing_stop and dir_correct: # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset - # Make sure current_profit is calculated using high for backtesting. - bound = low if trade.is_short else high - bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset): From 47e93dd2b2346510cbbbd841bb5159859ad23da5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Oct 2022 15:20:41 +0200 Subject: [PATCH 07/41] Update documentation --- docs/backtesting.md | 4 ++-- docs/strategy-callbacks.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index f20a53d22..eed741a23 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -522,13 +522,13 @@ Since backtesting lacks some detailed information about what happens within a ca - ROI - exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%) - exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit - - Forceexits caused by `=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) + - Force-exits caused by `=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss - Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered) - - On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point + - On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point. This rule is NOT applicable to custom-stoploss scenarios, since there's no information about the stoploss logic available. - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so exits with large high-low difference are backtested correctly) - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index ea10fc472..230968fb0 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -159,6 +159,7 @@ The stoploss price can only ever move upwards - if the stoploss value returned f The method must return a stoploss value (float / number) as a percentage of the current price. E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. +During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades). The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. From 2b6d00dde449934db8789c860d5e0e9dc9c528ab Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Sat, 22 Oct 2022 09:30:18 -0600 Subject: [PATCH 08/41] initial channel api change --- freqtrade/rpc/api_server/ws/channel.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index e9dbd63be..a1334bce9 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -24,6 +24,7 @@ class WebSocketChannel: self, websocket: WebSocketType, channel_id: Optional[str] = None, + drain_timeout: int = 3, serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer ): @@ -34,6 +35,8 @@ class WebSocketChannel: # The Serializing class for the WebSocket object self._serializer_cls = serializer_cls + self.drain_timeout = drain_timeout + self._subscriptions: List[str] = [] self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue(maxsize=32) self._relay_task = asyncio.create_task(self.relay()) @@ -57,11 +60,19 @@ class WebSocketChannel: """ await self._wrapped_ws.send(data) - async def send(self, data): + async def send(self, data) -> bool: """ - Add the data to the queue to be sent + Add the data to the queue to be sent. + :returns: True if data added to queue, False otherwise """ - self.queue.put_nowait(data) + try: + await asyncio.wait_for( + self.queue.put(data), + timeout=self.drain_timeout + ) + return True + except asyncio.TimeoutError: + return False async def recv(self): """ @@ -119,8 +130,8 @@ class WebSocketChannel: # Limit messages per sec. # Could cause problems with queue size if too low, and # problems with network traffik if too high. - # 0.001 = 1000/s - await asyncio.sleep(0.001) + # 0.01 = 100/s + await asyncio.sleep(0.01) except RuntimeError: # The connection was closed, just exit the task return @@ -186,9 +197,7 @@ class ChannelManager: message_type = data.get('type') for websocket, channel in self.channels.copy().items(): if channel.subscribed_to(message_type): - if not channel.queue.full(): - await channel.send(data) - else: + if not await channel.send(data): logger.info(f"Channel {channel} is too far behind, disconnecting") await self.on_disconnect(websocket) From 3d7a311caa3c58dbc4cfcfbe2e29a15abcfeffae Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Sat, 22 Oct 2022 19:02:05 -0600 Subject: [PATCH 09/41] removed sleep calls, better channel sending --- freqtrade/rpc/api_server/api_ws.py | 17 ++++++++-------- freqtrade/rpc/api_server/webserver.py | 4 ---- freqtrade/rpc/api_server/ws/channel.py | 23 +++++++++++++--------- freqtrade/rpc/api_server/ws/proxy.py | 4 ++++ freqtrade/rpc/external_message_consumer.py | 5 +++++ 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index 2f490b8a8..f3f6b852d 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -1,4 +1,3 @@ -import asyncio import logging from typing import Any, Dict @@ -11,6 +10,7 @@ from freqtrade.enums import RPCMessageType, RPCRequestType from freqtrade.rpc.api_server.api_auth import validate_ws_token from freqtrade.rpc.api_server.deps import get_channel_manager, get_rpc from freqtrade.rpc.api_server.ws import WebSocketChannel +from freqtrade.rpc.api_server.ws.channel import ChannelManager from freqtrade.rpc.api_server.ws_schemas import (WSAnalyzedDFMessage, WSMessageSchema, WSRequestSchema, WSWhitelistMessage) from freqtrade.rpc.rpc import RPC @@ -37,7 +37,8 @@ async def is_websocket_alive(ws: WebSocket) -> bool: async def _process_consumer_request( request: Dict[str, Any], channel: WebSocketChannel, - rpc: RPC + rpc: RPC, + channel_manager: ChannelManager ): """ Validate and handle a request from a websocket consumer @@ -72,9 +73,9 @@ async def _process_consumer_request( whitelist = rpc._ws_request_whitelist() # Format response - response = WSWhitelistMessage(data=whitelist) + response = WSWhitelistMessage(data=whitelist).dict(exclude_none=True) # Send it back - await channel.send(response.dict(exclude_none=True)) + await channel_manager.send_direct(channel, response) elif type == RPCRequestType.ANALYZED_DF: limit = None @@ -88,10 +89,8 @@ async def _process_consumer_request( # For every dataframe, send as a separate message for _, message in analyzed_df.items(): - response = WSAnalyzedDFMessage(data=message) - await channel.send(response.dict(exclude_none=True)) - # Throttle the messages to 50/s - await asyncio.sleep(0.02) + response = WSAnalyzedDFMessage(data=message).dict(exclude_none=True) + await channel_manager.send_direct(channel, response) @router.websocket("/message/ws") @@ -116,7 +115,7 @@ async def message_endpoint( request = await channel.recv() # Process the request here - await _process_consumer_request(request, channel, rpc) + await _process_consumer_request(request, channel, rpc, channel_manager) except (WebSocketDisconnect, WebSocketException): # Handle client disconnects diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 4a09fd78e..c6639f1a6 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -198,10 +198,6 @@ class ApiServer(RPCHandler): logger.debug(f"Found message of type: {message.get('type')}") # Broadcast it await self._ws_channel_manager.broadcast(message) - # Limit messages per sec. - # Could cause problems with queue size if too low, and - # problems with network traffik if too high. - await asyncio.sleep(0.001) except asyncio.CancelledError: pass diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index a1334bce9..4afca0d33 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -25,6 +25,7 @@ class WebSocketChannel: websocket: WebSocketType, channel_id: Optional[str] = None, drain_timeout: int = 3, + throttle: float = 0.01, serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer ): @@ -36,6 +37,7 @@ class WebSocketChannel: self._serializer_cls = serializer_cls self.drain_timeout = drain_timeout + self.throttle = throttle self._subscriptions: List[str] = [] self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue(maxsize=32) @@ -50,6 +52,10 @@ class WebSocketChannel: def __repr__(self): return f"WebSocketChannel({self.channel_id}, {self.remote_addr})" + @property + def raw(self): + return self._websocket.raw + @property def remote_addr(self): return self._websocket.remote_addr @@ -131,7 +137,7 @@ class WebSocketChannel: # Could cause problems with queue size if too low, and # problems with network traffik if too high. # 0.01 = 100/s - await asyncio.sleep(0.01) + await asyncio.sleep(self.throttle) except RuntimeError: # The connection was closed, just exit the task return @@ -171,6 +177,7 @@ class ChannelManager: with self._lock: channel = self.channels.get(websocket) if channel: + logger.info(f"Disconnecting channel {channel}") if not channel.is_closed(): await channel.close() @@ -181,9 +188,8 @@ class ChannelManager: Disconnect all Channels """ with self._lock: - for websocket, channel in self.channels.copy().items(): - if not channel.is_closed(): - await channel.close() + for websocket in self.channels.copy().keys(): + await self.on_disconnect(websocket) self.channels = dict() @@ -195,11 +201,9 @@ class ChannelManager: """ with self._lock: message_type = data.get('type') - for websocket, channel in self.channels.copy().items(): + for channel in self.channels.copy().values(): if channel.subscribed_to(message_type): - if not await channel.send(data): - logger.info(f"Channel {channel} is too far behind, disconnecting") - await self.on_disconnect(websocket) + await self.send_direct(channel, data) async def send_direct(self, channel, data): """ @@ -208,7 +212,8 @@ class ChannelManager: :param direct_channel: The WebSocketChannel object to send data through :param data: The data to send """ - await channel.send(data) + if not await channel.send(data): + await self.on_disconnect(channel.raw) def has_channels(self): """ diff --git a/freqtrade/rpc/api_server/ws/proxy.py b/freqtrade/rpc/api_server/ws/proxy.py index 2e5a59f05..8518709aa 100644 --- a/freqtrade/rpc/api_server/ws/proxy.py +++ b/freqtrade/rpc/api_server/ws/proxy.py @@ -15,6 +15,10 @@ class WebSocketProxy: def __init__(self, websocket: WebSocketType): self._websocket: Union[FastAPIWebSocket, WebSocket] = websocket + @property + def raw(self): + return self._websocket + @property def remote_addr(self) -> Tuple[Any, ...]: if isinstance(self._websocket, WebSocket): diff --git a/freqtrade/rpc/external_message_consumer.py b/freqtrade/rpc/external_message_consumer.py index 01bc974ad..e86f44c17 100644 --- a/freqtrade/rpc/external_message_consumer.py +++ b/freqtrade/rpc/external_message_consumer.py @@ -270,6 +270,11 @@ class ExternalMessageConsumer: logger.debug(f"Connection to {channel} still alive...") continue + except (websockets.exceptions.ConnectionClosed): + # Just eat the error and continue reconnecting + logger.warning(f"Disconnection in {channel} - retrying in {self.sleep_time}s") + await asyncio.sleep(self.sleep_time) + break except Exception as e: logger.warning(f"Ping error {channel} - retrying in {self.sleep_time}s") logger.debug(e, exc_info=e) From 9cffa3ca2b68ff66b5e6cfd8106b41cdb430cf79 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Sat, 22 Oct 2022 21:03:57 -0600 Subject: [PATCH 10/41] add comment in channel --- freqtrade/rpc/api_server/ws/channel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index 4afca0d33..92cb6dedc 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -40,6 +40,7 @@ class WebSocketChannel: self.throttle = throttle self._subscriptions: List[str] = [] + # 32 is the size of the receiving queue in websockets package self.queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue(maxsize=32) self._relay_task = asyncio.create_task(self.relay()) From c36141594eec5aed1da90b48354889d994c2b3e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 13:56:11 +0200 Subject: [PATCH 11/41] Simplify "refresh" condition --- 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 313d89e09..ca53e5333 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1996,9 +1996,9 @@ class Exchange: # Timeframe in seconds interval_in_sec = timeframe_to_seconds(timeframe) - return not ( + return ( (self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) - + interval_in_sec) >= arrow.utcnow().int_timestamp + + interval_in_sec) <= arrow.utcnow().int_timestamp ) @retrier_async From d0571464db6e01bf8a128993cec78279f29e56ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 14:17:01 +0200 Subject: [PATCH 12/41] Improve test for worker throttle --- freqtrade/worker.py | 7 ++++++- tests/test_worker.py | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index dea0acc44..9b81764b6 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -142,9 +142,14 @@ class Worker: sleep_duration = max(throttle_secs - time_passed, 0.0) logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " f"last iteration took {time_passed:.2f} s.") - time.sleep(sleep_duration) + self._sleep(sleep_duration) return result + @staticmethod + def _sleep(sleep_duration: float) -> None: + """Local sleep method - to improve testability""" + time.sleep(sleep_duration) + def _process_stopped(self) -> None: self.freqtrade.process_stopped() diff --git a/tests/test_worker.py b/tests/test_worker.py index ddca9525b..8e17f7bc1 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -1,7 +1,10 @@ import logging import time +from datetime import timedelta from unittest.mock import MagicMock, PropertyMock +import time_machine + from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import State from freqtrade.worker import Worker @@ -59,13 +62,48 @@ def test_throttle(mocker, default_conf, caplog) -> None: end = time.time() assert result == 42 - assert end - start > 0.1 + assert 0.3 > end - start > 0.1 assert log_has_re(r"Throttling with 'throttled_func\(\)': sleep for \d\.\d{2} s.*", caplog) result = worker._throttle(throttled_func, throttle_secs=-1) assert result == 42 +def test_throttle_sleep_time(mocker, default_conf, caplog) -> None: + + caplog.set_level(logging.DEBUG) + worker = get_patched_worker(mocker, default_conf) + sleep_mock = mocker.patch("freqtrade.worker.Worker._sleep") + with time_machine.travel("2022-09-01 05:00:00 +00:00") as t: + def throttled_func(x=1): + t.shift(timedelta(seconds=x)) + return 42 + + assert worker._throttle(throttled_func, throttle_secs=5) == 42 + # This moves the clock by 1 second + assert sleep_mock.call_count == 1 + assert 3.8 < sleep_mock.call_args[0][0] < 4.1 + + sleep_mock.reset_mock() + # This moves the clock by 1 second + assert worker._throttle(throttled_func, throttle_secs=10) == 42 + assert sleep_mock.call_count == 1 + assert 8.8 < sleep_mock.call_args[0][0] < 9.1 + + sleep_mock.reset_mock() + # This moves the clock by 5 second, so we only throttle by 5s + assert worker._throttle(throttled_func, throttle_secs=10, x=5) == 42 + assert sleep_mock.call_count == 1 + assert 4.8 < sleep_mock.call_args[0][0] < 5.1 + + t.move_to("2022-09-01 05:01:00 +00:00") + sleep_mock.reset_mock() + # Throttle for more than 5m (1 timeframe) + assert worker._throttle(throttled_func, throttle_secs=400, x=5) == 42 + assert sleep_mock.call_count == 1 + assert 394.8 < sleep_mock.call_args[0][0] < 395.1 + + def test_throttle_with_assets(mocker, default_conf) -> None: def throttled_func(nb_assets=-1): return nb_assets From 10090a36d59dd4997f43a52424209564a70507e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 14:51:17 +0200 Subject: [PATCH 13/41] simplify throttle --- freqtrade/worker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 9b81764b6..a8bb931f8 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -35,7 +35,6 @@ class Worker: self._config = config self._init(False) - self.last_throttle_start_time: float = 0 self._heartbeat_msg: float = 0 # Tell systemd that we completed initialization phase @@ -135,10 +134,10 @@ class Worker: :param throttle_secs: throttling interation execution time limit in seconds :return: Any (result of execution of func) """ - self.last_throttle_start_time = time.time() + last_throttle_start_time = time.time() logger.debug("========================================") result = func(*args, **kwargs) - time_passed = time.time() - self.last_throttle_start_time + time_passed = time.time() - last_throttle_start_time sleep_duration = max(throttle_secs - time_passed, 0.0) logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " f"last iteration took {time_passed:.2f} s.") From 49ff51f11f6e3be5e8ee2a252f64920f9cb983c0 Mon Sep 17 00:00:00 2001 From: th0rntwig Date: Sun, 23 Oct 2022 16:24:02 +0200 Subject: [PATCH 14/41] Change storage loc and fix test fail --- freqtrade/freqai/data_kitchen.py | 9 ++++----- tests/freqai/test_freqai_datakitchen.py | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index edf6b366d..37d9267bb 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -71,7 +71,6 @@ class FreqaiDataKitchen: self.data_path = Path() self.label_list: List = [] self.training_features_list: List = [] - self.constant_features_list: List = [] self.model_filename: str = "" self.backtesting_results_path = Path() self.backtest_predictions_folder: str = "backtesting_predictions" @@ -211,10 +210,10 @@ class FreqaiDataKitchen: const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) if const_cols: filtered_df = filtered_df.filter(filtered_df.columns.difference(const_cols)) - self.constant_features_list = const_cols + self.data['constant_features_list'] = const_cols logger.warning(f"Removed features {const_cols} with constant values.") else: - self.constant_features_list = [] + self.data['constant_features_list'] = [] # we don't care about total row number (total no. datapoints) in training, we only care # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here @@ -245,7 +244,7 @@ class FreqaiDataKitchen: self.data["filter_drop_index_training"] = drop_index else: - if len(self.constant_features_list): + if len(self.data['constant_features_list']): filtered_df = self.check_pred_labels(filtered_df) # we are backtesting so we need to preserve row number to send back to strategy, # so now we use do_predict to avoid any prediction based on a NaN @@ -472,7 +471,7 @@ class FreqaiDataKitchen: :params: :df_predictions: incoming predictions """ - constant_labels = self.constant_features_list + constant_labels = self.data['constant_features_list'] df_predictions = df_predictions.filter( df_predictions.columns.difference(constant_labels) ) diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index f60b29bf1..f1203877e 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -125,7 +125,8 @@ def test_normalize_data(mocker, freqai_conf): freqai = make_data_dictionary(mocker, freqai_conf) data_dict = freqai.dk.data_dictionary freqai.dk.normalize_data(data_dict) - assert len(freqai.dk.data) == 32 + assert any('_max' in entry for entry in freqai.dk.data.keys()) + assert any('_min' in entry for entry in freqai.dk.data.keys()) def test_filter_features(mocker, freqai_conf): From 94b65a007a6e2e163f18eb88647412e8f6b04860 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Sun, 23 Oct 2022 11:42:59 -0600 Subject: [PATCH 15/41] fix message typing in channel manager, minor improvements --- freqtrade/rpc/api_server/api_ws.py | 4 ++-- freqtrade/rpc/api_server/ws/channel.py | 30 ++++++++++++-------------- freqtrade/rpc/api_server/ws/proxy.py | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index f3f6b852d..cafbaefcb 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -73,7 +73,7 @@ async def _process_consumer_request( whitelist = rpc._ws_request_whitelist() # Format response - response = WSWhitelistMessage(data=whitelist).dict(exclude_none=True) + response = WSWhitelistMessage(data=whitelist) # Send it back await channel_manager.send_direct(channel, response) @@ -89,7 +89,7 @@ async def _process_consumer_request( # For every dataframe, send as a separate message for _, message in analyzed_df.items(): - response = WSAnalyzedDFMessage(data=message).dict(exclude_none=True) + response = WSAnalyzedDFMessage(data=message) await channel_manager.send_direct(channel, response) diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index 92cb6dedc..942a3df70 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -10,6 +10,7 @@ from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy from freqtrade.rpc.api_server.ws.serializer import (HybridJSONWebSocketSerializer, WebSocketSerializer) from freqtrade.rpc.api_server.ws.types import WebSocketType +from freqtrade.rpc.api_server.ws_schemas import WSMessageSchema logger = logging.getLogger(__name__) @@ -54,8 +55,8 @@ class WebSocketChannel: return f"WebSocketChannel({self.channel_id}, {self.remote_addr})" @property - def raw(self): - return self._websocket.raw + def raw_websocket(self): + return self._websocket.raw_websocket @property def remote_addr(self): @@ -192,29 +193,26 @@ class ChannelManager: for websocket in self.channels.copy().keys(): await self.on_disconnect(websocket) - self.channels = dict() - - async def broadcast(self, data): + async def broadcast(self, message: WSMessageSchema): """ - Broadcast data on all Channels + Broadcast a message on all Channels - :param data: The data to send + :param message: The message to send """ with self._lock: - message_type = data.get('type') for channel in self.channels.copy().values(): - if channel.subscribed_to(message_type): - await self.send_direct(channel, data) + if channel.subscribed_to(message.type): + await self.send_direct(channel, message) - async def send_direct(self, channel, data): + async def send_direct(self, channel: WebSocketChannel, message: WSMessageSchema): """ - Send data directly through direct_channel only + Send a message directly through direct_channel only - :param direct_channel: The WebSocketChannel object to send data through - :param data: The data to send + :param direct_channel: The WebSocketChannel object to send the message through + :param message: The message to send """ - if not await channel.send(data): - await self.on_disconnect(channel.raw) + if not await channel.send(message.dict(exclude_none=True)): + await self.on_disconnect(channel.raw_websocket) def has_channels(self): """ diff --git a/freqtrade/rpc/api_server/ws/proxy.py b/freqtrade/rpc/api_server/ws/proxy.py index 8518709aa..ae123dd2d 100644 --- a/freqtrade/rpc/api_server/ws/proxy.py +++ b/freqtrade/rpc/api_server/ws/proxy.py @@ -16,7 +16,7 @@ class WebSocketProxy: self._websocket: Union[FastAPIWebSocket, WebSocket] = websocket @property - def raw(self): + def raw_websocket(self): return self._websocket @property From c29f96a64337fb3c13d88ab320725214d33f8ad2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:02:32 +0000 Subject: [PATCH 16/41] Bump pytest-asyncio from 0.19.0 to 0.20.1 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.19.0 to 0.20.1. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.19.0...v0.20.1) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3f7277020..438f6112a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ flake8-tidy-imports==4.8.0 mypy==0.982 pre-commit==2.20.0 pytest==7.1.3 -pytest-asyncio==0.19.0 +pytest-asyncio==0.20.1 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-random-order==1.0.4 From 96f4de442a7f0589780b11431457af993afd7f98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:02:37 +0000 Subject: [PATCH 17/41] Bump progressbar2 from 4.0.0 to 4.1.1 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 4.0.0 to 4.1.1. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/wolph/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v4.0.0...v4.1.1) --- updated-dependencies: - dependency-name: progressbar2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 3e76a6c22..602a5e699 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,4 +6,4 @@ scipy==1.9.2 scikit-learn==1.1.2 scikit-optimize==0.9.0 filelock==3.8.0 -progressbar2==4.0.0 +progressbar2==4.1.1 From 54d029da7a24477dc9ec82a8617dd61768bded5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:02:47 +0000 Subject: [PATCH 18/41] Bump ccxt from 2.0.25 to 2.0.58 Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.0.25 to 2.0.58. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/2.0.25...2.0.58) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64d861469..6a1ff61f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.5.0; platform_machine != 'armv7l' pandas==1.4.3; platform_machine == 'armv7l' pandas-ta==0.3.14b -ccxt==2.0.25 +ccxt==2.0.58 # Pin cryptography for now due to rust build errors with piwheels cryptography==38.0.1 aiohttp==3.8.3 From 3480549f4ef8f709a74c9d1c6cc3fc3e1c071099 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:02:50 +0000 Subject: [PATCH 19/41] Bump python-rapidjson from 1.8 to 1.9 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.8 to 1.9. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.8...v1.9) --- updated-dependencies: - dependency-name: python-rapidjson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64d861469..e2b412dec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ pyarrow==9.0.0; platform_machine != 'armv7l' py_find_1st==1.1.5 # Load ticker files 30% faster -python-rapidjson==1.8 +python-rapidjson==1.9 # Properly format api responses orjson==3.8.0 From e516190b63d8919828952dc4dc3c04438ba3f86a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:02:54 +0000 Subject: [PATCH 20/41] Bump mkdocs-material from 8.5.6 to 8.5.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.6 to 8.5.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.6...8.5.7) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ad4aa7e89..ed7bec518 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.1 -mkdocs-material==8.5.6 +mkdocs-material==8.5.7 mdx_truly_sane_lists==1.3 pymdown-extensions==9.6 jinja2==3.1.2 From bde4fbbc592a03e941725b50e2b10ce7edd19823 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:02:57 +0000 Subject: [PATCH 21/41] Bump types-python-dateutil from 2.8.19.1 to 2.8.19.2 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.1 to 2.8.19.2. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3f7277020..c52ad5c1b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,4 +27,4 @@ types-cachetools==5.2.1 types-filelock==3.2.7 types-requests==2.28.11.2 types-tabulate==0.9.0.0 -types-python-dateutil==2.8.19.1 +types-python-dateutil==2.8.19.2 From 0328cd50260a535c7e3496fbb220db174d74bcc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:03:14 +0000 Subject: [PATCH 22/41] Bump scipy from 1.9.2 to 1.9.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.9.2 to 1.9.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.9.2...v1.9.3) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 3e76a6c22..47289aeec 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.9.2 +scipy==1.9.3 scikit-learn==1.1.2 scikit-optimize==0.9.0 filelock==3.8.0 From 06311b6a17ba2f10f385deeb229cf4b7f59655ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 03:03:19 +0000 Subject: [PATCH 23/41] Bump pyjwt from 2.5.0 to 2.6.0 Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.5.0 to 2.6.0. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/commits) --- updated-dependencies: - dependency-name: pyjwt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64d861469..9562e0823 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ sdnotify==0.3.2 fastapi==0.85.1 pydantic>=1.8.0 uvicorn==0.18.3 -pyjwt==2.5.0 +pyjwt==2.6.0 aiofiles==22.1.0 psutil==5.9.2 From af89c83fa553c3ff166adda1a67cd84a7bd6b6bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 04:39:32 +0000 Subject: [PATCH 24/41] Bump pymdown-extensions from 9.6 to 9.7 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.6 to 9.7. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.6...9.7) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ed7bec518..0e1e80e09 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,5 +2,5 @@ markdown==3.3.7 mkdocs==1.4.1 mkdocs-material==8.5.7 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.6 +pymdown-extensions==9.7 jinja2==3.1.2 From afe0a29fb05244430d4fef24accbd9cff19b3307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Oct 2022 06:22:46 +0000 Subject: [PATCH 25/41] Bump pandas from 1.5.0 to 1.5.1 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/main/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.5.0...v1.5.1) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6a1ff61f5..c710cbc5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.23.4 pandas==1.5.0; platform_machine != 'armv7l' # Piwheels doesn't have 1.5.0 yet. -pandas==1.4.3; platform_machine == 'armv7l' +pandas==1.5.1; platform_machine == 'armv7l' pandas-ta==0.3.14b ccxt==2.0.58 From b9bc91a881c42e267f9b5bd5e27ecb018b05b09e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Oct 2022 08:25:39 +0200 Subject: [PATCH 26/41] Bump correct pandas version --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c710cbc5d..b5f31c6be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.23.4 -pandas==1.5.0; platform_machine != 'armv7l' +pandas==1.5.1; platform_machine != 'armv7l' # Piwheels doesn't have 1.5.0 yet. -pandas==1.5.1; platform_machine == 'armv7l' +pandas==1.4.3; platform_machine == 'armv7l' pandas-ta==0.3.14b ccxt==2.0.58 From ba82cd9baa5fa555311ac2ccd07b154bc003beb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Oct 2022 12:29:04 +0200 Subject: [PATCH 27/41] bump types-python-dateutil for pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92d9dc450..ca42402dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.11.2 - types-tabulate==0.9.0.0 - - types-python-dateutil==2.8.19.1 + - types-python-dateutil==2.8.19.2 # stages: [push] - repo: https://github.com/pycqa/isort From 6669714a737f32e9536f7bcb0a87d64e8bccb4f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Oct 2022 06:53:42 +0200 Subject: [PATCH 28/41] Update mal-formatted docstrings --- freqtrade/freqai/data_kitchen.py | 3 +-- freqtrade/freqai/prediction_models/XGBoostRFClassifier.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index ed64fb162..c0becd5ae 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -468,8 +468,7 @@ class FreqaiDataKitchen: def check_pred_labels(self, df_predictions: DataFrame) -> DataFrame: """ Check that prediction feature labels match training feature labels. - :params: - :df_predictions: incoming predictions + :param df_predictions: incoming predictions """ constant_labels = self.data['constant_features_list'] df_predictions = df_predictions.filter( diff --git a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py index 1aba8df85..470c283ea 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py @@ -26,9 +26,8 @@ class XGBoostRFClassifier(BaseClassifierModel): def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :params: - :data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. """ X = data_dictionary["train_features"].to_numpy() @@ -65,7 +64,7 @@ class XGBoostRFClassifier(BaseClassifierModel): ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. - :param: unfiltered_df: Full dataframe for the current backtest period. + :param unfiltered_df: Full dataframe for the current backtest period. :return: :pred_df: dataframe containing the predictions :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove From 5bbd8615128a75da769e76cca57a3e772c2d4262 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Oct 2022 19:30:46 +0200 Subject: [PATCH 29/41] Update binance sample config with better blacklist --- config_examples/config_binance.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json index 35b9fcd20..3e99bd114 100644 --- a/config_examples/config_binance.example.json +++ b/config_examples/config_binance.example.json @@ -53,7 +53,7 @@ "XTZ/BTC" ], "pair_blacklist": [ - "BNB/BTC" + "BNB/.*" ] }, "pairlists": [ From f93b6eec63a9c0dbfd3d34c6592e86f51d9a56c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Oct 2022 14:56:51 +0200 Subject: [PATCH 30/41] Improve timing for worker throttling --- freqtrade/worker.py | 23 ++++++++++++++++++++--- tests/test_worker.py | 10 ++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index a8bb931f8..a407de0d7 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -14,6 +14,7 @@ from freqtrade.configuration import Configuration from freqtrade.constants import PROCESS_THROTTLE_SECS, RETRY_TIMEOUT, Config from freqtrade.enums import State from freqtrade.exceptions import OperationalException, TemporaryError +from freqtrade.exchange import timeframe_to_next_date from freqtrade.freqtradebot import FreqtradeBot @@ -111,7 +112,10 @@ class Worker: # Ping systemd watchdog before throttling self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.") - self._throttle(func=self._process_running, throttle_secs=self._throttle_secs) + # Use an offset of 1s to ensure a new candle has been issued + self._throttle(func=self._process_running, throttle_secs=self._throttle_secs, + timeframe=self._config['timeframe'] if self._config else None, + timeframe_offset=1) if self._heartbeat_interval: now = time.time() @@ -126,19 +130,32 @@ class Worker: return state - def _throttle(self, func: Callable[..., Any], throttle_secs: float, *args, **kwargs) -> Any: + def _throttle(self, func: Callable[..., Any], throttle_secs: float, + timeframe: Optional[str] = None, timeframe_offset: float = 1.0, + *args, **kwargs) -> Any: """ Throttles the given callable that it takes at least `min_secs` to finish execution. :param func: Any callable :param throttle_secs: throttling interation execution time limit in seconds + :param timeframe: ensure iteration is executed at the beginning of the next candle. + :param timeframe_offset: offset in seconds to apply to the next candle time. :return: Any (result of execution of func) """ last_throttle_start_time = time.time() logger.debug("========================================") result = func(*args, **kwargs) time_passed = time.time() - last_throttle_start_time - sleep_duration = max(throttle_secs - time_passed, 0.0) + sleep_duration = throttle_secs - time_passed + if timeframe: + next_tf = timeframe_to_next_date(timeframe) + # Maximum throttling should be until new candle arrives + # Offset of 0.2s is added to ensure a new candle has been issued. + next_tf_with_offset = next_tf.timestamp() - time.time() + timeframe_offset + sleep_duration = min(sleep_duration, next_tf_with_offset) + sleep_duration = max(sleep_duration, 0.0) + # next_iter = datetime.now(timezone.utc) + timedelta(seconds=sleep_duration) + logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " f"last iteration took {time_passed:.2f} s.") self._sleep(sleep_duration) diff --git a/tests/test_worker.py b/tests/test_worker.py index 8e17f7bc1..2237e7f4c 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -103,6 +103,16 @@ def test_throttle_sleep_time(mocker, default_conf, caplog) -> None: assert sleep_mock.call_count == 1 assert 394.8 < sleep_mock.call_args[0][0] < 395.1 + t.move_to("2022-09-01 05:01:00 +00:00") + + sleep_mock.reset_mock() + # Throttle for more than 5m (1 timeframe) + assert worker._throttle(throttled_func, throttle_secs=400, timeframe='5m', + timeframe_offset=0.4, x=5) == 42 + assert sleep_mock.call_count == 1 + # 300 (5m) - 60 (1m - see set time above) - 5 (duration of throttled_func) = 235 + assert 235 < sleep_mock.call_args[0][0] < 235.4 + def test_throttle_with_assets(mocker, default_conf) -> None: def throttled_func(nb_assets=-1): From 32600a113f7aed46e241a6de905ceb7be2f45f25 Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Mon, 24 Oct 2022 12:21:17 -0600 Subject: [PATCH 31/41] fix broadcast --- freqtrade/rpc/api_server/webserver.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index c6639f1a6..22a05f07b 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -16,6 +16,7 @@ from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.ws import ChannelManager +from freqtrade.rpc.api_server.ws_schemas import WSMessageSchema from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler @@ -127,10 +128,10 @@ class ApiServer(RPCHandler): cls._has_rpc = False cls._rpc = None - def send_msg(self, msg: Dict[str, str]) -> None: + def send_msg(self, msg: Dict[str, Any]) -> None: if self._ws_queue: sync_q = self._ws_queue.sync_q - sync_q.put(msg) + sync_q.put(WSMessageSchema(**msg)) def handle_rpc_exception(self, request, exc): logger.exception(f"API Error calling: {exc}") @@ -194,8 +195,8 @@ class ApiServer(RPCHandler): while True: logger.debug("Getting queue messages...") # Get data from queue - message = await async_queue.get() - logger.debug(f"Found message of type: {message.get('type')}") + message: WSMessageSchema = await async_queue.get() + logger.debug(f"Found message of type: {message.type}") # Broadcast it await self._ws_channel_manager.broadcast(message) except asyncio.CancelledError: From f70c00dd4c91f29cb5b7ecb47c04348a39282da4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 06:14:42 +0000 Subject: [PATCH 32/41] Improve variance around worker timing test --- tests/test_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_worker.py b/tests/test_worker.py index 2237e7f4c..ae511852f 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -111,7 +111,7 @@ def test_throttle_sleep_time(mocker, default_conf, caplog) -> None: timeframe_offset=0.4, x=5) == 42 assert sleep_mock.call_count == 1 # 300 (5m) - 60 (1m - see set time above) - 5 (duration of throttled_func) = 235 - assert 235 < sleep_mock.call_args[0][0] < 235.4 + assert 235.2 < sleep_mock.call_args[0][0] < 235.6 def test_throttle_with_assets(mocker, default_conf) -> None: From 283dab667de37cd300de97fe844e43ba055d34fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 09:04:31 +0000 Subject: [PATCH 33/41] Fix pydantic version pin --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ad3abb7cb..78d3dd1cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ sdnotify==0.3.2 # API Server fastapi==0.85.1 -pydantic>=1.8.0 +pydantic==1.10.2 uvicorn==0.18.3 pyjwt==2.6.0 aiofiles==22.1.0 From f12d40bd6bde4cb1f7a5ff5dd59f5dc5513e4041 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Tue, 25 Oct 2022 20:59:39 +1100 Subject: [PATCH 34/41] Update freqai-running.md Updated the follower section of the docs to include parameters to make the config schema validate on start up for follow_mode --- docs/freqai-running.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/freqai-running.md b/docs/freqai-running.md index 7972a6c92..8947a02bf 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -161,9 +161,13 @@ You can indicate to the bot that it should not train models, but instead should ```json "freqai": { + "enabled": true, "follow_mode": true, - "identifier": "example" + "identifier": "example", + "feature_parameters": { + // leader bots feature_parameters inserted here + }, } ``` -In this example, the user has a leader bot with the `"identifier": "example"`. The leader bot is already running or is launched simultaneously with the follower. The follower will load models created by the leader and inference them to obtain predictions instead of training its own models. +In this example, the user has a leader bot with the `"identifier": "example"`. The leader bot is already running or is launched simultaneously with the follower. The follower will load models created by the leader and inference them to obtain predictions instead of training its own models. The user will also need to duplicate the `feature_parameters` parameters from from the leaders freqai configuration file into the freqai section of the followers config. From 1ef38f137dd40162d828abe89eb936e7d9ec6829 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 13:37:04 +0200 Subject: [PATCH 35/41] Fix XGBoost regressor "used before assignment" --- freqtrade/freqai/prediction_models/XGBoostRFRegressor.py | 1 + freqtrade/freqai/prediction_models/XGBoostRegressor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py index 4c18d594d..e7cc27f2e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py @@ -29,6 +29,7 @@ class XGBoostRFRegressor(BaseRegressionModel): if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: eval_set = None + eval_weights = None else: eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] eval_weights = [data_dictionary['test_weights']] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index c9be9ce74..9a280286b 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -29,6 +29,7 @@ class XGBoostRegressor(BaseRegressionModel): if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: eval_set = None + eval_weights = None else: eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] eval_weights = [data_dictionary['test_weights']] From 3fa50077c945d9273ca98eef8f4e4f1ec31f6238 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 19:36:40 +0200 Subject: [PATCH 36/41] Don't use pydantic to type-verify outgoing messages --- freqtrade/rpc/api_server/api_ws.py | 4 ++-- freqtrade/rpc/api_server/webserver.py | 8 ++++---- freqtrade/rpc/api_server/ws/channel.py | 13 +++++++------ freqtrade/rpc/api_server/ws_schemas.py | 8 +++++++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index cafbaefcb..c33f9c730 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -75,7 +75,7 @@ async def _process_consumer_request( # Format response response = WSWhitelistMessage(data=whitelist) # Send it back - await channel_manager.send_direct(channel, response) + await channel_manager.send_direct(channel, response.dict(exclude_none=True)) elif type == RPCRequestType.ANALYZED_DF: limit = None @@ -90,7 +90,7 @@ async def _process_consumer_request( # For every dataframe, send as a separate message for _, message in analyzed_df.items(): response = WSAnalyzedDFMessage(data=message) - await channel_manager.send_direct(channel, response) + await channel_manager.send_direct(channel, response.dict(exclude_none=True)) @router.websocket("/message/ws") diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 22a05f07b..1d0192a89 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -16,7 +16,7 @@ from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.ws import ChannelManager -from freqtrade.rpc.api_server.ws_schemas import WSMessageSchema +from freqtrade.rpc.api_server.ws_schemas import WSMessageSchemaType from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler @@ -131,7 +131,7 @@ class ApiServer(RPCHandler): def send_msg(self, msg: Dict[str, Any]) -> None: if self._ws_queue: sync_q = self._ws_queue.sync_q - sync_q.put(WSMessageSchema(**msg)) + sync_q.put(msg) def handle_rpc_exception(self, request, exc): logger.exception(f"API Error calling: {exc}") @@ -195,8 +195,8 @@ class ApiServer(RPCHandler): while True: logger.debug("Getting queue messages...") # Get data from queue - message: WSMessageSchema = await async_queue.get() - logger.debug(f"Found message of type: {message.type}") + message: WSMessageSchemaType = await async_queue.get() + logger.debug(f"Found message of type: {message.get('type')}") # Broadcast it await self._ws_channel_manager.broadcast(message) except asyncio.CancelledError: diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index 942a3df70..34f03f0c4 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -1,7 +1,7 @@ import asyncio import logging from threading import RLock -from typing import Any, Dict, List, Optional, Type +from typing import Any, Dict, List, Optional, Type, Union from uuid import uuid4 from fastapi import WebSocket as FastAPIWebSocket @@ -10,7 +10,7 @@ from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy from freqtrade.rpc.api_server.ws.serializer import (HybridJSONWebSocketSerializer, WebSocketSerializer) from freqtrade.rpc.api_server.ws.types import WebSocketType -from freqtrade.rpc.api_server.ws_schemas import WSMessageSchema +from freqtrade.rpc.api_server.ws_schemas import WSMessageSchemaType logger = logging.getLogger(__name__) @@ -193,7 +193,7 @@ class ChannelManager: for websocket in self.channels.copy().keys(): await self.on_disconnect(websocket) - async def broadcast(self, message: WSMessageSchema): + async def broadcast(self, message: WSMessageSchemaType): """ Broadcast a message on all Channels @@ -201,17 +201,18 @@ class ChannelManager: """ with self._lock: for channel in self.channels.copy().values(): - if channel.subscribed_to(message.type): + if channel.subscribed_to(message.get('type')): await self.send_direct(channel, message) - async def send_direct(self, channel: WebSocketChannel, message: WSMessageSchema): + async def send_direct( + self, channel: WebSocketChannel, message: Union[WSMessageSchemaType, Dict[str, Any]]): """ Send a message directly through direct_channel only :param direct_channel: The WebSocketChannel object to send the message through :param message: The message to send """ - if not await channel.send(message.dict(exclude_none=True)): + if not await channel.send(message): await self.on_disconnect(channel.raw_websocket) def has_channels(self): diff --git a/freqtrade/rpc/api_server/ws_schemas.py b/freqtrade/rpc/api_server/ws_schemas.py index 255226d84..877232213 100644 --- a/freqtrade/rpc/api_server/ws_schemas.py +++ b/freqtrade/rpc/api_server/ws_schemas.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, TypedDict from pandas import DataFrame from pydantic import BaseModel @@ -18,6 +18,12 @@ class WSRequestSchema(BaseArbitraryModel): data: Optional[Any] = None +class WSMessageSchemaType(TypedDict): + # Type for typing to avoid doing pydantic typechecks. + type: RPCMessageType + data: Optional[Dict[str, Any]] + + class WSMessageSchema(BaseArbitraryModel): type: RPCMessageType data: Optional[Any] = None From d94c0039eb454d2f34fbb74b25d707e24cdc302e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 20:20:51 +0200 Subject: [PATCH 37/41] Add missing import to hyperopt docs --- docs/advanced-hyperopt.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 9933628d1..d80db1e62 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -78,6 +78,8 @@ This function needs to return a floating point number (`float`). Smaller numbers To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows: ```python +from freqtrade.optimize.space import Integer, SKDecimal + class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. From 604f966c82801e9bac7802633d5622f2e4f0e5e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 20:27:38 +0200 Subject: [PATCH 38/41] Enhance documentation with full space override sample closes #7643 --- docs/advanced-hyperopt.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index d80db1e62..0dace9985 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -78,7 +78,7 @@ This function needs to return a floating point number (`float`). Smaller numbers To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows: ```python -from freqtrade.optimize.space import Integer, SKDecimal +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal class MyAwesomeStrategy(IStrategy): class HyperOpt: @@ -96,6 +96,33 @@ class MyAwesomeStrategy(IStrategy): SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), ] + + def generate_roi_table(params: Dict) -> Dict[int, float]: + + roi_table = {} + roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] + roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] + roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] + roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 + + return roi_table + + def trailing_space() -> List[Dimension]: + # All parameters here are mandatory, you can only modify their type or the range. + return [ + # Fixed to true, if optimizing trailing_stop we assume to use trailing stop at all times. + Categorical([True], name='trailing_stop'), + + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), + # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', + # so this intermediate parameter is used as the value of the difference between + # them. The value of the 'trailing_stop_positive_offset' is constructed in the + # generate_trailing_params() method. + # This is similar to the hyperspace dimensions used for constructing the ROI tables. + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), + + Categorical([True, False], name='trailing_only_offset_is_reached'), + ] ``` !!! Note From fd5f31368c4335fb5fd61a82d60d7e910e71394e Mon Sep 17 00:00:00 2001 From: Timothy Pogue Date: Tue, 25 Oct 2022 14:08:28 -0600 Subject: [PATCH 39/41] fix indent in initial df send --- freqtrade/rpc/api_server/api_ws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index c33f9c730..b230cbe2b 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -90,7 +90,7 @@ async def _process_consumer_request( # For every dataframe, send as a separate message for _, message in analyzed_df.items(): response = WSAnalyzedDFMessage(data=message) - await channel_manager.send_direct(channel, response.dict(exclude_none=True)) + await channel_manager.send_direct(channel, response.dict(exclude_none=True)) @router.websocket("/message/ws") From d831d7d3170b848a80c05542549815ef9b4060f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Oct 2022 06:47:34 +0200 Subject: [PATCH 40/41] Rename Freqai hybrid example closes #7645 --- ...qaiHybridExampleStrategy.py => FreqaiExampleHybridStrategy.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename freqtrade/templates/{FreqaiHybridExampleStrategy.py => FreqaiExampleHybridStrategy.py} (100%) diff --git a/freqtrade/templates/FreqaiHybridExampleStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py similarity index 100% rename from freqtrade/templates/FreqaiHybridExampleStrategy.py rename to freqtrade/templates/FreqaiExampleHybridStrategy.py From 9e0b39cddcfced4ff690c128e9d8721c75fec4dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Oct 2022 06:56:33 +0200 Subject: [PATCH 41/41] Properly invert sign fixes 98ba57ff --- 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 ca53e5333..2a6be409f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1998,7 +1998,7 @@ class Exchange: return ( (self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) - + interval_in_sec) <= arrow.utcnow().int_timestamp + + interval_in_sec) < arrow.utcnow().int_timestamp ) @retrier_async