From ac2e8d760e2359ca5d26a8ce7dabf370873b0a6c Mon Sep 17 00:00:00 2001 From: rzrymiak <106121613+rzrymiak@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:24:44 -0700 Subject: [PATCH 001/132] Added description heading to README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 881895c9a..828a3d1f9 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) +## Description + Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) @@ -193,7 +195,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. -### Min hardware required +### Minimum hardware required To run this bot we recommend you a cloud instance with a minimum of: From 229e8864bbeb33bbd0b4c3bc525131939e276ad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 20:15:49 +0200 Subject: [PATCH 002/132] Add send_msg capability to dataprovider --- freqtrade/data/dataprovider.py | 21 +++++++++++++++++++++ freqtrade/enums/rpcmessagetype.py | 2 ++ freqtrade/freqtradebot.py | 1 + freqtrade/rpc/rpc_manager.py | 12 ++++++++++++ freqtrade/rpc/telegram.py | 3 ++- 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b9b118c00..800254533 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging +from collections import deque from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.enums import CandleType, RunMode @@ -33,6 +35,9 @@ class DataProvider: self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__slice_index: Optional[int] = None self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} + self._msg_queue: deque = deque() + self.__msg_cache = PeriodicCache( + maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) def _set_dataframe_max_index(self, limit_index: int): """ @@ -265,3 +270,19 @@ class DataProvider: if self._exchange is None: raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) + + def send_msg(self, message: str, always_send: bool = False) -> None: + """ + TODO: Document me + :param message: Message to be sent. Must be below 4096. + :param always_send: If False, will send the message only once per candle, and surpress + identical messages. + Careful as this can end up spaming your chat. + Defaults to False + """ + if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE): + return + + if always_send or message not in self.__msg_cache: + self._msg_queue.append(message) + self.__msg_cache[message] = True diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 584a011c2..415d8f18c 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -17,6 +17,8 @@ class RPCMessageType(Enum): PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' + STRATEGY_MSG = 'strategy_msg' + def __repr__(self): return self.value diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43608cae7..9ea195c45 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -214,6 +214,7 @@ class FreqtradeBot(LoggingMixin): if self.trading_mode == TradingMode.FUTURES: self._schedule.run_pending() Trade.commit() + self.rpc.process_msg_queue(self.dataprovider._msg_queue) self.last_process = datetime.now(timezone.utc) def process_stopped(self) -> None: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 66e84029f..3ccf23228 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -2,6 +2,7 @@ This module contains class to manage RPC communications (Telegram, API, ...) """ import logging +from collections import deque from typing import Any, Dict, List from freqtrade.enums import RPCMessageType @@ -77,6 +78,17 @@ class RPCManager: except NotImplementedError: logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") + def process_msg_queue(self, queue: deque) -> None: + """ + Process all messages in the queue. + """ + while queue: + msg = queue.popleft() + self.send_msg({ + 'type': RPCMessageType.STRATEGY_MSG, + 'msg': msg, + }) + def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2aff1d210..121324d90 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -376,7 +376,8 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.STARTUP: message = f"{msg['status']}" - + elif msg_type == RPCMessageType.STRATEGY_MSG: + message = f"{msg['msg']}" else: raise NotImplementedError(f"Unknown message type: {msg_type}") return message From 7bac0546681e770d3156db987620a666e4e1a489 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Jul 2022 20:24:52 +0200 Subject: [PATCH 003/132] Add documentation and clarity for send_msg --- docs/strategy-customization.md | 17 +++++++++++++++++ docs/telegram-usage.md | 4 +++- freqtrade/constants.py | 4 ++++ freqtrade/data/dataprovider.py | 8 +++++--- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6947380dd..38d34d51b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -731,6 +731,23 @@ if self.dp: !!! Warning "Warning about backtesting" This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results. +### Send Notification + +The dataprovider `.send_msg()` function allows you to send custom notifications from your strategy. +Identical notifications will only be sent once per candle, unless the 2nd argument (`always_send`) is set to True. + +``` python + self.dp.send_msg(f"{metadata['pair']} just got hot!") + + # Force send this notification, avoid caching (Please read warning below!) + self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True) +``` + +Notifications will only be sent in trading modes (Live/Dry-run) - so this method can be called without conditions for backtesting. + +!!! Warning "Spamming" + You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds. + ### Complete Data-provider sample ```python diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 9853e15c6..a690e18b9 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -98,6 +98,7 @@ Example configuration showing the different settings: "exit_fill": "off", "protection_trigger": "off", "protection_trigger_global": "on", + "strategy_msg": "off", "show_candle": "off" }, "reload": true, @@ -109,7 +110,8 @@ Example configuration showing the different settings: `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange. `*_fill` notifications are off by default and must be explicitly enabled. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. -`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc". +`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification). +`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`. `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. `reload` allows you to disable reload-buttons on selected messages. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6d74ceafd..1d83d21a0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -317,6 +317,10 @@ CONF_SCHEMA = { 'type': 'string', 'enum': ['off', 'ohlc'], }, + 'strategy_msg': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + }, } }, 'reload': {'type': 'boolean'}, diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 800254533..e21f10193 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -36,8 +36,9 @@ class DataProvider: self.__slice_index: Optional[int] = None self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} self._msg_queue: deque = deque() + self.__msg_cache = PeriodicCache( - maxsize=1000, ttl=timeframe_to_seconds(self._config['timeframe'])) + maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h'))) def _set_dataframe_max_index(self, limit_index: int): """ @@ -271,9 +272,10 @@ class DataProvider: raise OperationalException(NO_EXCHANGE_EXCEPTION) return self._exchange.fetch_l2_order_book(pair, maximum) - def send_msg(self, message: str, always_send: bool = False) -> None: + def send_msg(self, message: str, *, always_send: bool = False) -> None: """ - TODO: Document me + Send custom RPC Notifications from your bot. + Will not send any bot in modes other than Dry-run or Live. :param message: Message to be sent. Must be below 4096. :param always_send: If False, will send the message only once per candle, and surpress identical messages. From 0adfa4d9efe26d9423d769ca8829e723569c0545 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:32:23 +0200 Subject: [PATCH 004/132] Add tests for dataprovider send-message methods --- tests/data/test_dataprovider.py | 19 +++++++++++++++++++ tests/rpc/test_rpc_manager.py | 21 +++++++++++++++++++-- tests/rpc/test_rpc_telegram.py | 10 ++++++++++ tests/test_freqtradebot.py | 6 ++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 93f82de5d..843d60786 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -311,3 +311,22 @@ def test_no_exchange_mode(default_conf): with pytest.raises(OperationalException, match=message): dp.available_pairs() + + +def test_dp_send_msg(default_conf): + + default_conf["runmode"] = RunMode.DRY_RUN + + default_conf["timeframe"] = '1h' + dp = DataProvider(default_conf, None) + msg = 'Test message' + dp.send_msg(msg) + + assert msg in dp._msg_queue + dp._msg_queue.pop() + assert msg not in dp._msg_queue + # Message is not resent due to caching + dp.send_msg(msg) + assert msg not in dp._msg_queue + dp.send_msg(msg, always_send=True) + assert msg in dp._msg_queue diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 596b5ae20..b9ae16a20 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging import time +from collections import deque from unittest.mock import MagicMock from freqtrade.enums import RPCMessageType @@ -81,9 +82,25 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: assert telegram_mock.call_count == 0 +def test_process_msg_queue(mocker, default_conf, caplog) -> None: + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + queue = deque() + queue.append('Test message') + queue.append('Test message 2') + rpc_manager.process_msg_queue(queue) + + assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog) + assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog) + assert telegram_mock.call_count == 2 + + def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: - telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f69b7e878..8d244f3fd 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1994,6 +1994,16 @@ def test_startup_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' +def test_send_msg_strategy_msg_notification(default_conf, mocker) -> None: + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ + 'type': RPCMessageType.STRATEGY_MSG, + 'msg': 'hello world, Test msg' + }) + assert msg_mock.call_args[0][0] == 'hello world, Test msg' + + def test_send_msg_unknown_type(default_conf, mocker) -> None: telegram, _, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 66cbd7d9b..438a2704c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -68,6 +68,12 @@ def test_process_stopped(mocker, default_conf_usdt) -> None: assert coo_mock.call_count == 1 +def test_process_calls_sendmsg(mocker, default_conf_usdt) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + freqtrade.process() + assert freqtrade.rpc.process_msg_queue.call_count == 1 + + def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None: mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') From 31ddec834816d980d2802810868b62f44df787d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Jul 2022 06:51:56 +0200 Subject: [PATCH 005/132] Add missing test to confirm backtesting won't send messages --- tests/data/test_dataprovider.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 843d60786..49603feac 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -330,3 +330,8 @@ def test_dp_send_msg(default_conf): assert msg not in dp._msg_queue dp.send_msg(msg, always_send=True) assert msg in dp._msg_queue + + default_conf["runmode"] = RunMode.BACKTEST + dp = DataProvider(default_conf, None) + dp.send_msg(msg, always_send=True) + assert msg not in dp._msg_queue From c84d54b35ec455e02818bca592ce80c98a89d756 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 29 Jul 2022 08:12:50 +0200 Subject: [PATCH 006/132] Fix typing issue, avoid using .get() when unnecessary, convert to fstrings --- freqtrade/freqai/data_drawer.py | 45 ++++++++++--------- freqtrade/freqai/data_kitchen.py | 28 ++++++------ freqtrade/freqai/freqai_interface.py | 34 +++++++------- .../prediction_models/BaseRegressionModel.py | 4 +- .../CatboostPredictionModel.py | 3 +- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 4d37ef8c1..97cf7607a 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -11,7 +11,7 @@ import numpy as np import pandas as pd from joblib import dump, load from joblib.externals import cloudpickle -from numpy.typing import ArrayLike +from numpy.typing import ArrayLike, NDArray from pandas import DataFrame from freqtrade.configuration import TimeRange @@ -233,12 +233,13 @@ class FreqaiDataDrawer: mrv_df[f"{label}_mean"] = dk.data["labels_mean"][label] mrv_df[f"{label}_std"] = dk.data["labels_std"][label] - if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0) > 0: + if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0: mrv_df["DI_values"] = dk.DI_values mrv_df["do_predict"] = do_preds - def append_model_predictions(self, pair: str, predictions: DataFrame, do_preds: ArrayLike, + def append_model_predictions(self, pair: str, predictions: DataFrame, + do_preds: NDArray[np.int_], dk: FreqaiDataKitchen, len_df: int) -> None: # strat seems to feed us variable sized dataframes - and since we are trying to build our @@ -266,10 +267,10 @@ class FreqaiDataDrawer: df[label].iloc[-1] = predictions[label].iloc[-1] df[f"{label}_mean"].iloc[-1] = dk.data["labels_mean"][label] df[f"{label}_std"].iloc[-1] = dk.data["labels_std"][label] - # df['prediction'].iloc[-1] = predictions[-1] + df["do_predict"].iloc[-1] = do_preds[-1] - if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0) > 0: + if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0: df["DI_values"].iloc[-1] = dk.DI_values[-1] # append the new predictions to persistent storage @@ -309,7 +310,7 @@ class FreqaiDataDrawer: # dataframe['prediction'] = 0 dataframe["do_predict"] = 0 - if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0) > 0: + if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0: dataframe["DI_value"] = 0 dk.return_dataframe = dataframe @@ -379,24 +380,24 @@ class FreqaiDataDrawer: model.save(save_path / f"{dk.model_filename}_model.h5") if dk.svm_model is not None: - dump(dk.svm_model, save_path / str(dk.model_filename + "_svm_model.joblib")) + dump(dk.svm_model, save_path / f"{dk.model_filename}_svm_model.joblib") dk.data["data_path"] = str(dk.data_path) dk.data["model_filename"] = str(dk.model_filename) dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns) dk.data["label_list"] = dk.label_list # store the metadata - with open(save_path / str(dk.model_filename + "_metadata.json"), "w") as fp: + with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp: json.dump(dk.data, fp, default=dk.np_encoder) # save the train data to file so we can check preds for area of applicability later dk.data_dictionary["train_features"].to_pickle( - save_path / str(dk.model_filename + "_trained_df.pkl") + save_path / f"{dk.model_filename}_trained_df.pkl" ) - if self.freqai_info.get("feature_parameters", {}).get("principal_component_analysis"): + if self.freqai_info["feature_parameters"].get("principal_component_analysis"): cloudpickle.dump( - dk.pca, open(dk.data_path / str(dk.model_filename + "_pca_object.pkl"), "wb") + dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb") ) # if self.live: @@ -429,27 +430,27 @@ class FreqaiDataDrawer: / dk.data_path.parts[-1] ) - with open(dk.data_path / str(dk.model_filename + "_metadata.json"), "r") as fp: + with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp: dk.data = json.load(fp) dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] dk.data_dictionary["train_features"] = pd.read_pickle( - dk.data_path / str(dk.model_filename + "_trained_df.pkl") + dk.data_path / f"{dk.model_filename}_trained_df.pkl" ) # try to access model in memory instead of loading object from disk to save time if dk.live and dk.model_filename in self.model_dictionary: model = self.model_dictionary[dk.model_filename] elif not dk.keras: - model = load(dk.data_path / str(dk.model_filename + "_model.joblib")) + model = load(dk.data_path / f"{dk.model_filename}_model.joblib") else: from tensorflow import keras - model = keras.models.load_model(dk.data_path / str(dk.model_filename + "_model.h5")) + model = keras.models.load_model(dk.data_path / f"{dk.model_filename}_model.h5") - if Path(dk.data_path / str(dk.model_filename + "_svm_model.joblib")).resolve().exists(): - dk.svm_model = load(dk.data_path / str(dk.model_filename + "_svm_model.joblib")) + if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): + dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") if not model: raise OperationalException( @@ -458,7 +459,7 @@ class FreqaiDataDrawer: if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]: dk.pca = cloudpickle.load( - open(dk.data_path / str(dk.model_filename + "_pca_object.pkl"), "rb") + open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb") ) return model @@ -471,7 +472,7 @@ class FreqaiDataDrawer: :params: dataframe: DataFrame = strategy provided dataframe """ - feat_params = self.freqai_info.get("feature_parameters", {}) + feat_params = self.freqai_info["feature_parameters"] with self.history_lock: history_data = self.historic_data @@ -524,7 +525,7 @@ class FreqaiDataDrawer: for pair in dk.all_pairs: if pair not in history_data: history_data[pair] = {} - for tf in self.freqai_info.get("feature_parameters", {}).get("include_timeframes"): + for tf in self.freqai_info["feature_parameters"].get("include_timeframes"): history_data[pair][tf] = load_pair_history( datadir=self.config["datadir"], timeframe=tf, @@ -550,11 +551,11 @@ class FreqaiDataDrawer: corr_dataframes: Dict[Any, Any] = {} base_dataframes: Dict[Any, Any] = {} historic_data = self.historic_data - pairs = self.freqai_info.get("feature_parameters", {}).get( + pairs = self.freqai_info["feature_parameters"].get( "include_corr_pairlist", [] ) - for tf in self.freqai_info.get("feature_parameters", {}).get("include_timeframes"): + for tf in self.freqai_info["feature_parameters"].get("include_timeframes"): base_dataframes[tf] = dk.slice_dataframe(timerange, historic_data[pair][tf]) if pairs: for p in pairs: diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index f16e169b9..b5a3295b5 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -116,7 +116,7 @@ class FreqaiDataKitchen: :filtered_dataframe: cleaned dataframe ready to be split. :labels: cleaned labels ready to be split. """ - feat_dict = self.freqai_config.get("feature_parameters", {}) + feat_dict = self.freqai_config["feature_parameters"] weights: npt.ArrayLike if feat_dict.get("weight_factor", 0) > 0: @@ -515,7 +515,9 @@ class FreqaiDataKitchen: return if predict: - assert self.svm_model, "No svm model available for outlier removal" + if not self.svm_model: + logger.warning("No svm model available for outlier removal") + return y_pred = self.svm_model.predict(self.data_dictionary["prediction_features"]) do_predict = np.where(y_pred == -1, 0, y_pred) @@ -528,7 +530,7 @@ class FreqaiDataKitchen: else: # use SGDOneClassSVM to increase speed? - nu = self.freqai_config.get("feature_parameters", {}).get("svm_nu", 0.2) + nu = self.freqai_config["feature_parameters"].get("svm_nu", 0.2) self.svm_model = linear_model.SGDOneClassSVM(nu=nu).fit( self.data_dictionary["train_features"] ) @@ -551,7 +553,7 @@ class FreqaiDataKitchen: ) # same for test data - if self.freqai_config.get('data_split_parameters', {}).get('test_size', 0.1) != 0: + if self.freqai_config['data_split_parameters'].get('test_size', 0.1) != 0: y_pred = self.svm_model.predict(self.data_dictionary["test_features"]) dropped_points = np.where(y_pred == -1, 0, y_pred) self.data_dictionary["test_features"] = self.data_dictionary["test_features"][ @@ -605,7 +607,7 @@ class FreqaiDataKitchen: self.DI_values = distance.min(axis=0) / self.data["avg_mean_dist"] do_predict = np.where( - self.DI_values < self.freqai_config.get("feature_parameters", {}).get("DI_threshold"), + self.DI_values < self.freqai_config["feature_parameters"]["DI_threshold"], 1, 0, ) @@ -640,7 +642,7 @@ class FreqaiDataKitchen: self.append_df[f"{label}_std"] = self.data["labels_std"][label] self.append_df["do_predict"] = do_predict - if self.freqai_config.get("feature_parameters", {}).get("DI_threshold", 0) > 0: + if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0: self.append_df["DI_values"] = self.DI_values if self.full_df.empty: @@ -701,7 +703,7 @@ class FreqaiDataKitchen: full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d") self.full_path = Path( - self.config["user_data_dir"] / "models" / str(self.freqai_config.get("identifier")) + self.config["user_data_dir"] / "models" / f"{self.freqai_config['identifier']}" ) config_path = Path(self.config["config_files"][0]) @@ -741,10 +743,10 @@ class FreqaiDataKitchen: data_load_timerange = TimeRange() # find the max indicator length required - max_timeframe_chars = self.freqai_config.get("feature_parameters", {}).get( + max_timeframe_chars = self.freqai_config["feature_parameters"].get( "include_timeframes" )[-1] - max_period = self.freqai_config.get("feature_parameters", {}).get( + max_period = self.freqai_config["feature_parameters"].get( "indicator_max_period_candles", 50 ) additional_seconds = 0 @@ -832,7 +834,7 @@ class FreqaiDataKitchen: refresh_backtest_ohlcv_data( exchange, pairs=self.all_pairs, - timeframes=self.freqai_config.get("feature_parameters", {}).get("include_timeframes"), + timeframes=self.freqai_config["feature_parameters"].get("include_timeframes"), datadir=self.config["datadir"], timerange=timerange, new_pairs_days=new_pairs_days, @@ -845,7 +847,7 @@ class FreqaiDataKitchen: def set_all_pairs(self) -> None: self.all_pairs = copy.deepcopy( - self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) + self.freqai_config["feature_parameters"].get("include_corr_pairlist", []) ) for pair in self.config.get("exchange", "").get("pair_whitelist"): if pair not in self.all_pairs: @@ -876,8 +878,8 @@ class FreqaiDataKitchen: # for prediction dataframe creation, we let dataprovider handle everything in the strategy # so we create empty dictionaries, which allows us to pass None to # `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe. - tfs = self.freqai_config.get("feature_parameters", {}).get("include_timeframes") - pairs = self.freqai_config.get("feature_parameters", {}).get("include_corr_pairlist", []) + tfs = self.freqai_config["feature_parameters"].get("include_timeframes") + pairs = self.freqai_config["feature_parameters"].get("include_corr_pairlist", []) if not prediction_dataframe.empty: dataframe = prediction_dataframe.copy() for tf in tfs: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index ec69a78c4..47aeb32e4 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Tuple import numpy as np import pandas as pd -from numpy.typing import ArrayLike +from numpy.typing import NDArray from pandas import DataFrame from freqtrade.configuration import TimeRange @@ -204,14 +204,9 @@ class IFreqaiModel(ABC): dk.data_path = Path( dk.full_path - / str( - "sub-train" - + "-" - + metadata["pair"].split("/")[0] - + "_" - + str(int(trained_timestamp.stopts)) + / + f"sub-train-{metadata['pair'].split('/')[0]}_{int(trained_timestamp.stopts)}" ) - ) if not self.model_exists( metadata["pair"], dk, trained_timestamp=int(trained_timestamp.stopts) ): @@ -331,7 +326,8 @@ class IFreqaiModel(ABC): return elif self.dk.check_if_model_expired(trained_timestamp): pred_df = DataFrame(np.zeros((2, len(dk.label_list))), columns=dk.label_list) - do_preds, dk.DI_values = np.ones(2) * 2, np.zeros(2) + do_preds = np.ones(2, dtype=np.int_) * 2 + dk.DI_values = np.zeros(2) logger.warning( f"Model expired for {pair}, returning null values to strategy. Strategy " "construction should take care to consider this event with " @@ -379,15 +375,15 @@ class IFreqaiModel(ABC): example of how outlier data points are dropped from the dataframe used for training. """ - if self.freqai_info.get("feature_parameters", {}).get( + if self.freqai_info["feature_parameters"].get( "principal_component_analysis", False ): dk.principal_component_analysis() - if self.freqai_info.get("feature_parameters", {}).get("use_SVM_to_remove_outliers", False): + if self.freqai_info["feature_parameters"].get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=False) - if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): + if self.freqai_info["feature_parameters"].get("DI_threshold", 0): dk.data["avg_mean_dist"] = dk.compute_distances() def data_cleaning_predict(self, dk: FreqaiDataKitchen, dataframe: DataFrame) -> None: @@ -401,15 +397,15 @@ class IFreqaiModel(ABC): of how the do_predict vector is modified. do_predict is ultimately passed back to strategy for buy signals. """ - if self.freqai_info.get("feature_parameters", {}).get( + if self.freqai_info["feature_parameters"].get( "principal_component_analysis", False ): dk.pca_transform(dataframe) - if self.freqai_info.get("feature_parameters", {}).get("use_SVM_to_remove_outliers", False): + if self.freqai_info["feature_parameters"].get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=True) - if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): + if self.freqai_info["feature_parameters"].get("DI_threshold", 0): dk.check_if_pred_in_training_spaces() def model_exists( @@ -430,9 +426,9 @@ class IFreqaiModel(ABC): coin, _ = pair.split("/") if not self.live: - dk.model_filename = model_filename = "cb_" + coin.lower() + "_" + str(trained_timestamp) + dk.model_filename = model_filename = f"cb_{coin.lower()}_{trained_timestamp}" - path_to_modelfile = Path(dk.data_path / str(model_filename + "_model.joblib")) + path_to_modelfile = Path(dk.data_path / f"{model_filename}_model.joblib") file_exists = path_to_modelfile.is_file() if file_exists and not scanning: logger.info("Found model at %s", dk.data_path / dk.model_filename) @@ -442,7 +438,7 @@ class IFreqaiModel(ABC): def set_full_path(self) -> None: self.full_path = Path( - self.config["user_data_dir"] / "models" / str(self.freqai_info.get("identifier")) + self.config["user_data_dir"] / "models" / f"{self.freqai_info['identifier']}" ) self.full_path.mkdir(parents=True, exist_ok=True) shutil.copy( @@ -550,7 +546,7 @@ class IFreqaiModel(ABC): @abstractmethod def predict( self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True - ) -> Tuple[DataFrame, ArrayLike]: + ) -> Tuple[DataFrame, NDArray[np.int_]]: """ Filter the prediction features data and predict with it. :param unfiltered_dataframe: Full dataframe for the current backtest period. diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 2baec9fc3..85d7ae1ee 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -3,7 +3,7 @@ from typing import Any, Tuple import numpy.typing as npt from pandas import DataFrame - +import numpy as np from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel @@ -85,7 +85,7 @@ class BaseRegressionModel(IFreqaiModel): def predict( self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False - ) -> Tuple[DataFrame, npt.ArrayLike]: + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: """ Filter the prediction features data and predict with it. :param: unfiltered_dataframe: Full dataframe for the current backtest period. diff --git a/freqtrade/freqai/prediction_models/CatboostPredictionModel.py b/freqtrade/freqai/prediction_models/CatboostPredictionModel.py index 884933803..9731e0c01 100644 --- a/freqtrade/freqai/prediction_models/CatboostPredictionModel.py +++ b/freqtrade/freqai/prediction_models/CatboostPredictionModel.py @@ -1,6 +1,7 @@ +import gc import logging from typing import Any, Dict -import gc + from catboost import CatBoostRegressor, Pool from freqtrade.freqai.prediction_models.BaseRegressionModel import BaseRegressionModel From 59624181bd1005feb93e822e0fffddf0644fec44 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 29 Jul 2022 08:23:44 +0200 Subject: [PATCH 007/132] isort BaseRegressionModel imports --- freqtrade/freqai/prediction_models/BaseRegressionModel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 85d7ae1ee..1901c18fe 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -1,9 +1,10 @@ import logging from typing import Any, Tuple +import numpy as np import numpy.typing as npt from pandas import DataFrame -import numpy as np + from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel From 08d3ac7ef821e42e428d8250c279fb00c8da553e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 29 Jul 2022 08:49:35 +0200 Subject: [PATCH 008/132] add keras and conv_width to schema and documentation --- docs/freqai.md | 3 +++ freqtrade/constants.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/freqai.md b/docs/freqai.md index 4060b5394..3c04d5b31 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -110,6 +110,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `n_estimators` | A common parameter among regressors which sets the number of boosted trees to fit
**Datatype:** integer. | `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. | `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. +| | **Extraneous parameters** +| `keras` | If your model makes use of keras (typical of Tensorflow based prediction models), activate this flag so that the model save/loading follows keras standards. Default value `false`
**Datatype:** boolean. +| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for `shift` by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. Default value, 2
**Datatype:** integer. ### Important FreqAI dataframe key patterns diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 90c87809b..0134dafc0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -481,6 +481,8 @@ CONF_SCHEMA = { "freqai": { "type": "object", "properties": { + "keras": {"type": "boolean", "default": False}, + "conv_width": {"type": "integer", "default": 2}, "train_period_days": {"type": "integer", "default": 0}, "backtest_period_days": {"type": "float", "default": 7}, "identifier": {"type": "str", "default": "example"}, From f22b14078259aea39ba6731091538dc307fccc30 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 29 Jul 2022 17:27:35 +0200 Subject: [PATCH 009/132] fix backtesting bug, undo move of label stat calc, fix example strat exit logic --- freqtrade/freqai/data_kitchen.py | 18 +++++++++--------- freqtrade/freqai/freqai_interface.py | 3 --- .../prediction_models/BaseRegressionModel.py | 4 ++-- freqtrade/templates/FreqaiExampleStrategy.py | 2 +- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index b5a3295b5..788572ba6 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -635,20 +635,20 @@ class FreqaiDataKitchen: Append backtest prediction from current backtest period to all previous periods """ - self.append_df = DataFrame() + append_df = DataFrame() for label in self.label_list: - self.append_df[label] = predictions[label] - self.append_df[f"{label}_mean"] = self.data["labels_mean"][label] - self.append_df[f"{label}_std"] = self.data["labels_std"][label] + append_df[label] = predictions[label] + append_df[f"{label}_mean"] = self.data["labels_mean"][label] + append_df[f"{label}_std"] = self.data["labels_std"][label] - self.append_df["do_predict"] = do_predict + append_df["do_predict"] = do_predict if self.freqai_config["feature_parameters"].get("DI_threshold", 0) > 0: - self.append_df["DI_values"] = self.DI_values + append_df["DI_values"] = self.DI_values if self.full_df.empty: - self.full_df = self.append_df + self.full_df = append_df else: - self.full_df = pd.concat([self.full_df, self.append_df], axis=0) + self.full_df = pd.concat([self.full_df, append_df], axis=0) return @@ -668,7 +668,7 @@ class FreqaiDataKitchen: to_keep = [col for col in dataframe.columns if not col.startswith("&")] self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1) - self.append_df = DataFrame() + # self.append_df = DataFrame() self.full_df = DataFrame() return diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 47aeb32e4..3b97637a8 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -1,7 +1,6 @@ # import contextlib import copy import datetime -import gc import logging import shutil import threading @@ -183,8 +182,6 @@ class IFreqaiModel(ABC): (_, _, _) = self.dd.get_pair_dict_info(metadata["pair"]) train_it += 1 total_trains = len(dk.backtesting_timeranges) - gc.collect() - dk.data = {} # clean the pair specific data between training window sliding self.training_timerange = tr_train dataframe_train = dk.slice_dataframe(tr_train, dataframe) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) diff --git a/freqtrade/freqai/prediction_models/BaseRegressionModel.py b/freqtrade/freqai/prediction_models/BaseRegressionModel.py index 1901c18fe..112e48183 100644 --- a/freqtrade/freqai/prediction_models/BaseRegressionModel.py +++ b/freqtrade/freqai/prediction_models/BaseRegressionModel.py @@ -56,6 +56,8 @@ class BaseRegressionModel(IFreqaiModel): f"{end_date}--------------------") # split data into train/test data. data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) + if not self.freqai_info.get('fit_live_predictions', 0) or not self.live: + dk.fit_labels() # normalize all data based on train_dataset only data_dictionary = dk.normalize_data(data_dictionary) @@ -75,8 +77,6 @@ class BaseRegressionModel(IFreqaiModel): if self.freqai_info.get('fit_live_predictions_candles', 0) and self.live: self.fit_live_predictions(dk) - else: - dk.fit_labels() self.dd.save_historic_predictions_to_disk() diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 58eb47532..1196405ab 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -235,7 +235,7 @@ class FreqaiExampleStrategy(IStrategy): if ( "prediction" + entry_tag not in pair_dict[pair] - or pair_dict[pair]["prediction" + entry_tag] > 0 + or pair_dict[pair]["prediction" + entry_tag] == 0 ): with self.freqai.lock: pair_dict[pair]["prediction" + entry_tag] = abs(trade_candle["&-s_close"]) From d70650b074e023c649a04ce15ca84946b3409eb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 08:20:22 +0200 Subject: [PATCH 010/132] Add note for plot-dataframe and current-whitelist closes #7142 --- docs/strategy-customization.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6947380dd..78256e0ee 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -646,6 +646,9 @@ This is where calling `self.dp.current_whitelist()` comes in handy. return informative_pairs ``` +??? Note "Plotting with current_whitelist" + Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist - and would therefore make the return values of this method misleading. + ### *get_pair_dataframe(pair, timeframe)* ``` python From dd8288c090a7abb0c6e503e80bdfd7c2220decca Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 30 Jul 2022 13:40:05 +0200 Subject: [PATCH 011/132] expose full parameter set for SVM outlier detection. Set default shuffle to false to improve reproducibility --- docs/freqai.md | 2 +- freqtrade/freqai/data_kitchen.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index 3c04d5b31..c4d0ad0f8 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -97,7 +97,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](##controlling-the-model-learning-process).
**Datatype:** positive float (typically below 1). | `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA.
**Datatype:** boolean. | `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points.
**Datatype:** boolean. -| `svm_nu` | The `nu` parameter for the support vector machine. *Very* broadly, this is the percentage of data points that should be considered outliers.
**Datatype:** float between 0 and 1. +| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. E.g. `nu` *Very* broadly, is the percentage of data points that should be considered outliers. `shuffle` is by default false to maintain reprodicibility. But these and all others can be added/changed in this dictionary.
**Datatype:** dictionary. | `stratify_training_data` | This value is used to indicate the stratification of the data. e.g. 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing.
**Datatype:** positive integer. | `indicator_max_period_candles` | The maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN
**Datatype:** positive integer. | `indicator_periods_candles` | A list of integers used to duplicate all indicators according to a set of periods and add them to the feature set.
**Datatype:** list of positive integers. diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 788572ba6..4a936475a 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -530,8 +530,9 @@ class FreqaiDataKitchen: else: # use SGDOneClassSVM to increase speed? - nu = self.freqai_config["feature_parameters"].get("svm_nu", 0.2) - self.svm_model = linear_model.SGDOneClassSVM(nu=nu).fit( + svm_params = self.freqai_config["feature_parameters"].get( + "svm_params", {"shuffle": False, "nu": 0.1}) + self.svm_model = linear_model.SGDOneClassSVM(**svm_params).fit( self.data_dictionary["train_features"] ) y_pred = self.svm_model.predict(self.data_dictionary["train_features"]) From 995be90f91a21c351a5c22264135694f3e11d62b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jul 2022 20:35:23 +0200 Subject: [PATCH 012/132] Liquidation should be a separate exit type --- freqtrade/enums/exittype.py | 1 + freqtrade/optimize/backtesting.py | 14 ++++++++++---- freqtrade/persistence/trade_model.py | 12 ++---------- freqtrade/plugins/protections/stoploss_guard.py | 2 +- freqtrade/strategy/interface.py | 13 ++++++++++++- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/freqtrade/enums/exittype.py b/freqtrade/enums/exittype.py index b2c5b62ea..1e15e70cd 100644 --- a/freqtrade/enums/exittype.py +++ b/freqtrade/enums/exittype.py @@ -9,6 +9,7 @@ class ExitType(Enum): STOP_LOSS = "stop_loss" STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" TRAILING_STOP_LOSS = "trailing_stop_loss" + LIQUIDATION = "liquidation" EXIT_SIGNAL = "exit_signal" FORCE_EXIT = "force_exit" EMERGENCY_EXIT = "emergency_exit" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4d16dc0f1..598ce6710 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -381,7 +381,8 @@ class Backtesting: Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + if exit.exit_type in ( + ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION): return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur) elif exit.exit_type == (ExitType.ROI): return self._get_close_rate_for_roi(row, trade, exit, trade_dur) @@ -396,11 +397,16 @@ class Backtesting: is_short = trade.is_short or False leverage = trade.leverage or 1.0 side_1 = -1 if is_short else 1 + if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price: + stoploss_value = trade.liquidation_price + else: + stoploss_value = trade.stop_loss + if is_short: - if trade.stop_loss < row[LOW_IDX]: + if stoploss_value < row[LOW_IDX]: return row[OPEN_IDX] else: - if trade.stop_loss > row[HIGH_IDX]: + if stoploss_value > row[HIGH_IDX]: return row[OPEN_IDX] # Special case: trailing triggers within same candle as trade opened. Assume most @@ -433,7 +439,7 @@ class Backtesting: return max(row[LOW_IDX], stop_rate) # Set close_rate to stoploss - return trade.stop_loss + return stoploss_value def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple, trade_dur: int) -> float: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 5f302de71..2ff65d9d0 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -511,17 +511,9 @@ class LocalTrade(): Method you should use to set self.stop_loss. Assures stop_loss is not passed the liquidation price """ - if self.liquidation_price is not None: - if self.is_short: - sl = min(stop_loss, self.liquidation_price) - else: - sl = max(stop_loss, self.liquidation_price) - else: - sl = stop_loss - if not self.stop_loss: - self.initial_stop_loss = sl - self.stop_loss = sl + self.initial_stop_loss = stop_loss + self.stop_loss = stop_loss self.stop_loss_pct = -1 * abs(percent) self.stoploss_last_update = datetime.utcnow() diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index abc90a685..e80d13e9d 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -49,7 +49,7 @@ class StoplossGuard(IProtection): trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades = [trade for trade in trades1 if (str(trade.exit_reason) in ( ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value, - ExitType.STOPLOSS_ON_EXCHANGE.value) + ExitType.STOPLOSS_ON_EXCHANGE.value, ExitType.LIQUIDATION.value) and trade.close_profit and trade.close_profit < self._profit_limit)] if self._only_per_side: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c60817c99..f50721583 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -963,7 +963,7 @@ class IStrategy(ABC, HyperStrategyMixin): # ROI # Trailing stoploss - if stoplossflag.exit_type == ExitType.STOP_LOSS: + if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION): logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}") exits.append(stoplossflag) @@ -1035,6 +1035,17 @@ class IStrategy(ABC, HyperStrategyMixin): sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short) + liq_higher_long = (trade.liquidation_price + and trade.liquidation_price >= (low or current_rate) + and not trade.is_short) + liq_lower_short = (trade.liquidation_price + and trade.liquidation_price <= (high or current_rate) + and trade.is_short) + + if (liq_higher_long or liq_lower_short): + logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION") + return ExitCheckTuple(exit_type=ExitType.LIQUIDATION) + # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. From 8711b7d99f224b3d0ee26553e32774dd247d605a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Jul 2022 20:37:10 +0200 Subject: [PATCH 013/132] Liquidations cannot be rejected. --- freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43608cae7..d58c05d7f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1463,11 +1463,12 @@ class FreqtradeBot(LoggingMixin): amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['exit'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + if (exit_check.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, exit_reason=exit_reason, sell_reason=exit_reason, # sellreason -> compatibility - current_time=datetime.now(timezone.utc)): + current_time=datetime.now(timezone.utc))): logger.info(f"User denied exit for {trade.pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 598ce6710..6bbace185 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -598,7 +598,8 @@ class Backtesting: # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['exit'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, # type: ignore[arg-type] order_type='limit', @@ -607,7 +608,7 @@ class Backtesting: time_in_force=time_in_force, sell_reason=exit_reason, # deprecated exit_reason=exit_reason, - current_time=exit_candle_time): + current_time=exit_candle_time)): return None trade.exit_reason = exit_reason From f57ecb18615dc2e75fc73d0da78540ea0ebd6804 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 06:55:42 +0200 Subject: [PATCH 014/132] Simplify adjust_stop test --- tests/test_persistence.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 838c4c22a..4703075eb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1537,26 +1537,26 @@ def test_adjust_stop_loss(fee): # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(1.3, -0.1) - assert round(trade.stop_loss, 8) == 1.17 + assert pytest.approx(trade.stop_loss) == 1.17 assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate lower again ... should not change trade.adjust_stop_loss(1.2, 0.1) - assert round(trade.stop_loss, 8) == 1.17 + assert pytest.approx(trade.stop_loss) == 1.17 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate higher... should raise stoploss trade.adjust_stop_loss(1.4, 0.1) - assert round(trade.stop_loss, 8) == 1.26 + assert pytest.approx(trade.stop_loss) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(1.7, 0.1, True) - assert round(trade.stop_loss, 8) == 1.26 + assert pytest.approx(trade.stop_loss) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 From 9852733ef7f7284765b8092eb6cd27edd2b7c0e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 06:58:59 +0200 Subject: [PATCH 015/132] Improve tests to align with modified logic --- freqtrade/persistence/trade_model.py | 6 ------ tests/test_persistence.py | 17 ++++++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 2ff65d9d0..919750886 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -535,14 +535,8 @@ class LocalTrade(): leverage = self.leverage or 1.0 if self.is_short: new_loss = float(current_price * (1 + abs(stoploss / leverage))) - # If trading with leverage, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = min(self.liquidation_price, new_loss) else: new_loss = float(current_price * (1 - abs(stoploss / leverage))) - # If trading with leverage, don't set the stoploss below the liquidation price - if self.liquidation_price: - new_loss = max(self.liquidation_price, new_loss) # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4703075eb..5476e1d50 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -133,13 +133,14 @@ def test_set_stop_loss_isolated_liq(fee): trade.set_isolated_liq(0.11) trade._set_stop_loss(0.1, 0) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.11 + # Stoploss does not change from liquidation price + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.1 # lower stop doesn't move stoploss trade._set_stop_loss(0.1, 0) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.11 + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.1 trade.stop_loss = None @@ -174,13 +175,14 @@ def test_set_stop_loss_isolated_liq(fee): trade.set_isolated_liq(0.07) trade._set_stop_loss(0.1, (1.0 / 8.0)) assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.07 + # Stoploss does not change from liquidation price + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.08 # Stop doesn't move stop higher trade._set_stop_loss(0.1, (1.0 / 9.0)) assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.07 + assert trade.stop_loss == 0.1 assert trade.initial_stop_loss == 0.08 @@ -1609,9 +1611,10 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 + # Liquidation price is lower than stoploss - so liquidation would trigger first. trade.set_isolated_liq(0.63) trade.adjust_stop_loss(0.59, -0.1) - assert trade.stop_loss == 0.63 + assert trade.stop_loss == 0.649 assert trade.liquidation_price == 0.63 @@ -2011,8 +2014,8 @@ def test_stoploss_reinitialization_short(default_conf, fee): # Stoploss can't go above liquidation price trade_adj.set_isolated_liq(0.985) trade.adjust_stop_loss(0.9799, -0.05) - assert trade_adj.stop_loss == 0.985 - assert trade_adj.stop_loss == 0.985 + assert trade_adj.stop_loss == 0.989699 + assert trade_adj.liquidation_price == 0.985 def test_update_fee(fee): From ff4cc5d3165b53058c1b80f8f94bb16df2345269 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 07:14:25 +0200 Subject: [PATCH 016/132] Revamp liquidation test to actually make sense --- tests/test_persistence.py | 79 +++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5476e1d50..5dbd6b86b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -120,70 +120,83 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss is None assert trade.initial_stop_loss is None - trade._set_stop_loss(0.1, (1.0 / 9.0)) + trade.adjust_stop_loss(2.0, 0.2, True) assert trade.liquidation_price == 0.09 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 trade.set_isolated_liq(0.08) assert trade.liquidation_price == 0.08 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 trade.set_isolated_liq(0.11) - trade._set_stop_loss(0.1, 0) + trade.adjust_stop_loss(2.0, 0.2) assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 # lower stop doesn't move stoploss - trade._set_stop_loss(0.1, 0) + trade.adjust_stop_loss(1.8, 0.2) assert trade.liquidation_price == 0.11 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.1 + assert trade.stop_loss == 1.8 + assert trade.initial_stop_loss == 1.8 + + # higher stop does move stoploss + trade.adjust_stop_loss(2.1, 0.1) + assert trade.liquidation_price == 0.11 + assert pytest.approx(trade.stop_loss) == 1.994999 + assert trade.initial_stop_loss == 1.8 trade.stop_loss = None trade.liquidation_price = None trade.initial_stop_loss = None + trade.initial_stop_loss_pct = None - trade._set_stop_loss(0.07, 0) + trade.adjust_stop_loss(2.0, 0.1, True) assert trade.liquidation_price is None - assert trade.stop_loss == 0.07 - assert trade.initial_stop_loss == 0.07 + assert trade.stop_loss == 1.9 + assert trade.initial_stop_loss == 1.9 trade.is_short = True trade.recalc_open_trade_value() trade.stop_loss = None trade.initial_stop_loss = None + trade.initial_stop_loss_pct = None - trade.set_isolated_liq(0.09) - assert trade.liquidation_price == 0.09 + trade.set_isolated_liq(3.09) + assert trade.liquidation_price == 3.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None - trade._set_stop_loss(0.08, (1.0 / 9.0)) - assert trade.liquidation_price == 0.09 - assert trade.stop_loss == 0.08 - assert trade.initial_stop_loss == 0.08 + trade.adjust_stop_loss(2.0, 0.2) + assert trade.liquidation_price == 3.09 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 - trade.set_isolated_liq(0.1) - assert trade.liquidation_price == 0.1 - assert trade.stop_loss == 0.08 - assert trade.initial_stop_loss == 0.08 + trade.set_isolated_liq(3.1) + assert trade.liquidation_price == 3.1 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 - trade.set_isolated_liq(0.07) - trade._set_stop_loss(0.1, (1.0 / 8.0)) - assert trade.liquidation_price == 0.07 + trade.set_isolated_liq(3.8) + assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.08 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 # Stop doesn't move stop higher - trade._set_stop_loss(0.1, (1.0 / 9.0)) - assert trade.liquidation_price == 0.07 - assert trade.stop_loss == 0.1 - assert trade.initial_stop_loss == 0.08 + trade.adjust_stop_loss(2.0, 0.3) + assert trade.liquidation_price == 3.8 + assert trade.stop_loss == 2.2 + assert trade.initial_stop_loss == 2.2 + + # Stoploss does move lower + trade.adjust_stop_loss(1.8, 0.1) + assert trade.liquidation_price == 3.8 + assert pytest.approx(trade.stop_loss) == 1.89 + assert trade.initial_stop_loss == 2.2 @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ From 15752ce3c2d4b2131e9141c3aa496dda25c5d9d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Jul 2022 07:15:01 +0200 Subject: [PATCH 017/132] Rename set_stoploss method to be fully private --- freqtrade/persistence/trade_model.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 919750886..1b8bcc42f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -506,10 +506,9 @@ class LocalTrade(): return self.liquidation_price = liquidation_price - def _set_stop_loss(self, stop_loss: float, percent: float): + def __set_stop_loss(self, stop_loss: float, percent: float): """ - Method you should use to set self.stop_loss. - Assures stop_loss is not passed the liquidation price + Method used internally to set self.stop_loss. """ if not self.stop_loss: self.initial_stop_loss = stop_loss @@ -540,7 +539,7 @@ class LocalTrade(): # no stop loss assigned yet if self.initial_stop_loss_pct is None or refresh: - self._set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(new_loss, stoploss) self.initial_stop_loss = new_loss self.initial_stop_loss_pct = -1 * abs(stoploss) @@ -555,7 +554,7 @@ class LocalTrade(): # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): logger.debug(f"{self.pair} - Adjusting stoploss...") - self._set_stop_loss(new_loss, stoploss) + self.__set_stop_loss(new_loss, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From 4da96bc511c1a01ee717f304d60696141797d200 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 07:27:24 +0200 Subject: [PATCH 018/132] Update docs --- docs/strategy-callbacks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index f584bd1bb..59d221bfc 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -623,6 +623,7 @@ class AwesomeStrategy(IStrategy): !!! Warning `confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits. + `confirm_trade_exit()` will not be called for Liquidations - as liquidations are forced by the exchange, and therefore cannot be rejected. ## Adjust trade position From 845cecd38fe0acfed72d99eddccc5aea37234cfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 08:12:48 +0200 Subject: [PATCH 019/132] Add stoploss or liquidation property --- freqtrade/persistence/trade_model.py | 10 ++++++++++ freqtrade/strategy/interface.py | 7 ------- tests/test_persistence.py | 10 +++++++++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1b8bcc42f..244ca79cd 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -302,6 +302,16 @@ class LocalTrade(): # Futures properties funding_fees: Optional[float] = None + @property + def stoploss_or_liquidation(self) -> float: + if self.liquidation_price: + if self.is_short: + return min(self.stop_loss, self.liquidation_price) + else: + return max(self.stop_loss, self.liquidation_price) + + return self.stop_loss + @property def buy_tag(self) -> Optional[str]: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f50721583..824f31258 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -1063,13 +1063,6 @@ class IStrategy(ABC, HyperStrategyMixin): f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") - new_stoploss = ( - trade.stop_loss + trade.initial_stop_loss - if trade.is_short else - trade.stop_loss - trade.initial_stop_loss - ) - logger.debug(f"{trade.pair} - Trailing stop saved " - f"{new_stoploss:.6f}") return ExitCheckTuple(exit_type=exit_type) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5dbd6b86b..3eca035c9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -148,6 +148,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price == 0.11 assert pytest.approx(trade.stop_loss) == 1.994999 assert trade.initial_stop_loss == 1.8 + assert trade.stoploss_or_liquidation == trade.stop_loss trade.stop_loss = None trade.liquidation_price = None @@ -158,6 +159,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price is None assert trade.stop_loss == 1.9 assert trade.initial_stop_loss == 1.9 + assert trade.stoploss_or_liquidation == 1.9 trade.is_short = True trade.recalc_open_trade_value() @@ -174,11 +176,13 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.liquidation_price == 3.09 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 2.2 trade.set_isolated_liq(3.1) assert trade.liquidation_price == 3.1 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 2.2 trade.set_isolated_liq(3.8) assert trade.liquidation_price == 3.8 @@ -193,10 +197,14 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 # Stoploss does move lower + trade.set_isolated_liq(1.5) trade.adjust_stop_loss(1.8, 0.1) - assert trade.liquidation_price == 3.8 + assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 assert trade.initial_stop_loss == 2.2 + assert trade.stoploss_or_liquidation == 1.5 + + @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ From dba7a7257dec1371c1bea14c099dbca06474e3ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 09:18:15 +0200 Subject: [PATCH 020/132] Use stop_or_liquidation instead of stop_loss --- freqtrade/freqtradebot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d58c05d7f..7440212c3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1015,7 +1015,7 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( + self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( exit_type=ExitType.EMERGENCY_EXIT)) except ExchangeError: @@ -1114,7 +1114,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss) + stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): # we check if the update is necessary @@ -1132,7 +1132,7 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}") # Create new stoploss order - if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") @@ -1431,14 +1431,15 @@ class FreqtradeBot(LoggingMixin): ) exit_type = 'exit' exit_reason = exit_tag or exit_check.exit_reason - if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): + if exit_check.exit_type in ( + ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION): exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if (self.config['dry_run'] and exit_type == 'stoploss' and self.strategy.order_types['stoploss_on_exchange']): - limit = trade.stop_loss + limit = trade.stoploss_or_liquidation # set custom_exit_price if available proposed_limit_rate = limit From d046f0cc5e766ec24f5c88fd967b44ea88698481 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 09:19:48 +0200 Subject: [PATCH 021/132] Improve method wording for liquidation price setter --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/trade_model.py | 2 +- tests/test_persistence.py | 22 ++++++++++------------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7440212c3..3490b58d6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1085,7 +1085,7 @@ class FreqtradeBot(LoggingMixin): if (trade.is_open and stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled')): - if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): return False else: trade.stoploss_order_id = None @@ -1662,7 +1662,7 @@ class FreqtradeBot(LoggingMixin): trade = self.cancel_stoploss_on_exchange(trade) # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() - trade.set_isolated_liq(self.exchange.get_liquidation_price( + trade.set_liquidation_price(self.exchange.get_liquidation_price( leverage=trade.leverage, pair=trade.pair, amount=trade.amount, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6bbace185..2c6cfb0e9 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -814,7 +814,7 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - trade.set_isolated_liq(self.exchange.get_liquidation_price( + trade.set_liquidation_price(self.exchange.get_liquidation_price( pair=pair, open_rate=propose_rate, amount=amount, diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 244ca79cd..44e148a0c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -507,7 +507,7 @@ class LocalTrade(): self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate) - def set_isolated_liq(self, liquidation_price: Optional[float]): + def set_liquidation_price(self, liquidation_price: Optional[float]): """ Method you should use to set self.liquidation price. Assures stop_loss is not passed the liquidation price diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3eca035c9..0c1fc01a5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -99,7 +99,7 @@ def test_enter_exit_side(fee, is_short): @pytest.mark.usefixtures("init_persistence") -def test_set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_liquidation(fee): trade = Trade( id=2, pair='ADA/USDT', @@ -115,7 +115,7 @@ def test_set_stop_loss_isolated_liq(fee): leverage=2.0, trading_mode=margin ) - trade.set_isolated_liq(0.09) + trade.set_liquidation_price(0.09) assert trade.liquidation_price == 0.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None @@ -125,12 +125,12 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stop_loss == 1.8 assert trade.initial_stop_loss == 1.8 - trade.set_isolated_liq(0.08) + trade.set_liquidation_price(0.08) assert trade.liquidation_price == 0.08 assert trade.stop_loss == 1.8 assert trade.initial_stop_loss == 1.8 - trade.set_isolated_liq(0.11) + trade.set_liquidation_price(0.11) trade.adjust_stop_loss(2.0, 0.2) assert trade.liquidation_price == 0.11 # Stoploss does not change from liquidation price @@ -167,7 +167,7 @@ def test_set_stop_loss_isolated_liq(fee): trade.initial_stop_loss = None trade.initial_stop_loss_pct = None - trade.set_isolated_liq(3.09) + trade.set_liquidation_price(3.09) assert trade.liquidation_price == 3.09 assert trade.stop_loss is None assert trade.initial_stop_loss is None @@ -178,13 +178,13 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 2.2 - trade.set_isolated_liq(3.1) + trade.set_liquidation_price(3.1) assert trade.liquidation_price == 3.1 assert trade.stop_loss == 2.2 assert trade.initial_stop_loss == 2.2 assert trade.stoploss_or_liquidation == 2.2 - trade.set_isolated_liq(3.8) + trade.set_liquidation_price(3.8) assert trade.liquidation_price == 3.8 # Stoploss does not change from liquidation price assert trade.stop_loss == 2.2 @@ -197,7 +197,7 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 2.2 # Stoploss does move lower - trade.set_isolated_liq(1.5) + trade.set_liquidation_price(1.5) trade.adjust_stop_loss(1.8, 0.1) assert trade.liquidation_price == 1.5 assert pytest.approx(trade.stop_loss) == 1.89 @@ -205,8 +205,6 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.stoploss_or_liquidation == 1.5 - - @pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin), ("binance", True, 3, 10, 0.0005, 0.000625, margin), @@ -1633,7 +1631,7 @@ def test_adjust_stop_loss_short(fee): assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1 # Liquidation price is lower than stoploss - so liquidation would trigger first. - trade.set_isolated_liq(0.63) + trade.set_liquidation_price(0.63) trade.adjust_stop_loss(0.59, -0.1) assert trade.stop_loss == 0.649 assert trade.liquidation_price == 0.63 @@ -2033,7 +2031,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade_adj.initial_stop_loss == 1.01 assert trade_adj.initial_stop_loss_pct == -0.05 # Stoploss can't go above liquidation price - trade_adj.set_isolated_liq(0.985) + trade_adj.set_liquidation_price(0.985) trade.adjust_stop_loss(0.9799, -0.05) assert trade_adj.stop_loss == 0.989699 assert trade_adj.liquidation_price == 0.985 From dc82675f00e5ed1b006eeb1490a3f14147cde979 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 17:28:16 +0200 Subject: [PATCH 022/132] Add Test for liquidation in stop-loss-reached --- tests/strategy/test_interface.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f6996a7a2..4257b2cf9 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -408,28 +408,31 @@ def test_min_roi_reached3(default_conf, fee) -> None: @pytest.mark.parametrize( - 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ + 'profit,adjusted,expected,liq,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, # enable custom stoploss, expected after 1st call, expected after 2nd call - (0.2, 0.9, ExitType.NONE, False, False, 0.3, 0.9, ExitType.NONE, None), - (0.2, 0.9, ExitType.NONE, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), - (0.2, 1.14, ExitType.NONE, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, None), - (0.01, 0.96, ExitType.NONE, True, False, 0.05, 1, ExitType.NONE, None), - (0.05, 1, ExitType.NONE, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, None, False, False, 0.3, 0.9, ExitType.NONE, None), + (0.2, 0.9, ExitType.NONE, None, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None), + (0.2, 0.9, ExitType.NONE, 0.8, False, False, -0.2, 0.9, ExitType.LIQUIDATION, None), + (0.2, 1.14, ExitType.NONE, None, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, + None), + (0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None), + (0.05, 1, ExitType.NONE, None, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), # Default custom case - trails with 10% - (0.05, 0.95, ExitType.NONE, False, True, -0.02, 0.95, ExitType.NONE, None), - (0.05, 0.95, ExitType.NONE, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, None), - (0.05, 1, ExitType.NONE, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, + (0.05, 0.95, ExitType.NONE, None, False, True, -0.02, 0.95, ExitType.NONE, None), + (0.05, 0.95, ExitType.NONE, None, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, + None), + (0.05, 1, ExitType.NONE, None, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS, lambda **kwargs: -0.05), - (0.05, 1, ExitType.NONE, False, True, 0.09, 1.04, ExitType.NONE, + (0.05, 1, ExitType.NONE, None, False, True, 0.09, 1.04, ExitType.NONE, lambda **kwargs: -0.05), - (0.05, 0.95, ExitType.NONE, False, True, 0.09, 0.98, ExitType.NONE, + (0.05, 0.95, ExitType.NONE, None, False, True, 0.09, 0.98, ExitType.NONE, lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), # Error case - static stoploss in place - (0.05, 0.9, ExitType.NONE, False, True, 0.09, 0.9, ExitType.NONE, + (0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE, lambda **kwargs: None), ]) -def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, +def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom, profit2, adjusted2, expected2, custom_stop) -> None: strategy = StrategyResolver.load_strategy(default_conf) @@ -442,6 +445,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili fee_close=fee.return_value, exchange='binance', open_rate=1, + liquidation_price=liq, ) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate) strategy.trailing_stop = trailing From bad15f077c66710314b7dea75d0e5f7abee767a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Jul 2022 17:49:06 +0200 Subject: [PATCH 023/132] Simplify fetch_positions by using already existing method --- freqtrade/exchange/exchange.py | 33 ++++++++++++++++----------------- tests/exchange/test_exchange.py | 14 -------------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 79bc769e6..e180c90b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1332,11 +1332,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def fetch_positions(self) -> List[Dict]: + def fetch_positions(self, pair: str = None) -> List[Dict]: + """ + Fetch positions from the exchange. + If no pair is given, all positions are returned. + :param pair: Pair for the query + """ if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES: return [] try: - positions: List[Dict] = self._api.fetch_positions() + symbols = [] + if pair: + symbols.append(pair) + positions: List[Dict] = self._api.fetch_positions(symbols) self._log_exchange_response('fetch_positions', positions) return positions except ccxt.DDoSProtection as e: @@ -2539,7 +2547,6 @@ class Exchange: else: return 0.0 - @retrier def get_or_calculate_liquidation_price( self, pair: str, @@ -2573,20 +2580,12 @@ class Exchange: upnl_ex_1=upnl_ex_1 ) else: - try: - positions = self._api.fetch_positions([pair]) - if len(positions) > 0: - pos = positions[0] - isolated_liq = pos['liquidationPrice'] - else: - return None - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + positions = self.fetch_positions(pair) + if len(positions) > 0: + pos = positions[0] + isolated_liq = pos['liquidationPrice'] + else: + return None if isolated_liq: buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9252040ea..e968b12c2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4099,20 +4099,6 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf): ) assert liq_price == 17.540699999999998 - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "binance", - "get_or_calculate_liquidation_price", - "fetch_positions", - pair="XRP/USDT", - open_rate=0.0, - is_short=False, - position=0.0, - wallet_balance=0.0, - ) - @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), From c2eaa3d2cde4db76111d93f9a1281676838861e0 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Sat, 30 Jul 2022 18:51:00 +0200 Subject: [PATCH 024/132] add image of algorithmic overview to doc --- docs/assets/freqai_algo.png | Bin 0 -> 335507 bytes docs/freqai.md | 18 ++++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 docs/assets/freqai_algo.png diff --git a/docs/assets/freqai_algo.png b/docs/assets/freqai_algo.png new file mode 100644 index 0000000000000000000000000000000000000000..39fb898558b638c4703c67a406fbc2cead95728f GIT binary patch literal 335507 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

`jI=0OPvGE+XkyM4LCk);n#?DXb_ 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�c(Y`HV;wnHG(YD?dc> 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

ytnY64L#I7zshZtDH3t~T z#g8=1+xxiE%=Er{WIwl=9Zs80O+;;eydH;jCY@ie1rCo1zC%H`w=sA%#&pEVg&hFI zZW*m?gHh>S8lj`CCfr<5;j~Ur;*Lc}f3SU6yp^YGzhH^Qvl>wo4$oKeiZqd*&~k5TInq zOA~a|e}t7n@AB)GC3Dzr>zFe;F!IMwXsjKx;buTVqn__hrzf#4N|L$wbV}O1J|hXl zE23+;Z1)3>7)x6M3y+tggF}?_6WP{S4v^Hw2Cn#|KqA55TDkJHalAtJti>$e3`l6j znUzf5pm>29y>E0Jv9|Fpu(y<-aGTckbtI49v}8vdxmfUk5E>WiFH@_S(PmS#2?f%i z-r)!d1y`mghC(p1oZDsWx%s%&f4u+|;<3jttjV3^JVp!+f36*5ON&4iwF>i@WRA6K zW!pB6aN%jW?1Sk_jU=Y0n=bF1OPA023B~irChxs?lGcwDJ<0VxT>Q7N{xE9LrqN** zy!m*PIBD^3G^Il0Et=`yeFr&j!=A}KVsId%P;0N@@}R|D%|w<%I&&8w{7enH37==1 znIa*hh}9d86|7cv3DKr=E4y2Hx&wqvRX+n?F&fO?a2}b*Sq2y{JU2wF*^Be zbs;+~jwt7RHUhXDR01}_qWj*OP&xFPk8=32io`P^by%S0Wua<#0~BTkdemv6uVTyRV%|7u~8oA4EMUAp%Tw87pDEpBcW+{4Sxz#t{4k zEo}udwF#<_L};HKR;$N2GyNRFq3fMg9C^fS*?-1{Kd%!c#A;7;6vuYr#d?ITOU=-! zoFmKC{k^!vrm)%i(=1dehQi5;F@J>fUtde>FZY-s{C_=KICB>2i?r;|t>Dao44 z{1x~;T@B&ioA9_&Ou5s3NR4$yI zn{LG5Vfpyu{Xr&sl$&2YK5~NgC+2{FVk|BK&|DAy9?IHPlYUFE{!iZc7Jowl2A18$ z4e0ViAt-hH8c|Vd=8y;& z8=OSYT5~?7Syw%gtNaWc~VPCp~A|>jx#Qg#WCFI0Pg*hEN1pU>N@S>hhM4Ywn8A zvTofKH8necd4WVxKnH*tV8pN|T@;DT7oC1t#zaM65>TV>@7{3^hgmF7=m}1jXWcbt zdWpkLZ-1umw&oHyAJ;e@ROvroOU|%&8lh*rKUv6BmPDbS6BHDOr<0ta++Ytw@J5)s z&0;-A){y-&wD>>Tw|(@sxT5DzEibih_(b}JpAUaCSyo~Cdg{{`wlDd~eM(Cl_1XCe zrtI!2h_o`1C!9AprVw%tFP!-u+M(6#jxh~ndgDV5OCBh~hX|OWKtbrTNS+N?Y zCg0s$g41et#k)Lh3ibT%^?b!pQ(=0ITsjBdG+Ar-Zn8BVT}}0FciKn$?y_`~tBfIc z)4{5XXnyWoo18&sTF2qt;~@ps`QdTUy2+81Csusf+Sp8zJF>Q#(Ej=H8a7A0CEQQ0 zJ?5yEFuGATQYK|BH)m+GcG>(xfozo@Pyg&>EujS8;vrdd6~(5)o@vXx4l5yI`(oWrAgHi(1_73 zG+3*mx<`T~t!_;UU9m2Dnz%yQJ)3H|GebSNlImIE|81{|D3+}991gXDB?c+l-Ya0J z??V0-2ic}FWybqM@tqbt8@HWn;M~5rDj`v`4aRZ;4!;@#?Jb$8Z=R*D7TMCeI(N2q zV*h$!z{Wtb7ABURH!;}+M*{FIUN86h=nzlirIhyep_VmJqP<3&n2l!9i50~YP2!X| z2S@}+auq%1375oX4)z%BHnP-lL66hPbT+T|6yIGYf^bC7pTAz(A{&-%eK+oPX+>F) zHR-2=Q-4{D9E?sAm9}2!DGhE`vMo2S8}FP{TsIJc_OA!80Y2Gh=wg;~KRBLV9+9>V zye7C`$At&M^N%t2=cn~l?3Gtd0AP2%5$qnS-;joTP6LM6Uz`vZ#(2M9KO3r%a6=$Y z9aGQlhp%tsJFBg&jF09X7Z>Ju8dAHQX{|&MN)y94$cGr7g0-jwk%;3VYp<`RNUbFB{uA%xj;vNTN1g$Ok-h+LBu8U(w@Ularwg-k>brvp z##8NE4x8JKtb__1;rS+9@4OE5DJ1?fp}^3^gsOwo6=iq(U9;!aSd?$qi}#KJrgvk^ zKsuKi8&X|~3RgW1v$J42W-=t;Z&A|sr5oYT3Xa?ch?)2Cg zg7Lz_&<5)(VqmwBUHj-HTVdGbjKQ+~I%4L9;cxgRJ0G% zD|^GlxL9vK!t?R(oEm!_5bN;(fiFars1?=ve0BxeDaq&HYIM#n0ZhJKRxo2 ziKf9lM})m0ttWy=$l?rxBi6rgawjLaq$tuU$Z=L_=jVwT?* ztj2*uN9L75RV^aXFLiOZJvOyj<@aDoe+eYk)t8U_K!|zl{X_P3^NxkE`-9TXrva}V zO|Qr`>!-qC3_Y#uz-;)N&&g+<$Cc@2$3ZuuuWO%zErByyVRNkU$d~{nQ;PkSAHn{a zxWLGO2CTace?thrfzMZ7$z99h1Z7d~hy9bf5f7R@{Kr&$1DOv`+J@u~u&Y6(y(Qv3 zm_AS9%|2nTafg5Ksdr<6we^4rT zc4%Mu;3GlpMCLB5B?)#%VW6Ye0XXZ5%1qlq+a}F_Fh`)u26Q*$xX@InX-A9B3ISt= zz_Rp|I_GSR)4NVIcyG4IV-EJ`C}wk$-KL1h^9(GCeNHqs|Bi9o-`^Q5y`>|JKf=Qy z9h5>JGXH)+YCGAhUA=quIc6J=y%d-(s^1j_!@JB}Fo!MaiSGG1^Mv1=TW7vxNiIsI z0jXhf@OV*kwe~K&9Z2rM#l=*igmpcxAvWuv)k*ZM&g%O##(|-BeK~s67 zqByd6lS*0)`xp&sAI(6DyQ^Q1>OEQ$97SDR6fcY5!wYCZo^3*QZZPvfswyqtdyF_M zlZg63+$dZ66GWehMPLIbFTdzy_0E8Nc-pUwniogfwfod(Sx(3x-2NPtbiZOfr;#VP zTMW!pg^h7H&yR!??GO==J3fWr7Bw6==pP$P?jZQfQTy&h-(&kX;f#FB={_zPah9!) zCO$v@?Uy(J)z9QO>>A!zD@jmF7|2f&k@j?8W(k#}=wi$!M6@;@+S6G;no(9KM_AuCnBuvpH#L}o57e7`twjX`pvZm$=l3Zk1@btio>{u(}DG5`prlT|U zwBsk|;_}br4*o-vt5t7=J$*)TrNlrb3vEaiqZrF0g?<~hv=)y(KNz3Tj7!`+j5 zl8ke-S&GqIU^A_MH0S+%UATG!;zbb7oJ6GFu}(1ZAyM{Kxk^n=Zp^R@ zSktdLOJ)BkopBhkrl)bV>NWb{9U@*%lzJiFZX#*5I?3yKnT}K)P#&mz zi~HBzJx6vliXC-{MUU7k*$Idw5D}xDS7aoPM>9$dUg1}TeCdeT+mQHf{bs6wpV};) zi6A_;NvP622f|TsKi1dngQP>J1f~2dHDNvPUBDCJlQGqEp!A9Y8*vH3gUzwp*emmG z*L=mN<0cjle9GhlxnWKGt006*pn{~j2(_Xy7*9c(qMPD(MX9prmH3C{DcmW+Jlkm2 z5TKfF#Yx!M8)AIzLWZNd3=r{_y?f2B1ZGC-7Z^FiY3ru-|HQ$*nQi$|v8DXkop-48 zh8+2FgJ}BBV)gPG;dgx(Pgk^GdOeI}=O9x%SLI`g!9-k;lrS4(Ia3|zlKT>1MEtb7F-`NSk`U2d<8!_8lAw_|m0R3#a<++-y!cVsZ&GzmwILO) zEuv<_sURDj8f|lTlx&hvIa5cvhmV*&j zyMU8J$>>Z^dnww<-QB$FNvTkA363+1irUoZ%>u*|nti8=XI_oVqlP*2#qkrxt9f94 z3mSy139^?{=uk`jynD2=W zCOm#`Ynbgl0%9G9N4J~$doq7vM`&RJX){IX{N0dS8c!<&a45}jaQ1H3z53aidt-!b zM$GFU`S!d*v3+PR&aWjhWmfN2Jh#tu4jbBBV++dI%!Dno-(%8e2P;Cs5NRVs7ACzm z9}UcK={HeL#em-@45)Tik~03tTj^sYRL_MhHsw?W>za8-S>6S&`m8;0hAhHMt_t9^ zBOAzT^Wll0olssQT{azPU>WyXS;8SWgB9sYx@ne%_(~6*KNPmFbhgPEc1Jd?W+Ev*G?M8r?N1V#5oW7N}u23m_(9BYK&%rVwMG&BMA5bnKN_1 zx<00xM^+2-wyE2RE<2VD=-6;!KAeME%ay1_8Gjl-eb-pU*kNn}-{!6#w60~Xs{q{? z+d*UPIisaAynQ5@{LUMju~uBXM76~3!LOAtj-0`~d8fu18CsamN|8)=xZkA@A`~Z# zW8($nRfv_sNCe^mb?=L7&%R`HKfRBgXrq^n zVZAZB+Oih8v{dLP1iZmSBV0S~?FwK?Sud?JP4AS{??pwP4?m5_#p|fX44H~J={En1 zWM*AWx`N?IK!rThOAASjHBMfyo#;ms21(+P^T5Sx+*AF3xP{pVb%oUNA(2B%aPoA* zSInz=Y0)cd_3pqzy<}bR?XD_FD6jp_lzwK`upr->g3O8j37X^BVe$+1f`Z(Yv#>UI z?36+8AO#0$?{co5tt2Fdqi6*gtP5PD#V8ww+gXudn&8nFF>v;VH1I))m*6i9VcEyT zV?wqy{Tl`j9$XONpw`}kZRCJTj_8=pREoUf?lXmDv;{;~9SAzl?8dIWlf_=f3uvo#WoDfm- z^BQxaP*p{RSORscLvPi^I+lPBAllnc;OU0^H+JnneN&X{h&kE^mBb-c)46*fnaQS< ztH}PfWX1Y+->Yp(uQk*+XRN1CN5dBMZ;JxqP}|OA54Z5~Vte7%2Ej|7v!zLqG?CIv+7}xuHIm@6>kc{CHU5 z@``~?Q_f12ZQ!E`1Cq_q_zPa1egSnhpR7q5{r7sM&E+R+B8!o9R@oyg)YFq`mZPLH zRPK~uQGbG*7)8m8w*YpsVOm#we0 zF$6KQ)4&bO6yKc0%IwMOZ1~Dy#so?RBRrB|9FFY67#m72Okwd-{OsmS|Avq_H7)`O z2Ssq!sl7qLqE%9~zM2Bemdr1W)*K>R(1j`McJ1@DnQKbTSdG@-)ConSoL*B@8^+jW z)pGZf7h5FCq^k140A%?!0QKiBZ0zi)1BF+A@C3>lWY(QT)P zaw0UedyS=ShR#KT%vvcbk>9hCqAB6_$r~91Y{_#p!HifF|+dS2TnjmUlL3+AZ>{` zs9<<4G;qenPyIxLJ?BjP4m|daJ?c}JBWD~boy6RMcm*Vs)isBL-`Sy=8(sjF4OqnX zfCeVM@-eFBCR^DeR|%M%zA-A;W&X@^(L6*tnMtteX!Bco+E>92N4Px_wdMb`ySsvi zNBDEbQ;Kp-E(y(Eis)Tv3}2KxsTSSf14IGw}EUkUuH3f;>Y+-x27Lv2(Q1SVtw&3 zM|1LduVG^y<2`8mG2To;kFu|(o}sqkC=jdbzYs^z4n13J)NU#Sl*jE2C#u&o_f{hj zoTf~iYSUI)AB5d7UYWk729HDBQ-j}unK@|rwy${sx9L^V z`&U5-pfZO{|HF#hR*PnFA$u_J8z8KhRyM5>Ah#WuWx~2WsQMb1q@?7lmcuwZnHV}l zpcN*oJepP}N8glcblS2=goyDr`LZbe6I*k z63->$u2~m2bNe+Qq!{QMc2Cz9?hR%FvT$goyW3`d;aFNCbjgebVU7kDI zWgMGvjS{O-c{a(AElu1df? zEzUx*LKDB3;U6et+wF|{E9*qwsQ$W-@b$;XhS6J2TL1DOI)(N@3a(Acyt}%91R({GDBwbWFc=>Hr1!6(OM4K%ple1;K zu2OJ}^qQl$_mP2Zv)~I2txSkYxqb@qGI-88EYsyt?9?0y2L1A)hTL_FI7P$75r($k zg3f=olArgu!f|rQXRiMv9CF7wZ}0rBzA-TveII3h)4r$SnQ$dGTXpBWClkAk7Xdjv<2D~pWO5aX1S6m!;spB-f6-h?zm3rFa6m*5t%P4L z%aWPB0uKv^KKsI3$!-y9By0^UfHnl$k{z9#U}4PLQ;o96$0c1aH^%^IG|-MA3mtWR zOH%U}5?DaJZHW!3niM=W6KkeJRAOXPwwW55wtmwnTkRm%fT;I&5K4#8yi~EYxlWOh z9lIrxmq{}gGGv^~GKYASq-MF_2B<6sc@@Cc7&Y66rr0by>Op&6RcKk?b zTGrl~CCAl0jyXZrOb$A4HgX?te;j3Q*J2^XV8aV~+MJZ%Cn%;|@~`gSU`6BhBg~r5X&YGuT{CeJUf zk#eIr&~Iz6#CWVC37d~4Cn%}%u1kbJ2C#M()wmR7D*P9zM`W{7wE#>65!D(*AL2z2 zy^>|f6Pkg^7jl}GJkx{FA^~>TVJ6uwc$>hHJ&GWa~7pY8v zR1^pybGd{f2m;Iy{!#a*WSif0~I%obn6>YD?*ei@71ynrPghr;SQ_U^unM{Mo2NNpSsZPZR-@D94p;wGA|#k^SvFVxkxSd zmli#Kb7h>)mTV316OU|L$79A#4Zky8%B(16C`2Fh_05q%CXfhh9@|o_cTDVxXbbNX z%B=La(ni;%qmYr@ObBrvi%i(@iX!gsglgx0UupL6n@iz*8BfzhN#i_nH5#cO|EJSx z_~yMl3EQ(CG$xY5N+et__q}f!IKmv0a>|3%<^&f>jzAD73ubWuiq9szm!%gKr(^<( zVsu`7(!87}bhgmMD0(^>zjWY(h9uOmJ_Ff6en%v5k)z3XJ}9Xq#JODUGYdn%Is0p@ zO!XTxHF|E~iX(|32g}NE6<#m9-~Biik$&s~Z7MQVJK@)r@{3$NU1Uf4>1E|}e62z~b(C#0_m7H#kNL{=4>{aG z%VH0t6Hzz|1Oq+a{x^a!pSYFe^X|Nuau(QHxIcYR@__t0F*dN3XEv6EHys0+zIpql z+uSs;vsYarEmb_|?~6+ST;-IhT`)Y5GK2^f_7#T=FVmItz`pSR_Z@ishY{y2mmfwMqf*)95Awv z%q#zr&l3Ub&y7;-5=QSl>Sf?zvl;TwZ}L3Y8juxS&M19y{I3 z<9rx=B68jgUzGP}$2+wcf(RlQFc1PuIRf~$+;U%z!g$tm2AOR9b94saQPxhz04e$eD( z5TW!Pw6qD(oPqYo07M) zVTZ}%l}tL=G1|4}%D%ntiQ%E$TmKTz2oIJpKoA#D<$!K=e>X+F#Vgq*9G~1RRSJJI z>Tg2uor^wS4$%dbbgHSH(TMMMegeJ3o;^y?Ma$x2b6z@0yPs2gAg=)ZSGn5TvFRTf zN`H8oY>0(S>Kz$=Ug*4e&HXwv2K_rI+sk9`Nnp*HIUcR;C7Qo9w6FIN`e5mD@F1dO@c)Y-` zC1H89b{+y|L^ktY7m_PWe)Do({y0->n_JXx2vp}%q7oWG3xRTg^t&^ps>WfpQ=pPm z^M&DQ=?>%Foyy}Yx%V?9@HTyy>$>O+9}U@fpRZ&rso7hm;}kpgThJ5k2P*;~WUI`F zU2<(Nd$kuEn>Rx(QSM$;Gj`8IzPXyke+F&pv__svToYWs>hj{8__p@&MO zV57FrAy@~FSvJPJzue1HYG9NyKVZ;`#EXZpCHiwfMaItF^teoGwcdqIwX*mC zKbU5_>gzCsl7s*qujN(?Okq^EVKQld!X8a9z{8Iy(_WGMpmjA z&k%CnUE#XsKu$xKx(S?(M~YGW)HxlG|Cp*s-gGYZQ5yS&oWa0kN`jsT$-B*$Dahxu zH}`Mn#N?i_;uI&`$8Rd;(O^ynT|^>H)fuQ#dy$BEdjJhF7#0oSJ-6&&0lO$LvZob* zm;M7IUS3|>a39jdo0HPP1kMJINu@ebvV=y>&wHjL_D~cf-^Z?8ySWf^_onV(ZTa+# zs8%XhrEz5MZV12h&~2;%3j{@{ep)1lJGQJ_9AESI45 zE>LgeV@M_5Y~I-Z2|HF=Zp%AAQI2< z4U7HY?Um{|*PlC$ZpBMlqD}M?uiu-4d|;L(g{_G^pGJC`G9BtOlK!+ND_&yz;V)Ea zpxnY3!~2eGpiA)R!%B@I7mGg@$)Pji=XPN>+>ngEV#)~7*1=3go?v$sP}xEx*Gx^j@&-%o;eFU~~)+3YVQG`qVFsI6priJ+UBZCH%HqO@_4O+W8UjKj$# zIB(k}g(=ecgiSru;}bA{r>G*MV4ND!?C^G>Tokx?jDNG`I3Lf2JF~KI-=%{QmA$48 z3KBGs>GQ$5B|`R!B>~JHa)Xymi-qK(5FoeZ# z%Hk7jeVL{DTA)M&Y`%4zU%-X}+hh$EU09yqkL)-b{Aj>`M%j5l??$lOOxnEdE%@CY z9M-zO^re2~`wC>1Alk~dlL%=+xNKH%%V>7IV`nE)?gbMAqS&6gjy)nEa?un%Iz|$; zWBbMjR1tI`&lY&Px!P6Vq)?zIV)aEzTBPiF7jx*Y7A48o25X0U01l1fm&p)_GcXUL zechUyR;Z>T=`Pp(m&#dQEfNic29d}%-E4ZU{CqLUOW8eR z~VO=m=mneh{Yemld@S$+#RPRZ~nbQ!m0h z5sotq0WznF)24d@Qx2$&V>cCvQweqYWQO!THNMCubb*SFro=9{Y1_}-2;alKsu@Z~ zEYu=6eTCzos7LH@&C^~qwR{m$GO~JLVA1%*#LfM^F)D+`BUP)DMaFTg zM=TM=koj2;L1Gs0io?ao^7INWpG5*+}S{~hjYLL7VUh}2Z}@U zO7o~>Q%S5$C> zAuS~+4ey-$eSd%WJlsp1bM|NNnKf(8o@L?WQjkLTYqW9xi8Bq7-^>9H>C2-hZSl-`gTWFWSVNE!Y3M6vhe2et#E%C9d){lYRM}b5*H5#2A z>)NI%SKTVM=*ka{PqvdYExGKgs0_lj49YbFS+nIrgUYXeYrdwga@rEK;9yHg&B5?> zJ5~wCcRYzYl9dr1dY?l#%7EEnDurH}yUP1>F8MA$Moa%QRShmX^XA^ee){QO5f9o~ zja9ZNc6f+NbLpFy!a7NMEBcb^#w8O?SJ;W6J3I?yi9JrjpzU!=V;vLXpJZjL5%cAb zb)%e}us144rQf>jg<->DQRaCvcWsVMXgJ%?i8XlnT z?G~Ym49At#ZCi`u=;;Y>lFlTphSdprvWR1(cn+z13wfU|H4)qsz5{XToMBB=iH+Ip zOnIX{2Cbn;U$#(g`DbByRlWC5Np$S^4<9pzUpTvLi|5s*3Y%VV4&=*ImZ9MY%Acs3 zmeM}k@H4o1_oH|~sJK$2TF4IVzKOR~UTqxa2DQ-Sn#0OG`a9gPVr?BA3YdvSh_T9DUC&E2*}>=yMx#|*jL{+7*xcRuVY=yiikcvOM`aAL20BY;qv`-fg-~qq zmuIhkzM_8F^lqhy?ZRj0eR|hcir@17w)pIgP=Eow@2=kNI^JII5OmWGwHtFR_{8-7 zu69WurKr>ff>_Z~9Wr*K zH500&SmaA+zUbe|16m{>Ps@wX4S(1xl#(e^mt)8Ki*D|+;6A(Y&W5XNsL8{l%i{n9?e-Zk^S;JC*(<-biCB1p zZt_gK`3BfJTVJz02};60_o}nAWTfDK6`Z8pR_K_Q)U&H^WT0h#`xZvFX6CINo%+{U z1*<2wC)}O~^*U7zS9ovXaCo2VABW=adOocA;APjEbn>HhBy8upQ!x7L?&dO_Np88~ zai4Bz8IqpL*dd*eG_ZtYjGeD7d6S+-E0O(bx{PslaPntS$x>{8}EYi15fFVr*Y+w!FQz zxA%U`!upuARu^Hpb9Q$2FH7QH^~}7bmww$j*E(3QT8ubgiideROx(V1^%d3M_ez;O ziFP`+M_n!&?Vbn%`sF=!s6B<_xRt}W!E|@1{n_W2m5lOSL~_vqvbVSdYP()C(-c!K zDyT4RmlGh>s(VdNGR3cMYCImqsM}ztz)+mr`~5xb z;V9}eMVONq#Jf~3F9HG_->@34`wV@#y#Kh5=h$I)h&VfXypu4Ss@!Rq)s2lZ3Spuh z_u<19%ry<_g;)%Ryl?Dpz+M?l=Cxvu0_6lzkzc_Y{F-isuWUcFLvyw{PFRr`|RdW3a|akJBvG>-$*3 zyEyL{Shi?8c%OP}=>>!_%#MU-pB^6Aw}^h$IJma_Ruafq(8eLJaE#(}cf<>wBCDLt z#GE}ZT5f1*0zc}WiR{+h7iPB%g^y1xGBpG=bzZw+HB%B$Xe^b6p)DB6y&Z3ONoY3c z^*%aDNYX_EZO%`pdolfPWwPS04YPysqk7}Y)WX8D8oT}9D?Q1F^@8?Y;$E98KSQ>R z^ABh8O*vA(#L#38H|nYa-VP#tVBFF}E6cVhci}IjM!j$D8?00Jxc$PjMs=quQzI_5 z-8M)&^M+n>oen?oKGP`kW+3w!agh?xfbXQnxdw&tuh0@T*=iFH9R8{LvP?%R)^T1|Mh(#d8n*gng|;Yfk44J_~2Q`eb!;`Wub9~u`$t`G(ya*Gxrn7&_q<1; z(fE7&_-a9tZDRa8=dwpCt(@W8Z1kiN6gqXUxD$Wi@FOKFGNI#A{A|y;BhaxTn9*fc zd$^Z+LiHaSFq4f7#^x~=DqHKT2s@lSj{$bIb1-^ZuLiBy+H%1mX47_4j7);ex+-Cp zQIK;^fsC7!B1PY{iE~Eb)S_i`pVUjthZaii=Gg0pqIWKXVa@uT$d$u{eU;k8C<;eC z5ayY7U#}8DmIGP0Z(yMM2H}RBpWovAiyw_}}bO z>Yy>r;j%cCZ{MCbJ-?lWXYsc+WZm09+3TaI+|Bbi1}U%izbYR_988=>#L1;k!(?d( zM@Kqt?mI08KYzJ`&KVpWOh`?=7?9I+2?EC`YoUg!&BYq zcV&e{h^YmR_v!ccutvJt?iSFL5{W3Gvp;_DT%0+;L|OFf^gEXnq?YG)1m8ZZ%W)*k zwH4z9A#+tUA@vU}<%Qj{(bMgV99-9XM?ge7l54^*M2a?b(PzPcr9(G*LOCSAbG&G_$Z8bOb~ za!4q7Fd8pTzpc!7VfBLr38Z0BC&E1nE%*e=ZoICMC0EICAmv=+tLun#?WD(_X@+r} z*%;-Q)VuSj%CoKRUg{w6W;Hzt=J9*rEjGwy-T%Rz@3!L`nr8&jC&4@3Vrn0;#?i58 zJ2ZMkAE!+R7{}{f98b&H39P9ydkB^Uk z>+g3wj(PY7N)~51^C$PVt7lDA!`YIL7tOr37=eTnBh;0C(5_#YlhpYZo@6~cyCgf$6+lzCdgx`6?UOS4i>+s!@>5Ys>0eZ>ST`cl%FlTH@TA_DR$q927A9Z%L4d_`*n?W7Sh~}#^NM}d z=lgo9im?!yX;E+_sM`#8rhc`GZxE4@ zNy9j4EG#U9$Aoe3`=_2VNj%WJ2+@Xtr!lpn$J*4-iuOqK=VE0L5iCbAdpbu>Tvp~? zp`SRPgtNwRGZ{L`SXE4goAe_FhKA{BY3YeA{1>VF_q?#PyT*Ax{Nt)9z+~f)t9bR6 z?{luasB097rn!7`I+CdwrW^ENE$NDm%Dny3CLVE)M;%t6HNklA7@fH%d|ZyKTVf}m zc((icMpqVYfJ_C@TV<4oJt9!_i23VRVy=8i{U4CYo`gfBopdd9_aEv)S_f-&;*rZ9 zU&*kcV#wt5FJCL!A^T|BhR^A&mGO=hD>PhORUGujopc`y>mWiDcbOEoGrf$Bl`WE^ z%VWdip$u*4%}#JuBv1+rE_$A5(mWFIC+IT+&j%l72fu?BcdO>-ZDOAb%ttc772{o@ zS^>h^PMTn&ipf&|kWZb+a-%3+b)t-4~R4W#Y z5iDrvskXs(x1`@+dBlGVc|qUE0T=fVs%-0bYBF(iE}z9G96lG8MQj6ig_!0{LiL}_ z+{*d#r%;8n!4S zHjHX0V=J>i;GSCfY^~o>5;sMbD!y%3$_Gu9yy}A1#5+22nVOoytR8@1Sa^7O;8W!8Nmz5MTzPcQxMrer6K8{2(i1)$1X%m}?3#Ene z5--L8OYo0Ta8huhJdRLWp?}w(S)v!dOZhD|I0JHbO9oZAgb+=ELA`Vd{z2GhY3TSX z$eI>2E_}mltBetMeFi6(W0oGq-V^#%9A=Qm+{4&E)@i{*7KAf0=$mTidlsr{%$iY_ z0lmu*){6@nNPwI7;Jw5$P<$!=+LUO(Ned`~7fk1q!`~dQoCJdUQ zpw*OKU(0VuJS^aQ$q$AI;Nmpd^tGZQ252mh{X4*T5kcFqX>2^;`tD)aI9gb!K@?YX z{Rq!I9bFdrJ{iiCB!-iZ^W)pTwj2hv(#LLyXcvsb1S1%!aYs%<4Kn(lUxuq|r=l{J z?x0M6d%@`KEH{ESgDx$XHR@ccn4Iu)pO*rkRO}sA+GK5B!ozioDo3Fll2)a#kq_C; zyh{hp++8!#><{%qaB}!yF5EA#vF&X)zA1~END9BEASp)v{&eE?isiNYaHJrx#07_2 zY#3bD?o4zw9OsVf4iz=?a132XVi#8$w6!zM$&|~k<#OIa`e6NOS~XCXmj{y{X*#Cz zo918@nm;i~frrSJ-&2!$!*Z-z#X5(Y75+zQ9`8>3)yTi2ucI%v3yU2cj5Zkl^2i&d zpyFQct}Os%>qqHF;Zc=>s;W3J7s9_M&;ow)efq0T_do21`@0W{N3WbQ$i>jw=4@P@YG9q5|=q4GlzFQyb zko-e{TL9(zb}7=Nh4b?sT-T+~{{@K^7oOuKxHwGld-1+b-5Fsn&l+0%N zZ;cx)OIs)|8M(6xwkFHPotJW_3_J3z!&j;LYl0raK3J?8g$>2}jqLXL6W%+>_%z7) z;%fi)7UZ?H$-$rHlUBI_9$dQ{9v*%!Daq-yu7Hk9j_@0)oO!q`zJ|^8UkuZfRab{A zN-_kR3!?aZL8M5fy4`~H1gkms3Z^qPmzG}qhvg?F7-gN^Lc4`u#gHYNoQm9CR1S44 zu2L-4s_0wF`9SfjSWk|cJp}m(jXEb0k6#yGl4t4Diyl|bq*SSogVN?kHlakATRT`I zQWEhI4bCL9!;u~%pNbz;G8C#uk_&2Z6SY2*!-NG6hh2Nx^=aumwQJQt9z zXc#cYb-nK|%X{e4E_I!L&UKU7a`E1-YW6<=wMX3E!nmrY{7c@qzgcBwByPjh+2@H_ zm(M9y#PzfuogmYF!VJXKdEH2g;YBJIqobK^TKDWELpGIZWIsAaMn+)EC=Gw9gu&s4iLRl;i*LM1_(M}^o8+4C~ws$@g9OcOJ`k5P7`=N{h>F}&FmsY(`YBVu8E zx3q0k@HkgnAW5gL`@l+=O8H5yCbn~c=demD0a6vNj)1na^dSzHPMM$j( zlI-`-FHo473y3o5Wr@Jej+uV-Fzq{yl>laRo|4$)3}>x!_61q%XaKY5&yMv8KJrDT z3L-BiA%%x}4Q*=!NfcZrG8%3$|9Ux__EzmZhDBRc!&}mEuarO0B5~%mK3BwOeEy`~ zlp?i3{2p^dEQ+)m&GcQWJ9#*Q^P25zgMx(#&Fzx1?}}9ixEMA%Pxy>3(yV9|BxTQ23@U!UrlO|fQMMSIZJ2 z#lpg-15hv!K8QfB=N1%H!*${a-#3a*TDwSgxqUqh&U@M0TDO2QpGly+ce+>MYsxw#8(UM~bh#t5 zIa(>~!sp@GZ6NRNEED%U2ptdFdLA7<1(tMk)8g{#3P;!s3|zaqy5gnD?(-vrsu24c zjX#|O`r1Cfu)Q93Yr41vV<2L-<$MDt_|!<}jHw{jX|f2djX?DB*+jnL*}mV+%awnw z`-_Tj%L3#ujJBbnD6skgza6vlBycA<|GNX8##!KO$D!9yA-=ck@OSXZ$N^Ydfmw-s zu{U(}fnkZP9c!IR6)WoUcs^eHPVtLh6Q=GyQS^`b4V&l?IeyZZ{E zAukBiUwT`jVOjjb_I;l<)_&+L5wIN>uF;vt(OLA4mwKs$<=TYe?F9L``{S94(xh@) zch!ji!LRbq?_p%W=E3n#oh+B`y-7p&5L`d&mqg1_^HOT1L`!^e*o-|`KKwh~aYF&8Mm z8+mV_E6g5{k*(vl&lp^O6B-8R+fIZB5i|#GeJ>;?W>)H&nMolt^p1r^t+rnlH}F_X zP^~RgXG1Ksk#?E+n>TOj4C5FC-1Y`reJ;pFeTANw4?i!S{Il1zpE&1(wY0Tm$+LNI zb@KBF+#k5Wvr%q;0vw&~U0fbG-+({w!O022H)(n4+Oj;`mmI`jcz&HFVC0w=na38Q$XUd4cl&NM^O7j z4{=EDvqw9(Dq^keHNKE|D1ieU_@GM{PZE)m_CVKtMK!w+iI-VEts*#%ZScG8rFHx2 zB+AOlW@lv~ZgVU<)EZfMWt>itip-D>U()UteGwEViDP(&Fo1MFFuJ}xQkIIQR)ktf zK9S=U3_8v0-*MwR8&+TZ6iy@?*EOCee+H)RX9t}GAPNYA`{bjEzo_z6)-Con@Y5-R zZnh-HmQwb?4u9bN1yf58B{i}~H+S{-~CqS@n5LiuNl;a=Z zV&q8ZKCAst5wT{dVl1pF7B1YnIxktZCI+%%Ov={vrK9oDOfMcj5=w(4SL0waL^7#y zr|P-!M$a^>b%c;~p?Yc+oB6=`<2J+)kt zg4uE>t!64-q2Q2mAkG~)y&iOv84|({M(|&Uhpa9i8LTCpzmxhEarqh%d%M=G^jPCW73NcSuN-onV z)i}-sJM$FyQ?Z3x+6`{$QgC~!Y~e{3*&tzH{Z1e(;JuTXXkxL0wI}H_60>B*I~Wy= zlM-5oF&QBHw@nd?hJGNV{M-Llg%GA5#N;aGR>i4PimENDGG~pwaZrDo)w!Ay77k-Pu2*e^;C8hO)Dc-AbZlGOGu!!wc2E%%3-ciNDC}PUoia=l@u38X*7NVSB=oS9 z{^Nvqs`!!D;p~rTT3Q+=KK?owVfXg*oM#HZ5etK>-^3&&l2%r1A|fI%9Au_uACRJ6 zlk1SxOUfv8aP&jRqcoXKOib*7GzVjXr3)sqRao4&@{-*D&6q#Pn1d@5f57gZZh))F zZJ&22>$7GlC4?~Uzc63ZCO0=XCN{Pmczd(4XefKYfAAZeYluUPPo8`z$nXpYqZ$;x zdcVZ?4`0?rB9!LTRPd6&OG`_O@G^#4GbSVBy;RPtKfn5pcjubTWh5;x!}R_ zR(w2AMSMWYRjPR6Xnn*_%S9`gZ3T>OfoniG)`G7b@P7e-K(@ih4vL~o??3M*X`VUA z5j%sirFa}%`dx>z(d+^-Efyc&44f9H=jNRLZfk%ET~8nyjzTZT%Q{>?KR-%ti)RH& z>EMHI0w>k`Vz(Z4+aI{B*_}6gge~AY_ zvkH?wBHeo1A=p3IK6raR*CT09JHbo-jc2lVWNYq}3ON>imL&t6N)r za6~=84;TJl^w#wE@9MDJAyH9yJ5%Kw;EeYg4jjU26pn88)Q)0renCOb)DGerkAI;) z3~j7#dmj;jlaY~OJzMh*N^|XbWgv9H_Pe*YHw^=gDm+zg$an7o z3^7N+?H4+DB;bdC`kdTLdSqzU7sq&cJfVy@hu2pZFPnd&X484hc68CWx8Ju;Ak08N zdf490(fONirlg}LQZoKVyI>e+3~B(( z#5?G~d2}%QrW|2O8W&;hFnPgpzGE)~oU!RmAsdg%BC z?-G^N+sq|GC_`*aSmR{acu25Q5Ydv7$BK}cxD!HNGMK_jgyPadPu}UNJM`lXWcpqA zb%fxgz>l+Qm6?E}IWDyjxj$gYD(IqI^x79z$`I=7z3*qO`**a`SQGb@iojetFfLL=NLs;pxQ1rBi@k5PH8=o#Z=in=pE!#D^N0B zY72-Y=S230Z9AGPOKfdzeR|k?ADExJU>{wvX$ODA6806C;x2*Twb?~gRTUzNB0N?- zj)wRY=XQVpK81Ju6lh9F6|3O-G(9sjmM2g4#d)g(3^T#WmWGKb^yd2HYyJ^9+O{|C zw+_IE*<5UCk~Bok{?x$w>UCCjkAzI*tAzHXq~+4{gWNC^%9W`;!dUvsHQrx4xQ6r( zP%zgS&Xf+2Wd_2|$dQbGCO3k2 z3Z#ig(diiKLCa&%=&-`*+~5;7DyZmHsw!XF!HNza%aJBa6&9;t&=kNFXBE;INg}|g zG3r;Aey224xV?v|Q3BqZ-;3RD+6%@}@3lR4$Bju(x0&VjId_I(J7-Hl1cUGI!CYG~ zjWt{D2+6mJV>tZvK^fTyCYWRM`@cf>0Sj6vQTX1NgF_~m4TnMl*hgf1sq5=&cygg& z0p7;3rUDz)!Nq0mTP%I^Vb5Kp68)ycYV(oQylu`he8|I`XjGAuyw)<{t_Uy0D_@9+^0k%14hLMcXcX zNue&t&3)|nONQ#+y)#Iu4)&+!9<~sNEf&7ivQ`)!gXj6J(D)ZW&QTbPj~whE-o-IK zmzI74j^xD~2=iY+_ryd*uj>{7?So;j8a6Y%p`k>^0N5ANUC?2YwCiB4A{&|b!;|dh zINc7<)~+!~WQ}=cSJlP5v21~TC%($ZcyY-D&pJHRPqHn6N`hwfxqvqXTI`J5`EAsk zD^s;LF``@lABg{<4Jg--lDqNN=xryH&eP8`P;-B5v4;HmJ6~gAtH#RYLswT<(w|>L zI^G>b(!`Ia-Fkr4l-3p@O0{$_H)2$L=c_%n7HskdNv?8toxcqXptQ}+seBiLvkQg} zMOx)bK@2jvlhIKq;t*yiu%Sm*1H)zyf}fM#=v96HG4s)yPWpI^}bIXcpTj9}<} z7-HGPF#x{9$;e0#A3n_Y?_KCca^9Pt`J}(P)KcpMxXPkd_ZTdU@WLTZ=S*xV%yCoV z0ZN6^{Mb{NEuo)-#pt)htC6p@J}q7+yfz<9Qy>R9Ui`D+wVr+kr)cOs9wz1zK->IY zu-kn8{JH(!{8P1?5`E%uKV|KmkDMgx;NV=Ep$4@plJD6X8B`25KN6#&q9kdCrmq() zvrx0Sp0XR&WM;2-oLe-+qaEce?e-8=dK~PM$V^dAJWR0N4LEeiVu@LPo|>(F+$L}^ zf%kTp=esvK8Ir`Cpw;w1BDy8S0Ya4F3=GKkkX7V(H-BBqHQ5S>(Hu7JajMgh*|V%Z zZv3K$WNT?VG1HRc{wV$la5TZHsxm}7JtB$`k9^l6?wOzc(s}VrszEdlOMroySxv<# zd0;MI@ot;Sl5X*&s)|Y*#1)9(GI7BzEw55O%jI=UO(mtJQK?;vt@4!ODZISqa?2jK;eDmV^x3(BK<+fk#y6`EZ9#{a)676;8sWQ#8XYis53R6U31fiYcsblV>*WmE*dU1Ix9Rou!AXC7f8YVaL=NY3r ze@T1EwLZ=_(RF|8>%*$hYc#?Ym=|=)sS)E8`;7UJH?&4d4y~lcRy*qPhLDAGOySi= z!+5LwzR$}Y(}tK^N!8ETgPI;h5(LuL;;}ynmd?~HuGPKiotc@b`BTgdsJRq92?Yg( z{rIGaV*sE}TxzPEt}Z2v>OwdnLeyE%YJ%bJaP~gHcL1)yZ)#2;iTs8zF6-^xymfYT zboAuqGE00W^xk}kixY?6hOJU#F%}4ygEbNC<7eamd@=VK&)Aqu71pWhq~Df%^nyp@{f`CEGz#n{W;GTVmuWRq z#TFnWAusLi*%mPy>760WkYV_k-_NNUUL-pOSrc6MdHG_V$n&-J^;DZA(;4n>`g(>X z57amY+Aab3B?Ch03?ncHV5MHHiRX{75FjDZcqoRE1ETaHICvZZW~p#Ld_D0GT-y_{D*ogRCQ8}`o5CSZb|KgrUQfd6Bbw~Vdf;^|YD?K==e;Sk=Lv4he9nwVwf<*R4S zSJ_C7vp#>Wi=4yRDWFkjl17U>MiE3SX-1c|QlRh~xL7Dmz?<@Fxh14UY~4L~Q+fM; z9<|UZz%^%UZZ7TY?3`Wvt>h_~jF;-xR>`1jE-!=o!6#Pz*ODrQ>NPfVKaMGG837hP zx#z6i2+3zK6C_cHkgdU~J&JY7e*X}flJ&m&woLm{V%(JO4t)D&)b;i@%f9_D{Bw5E^TnP zheDJ`e*Xya`go0{nyRX5gZEk0q!ZaA18dzVm3f}hyx*MpcwfA-u5Dpx2gVZA-*pZ3 z;SevHfXwp4FgiV0CcW5GDhJ%n`EPel%UU!-H-r<~3O?oJ=v0|tK_$Xz6CEDTRZ1r# zCr8J|h7HT3TVwIc^%}7K;GPZOgM6R8y-2KuItGeZzkltvhB%ogH9vh3jO)rHM z`g7u>OqV56w{#t_M2+nNB@*CibbKm{7iynBO%|$!tq*-n@@hgs$7MO;z0dvr{rh~4 zyyz-;Ae>-b&bBK7ctha>6I(y{h9?VqxrP`>5Q4esU?GLRkbt^{tq+>YPtLa)8N$!4 z7{xw6TPcudF|1N(mC~PV`OJ+iyHbF)@Pds&>e3!5z176p}3m;-FoclQ;GIAsy)VQ4(;0rAL>y_o(tFdw^qUA*HrO*=;V>_sfDfN z*yCek#dLL3b!_*Vn}yU~Y&TI%NK#D5Q}{q>8XOw>r=Pw20RAN}3;P)f# zmmT=Ij*~Y;~m|-y&q@8VN(bb=$8IaM+F!@H~+<@ zc=XQ8KoyGPHbd4J>lC`L^Rh%~1O5Yz-S^prW+S(PK7IOxjkmeDGiB(v1^}=8n{C4M zg5S-+z=B^jwP|#xk8bfRRT8!pAJ2M>H4!v1mein*cd@OHUQA^xHP#hB>qt(XklpHN z(4{Z5-BCN14WyY*lD<)kS7CLg#7pgZ{=Kg+jZG$3_KNg-;Xn89Pwky57@ZxJ1+GsB zL@uxHIAz+soYW_xCb~G;-|g|95@K!P%JsT;|V?HmYH`1rnlIg4zlGoG8*Mg82mVakYGfSCCI2 z+`g4hdKjCTInv;;x(TZaciDToyF=0i-MFl$A4|PI-5s_3zg_^UKHzBJWaES4(R?&4 zh-eNj|9U<9JrmHvAuL<=m<7M{CgF(i2~|KqfcN#;M#Gmc$f1F~Abc2)WC;6b(;S81 zkR@{(qeE8}0@K4TMLyUrE*~$`7bJb4+ZBp;clwJ9`||Sgw<-ky*PB3)|MWh>1HK#a zk${{)&}vX(K80>YXf#@j7QN1bEZBT~zSGduRPBA{C~|Y@BrYML_?HA~%;)6>E!tn4 zSw}=~nk`3t;XNQ6$^33EesbH+C)fY{B27~Z%?46wGSy}r^ zWd7#Let`T|%6b5AcL@ID6qtIXWS%#qcz1b!0c%$PRq^S?1%lpz2{ccgH)9GhpSF!= zOFXq-LIPk8-+N-zh6Eg3FSK)7E_~sEhz->&JSkSMYGlU1=&&bC+n#0FIthuu2RSl6R3#O?X)UQQ* z;(7cq03#*59Vbxb44DdZ%p&ZMO{k&SHA{@Z@dTKl z3D_OHc8k`!6xphU6_u6CrS*$k=>?Nkv8E}Pm)=l}44Y^App#?kQ%+p^XRG`L!{q5o zV+@2R7EE~o;ua>nSjB$H2Kp7TX`y!gX!F&J^dpRXg(?%#oM6@qT<-Gk--9q1Q?WZa z3b<25;*IgM9#0ALH2)7zRFnHIRUHgEn{?etvRu zb91QE#cr;zhRR-*3{}95rHBLq_=x-h0tkQS;__SMs`O6<1wT@Q@Hzq1O9;G6$F+VE z=&d0abmfrCi!&Qa<$T?R2~s5ow&2%)vu~PDS0bR0QK`4pzr4IuxJZ0tIQ(aA0B0Aa zElVtjoZBMCk|ZcI^FAZ_ zir1F04ct>x_{bX-6GI5+&ICK_B{4B^(8rH28-GAkd^pVj7jSHk{#;$M$S5kB!hwUV zP*>DMhbfLF!{R>M!k&VrQ}WJGSyznmgv>o=Y=BD%+Z^5V#9aF?Sw#X{T^;m#0V8|d_u(ApNt4~!aucN4sIj;j}#ygKu4E$ z_QlR*HFa}SJ#R+~gX|~B*P2c!7^tWa7>zsuxLt|QHC{A$0TLpa3G-sTgN@(@pk{pV zZy}v)%K+BzM8B&F*8h8n&Qa?uVK9bdGvyzSCzTqQSe?>o(?Z|+lN?+LT@j@{-rfoy zu!5@q!ZEY57EVtcLK(FJ&(n;ZMqV6ZQA~_KfDO0^cQUu23K;WQp6c?>&YRl39}7?d zgUtFQ?>7hD>d5~fgep`kRIb=w=pLdG#Zkn?D?glG5{Gq(vXjb5hzArzu}c&Yte|Dm zHn6eZoFvi+y$7j?>+4s0Qm?sZimboz?1Z=M=G?3;FNi2R zitwhJC;eV7*BK%Db!9O^`Cpf7fBdIgiesj)`x6+%@<|MfP^S?by05RVH_&G!x(ZK$EM$~VtN`xTM5hPn$AbfV zi1u9<`(KsoZ5KF(XW;ZhVq<*pAPOD^9A5xRkhtu5%?6{^Nmi8K$UZH za>4`V1b&f8A=#fn78(H#A;c4tl1hOICfxggA-e}4nWUDA*Z!MFWJQp6;2Zv2$mFo6b#PJN`%$)}KodEX4^rdNPoa|GHr5foYwGT%i+(8GR{{lLU&+JEQw z@9>PL-dE=?z_fJ0z~2~CiV$%z7@Ik$Sx(9XgfC=ehhN`rL$y#@Te~)x^5l5`XC^Ep zB92tAx(M7utPqs%a7=F5Z;sMdUmTyD)U!Oa(Yva2-PHxg_TcZ|HCQbuJEI`i9Rd}6 zM<+~?F!+x#{mj@v4&ODtZ;AYL*UuDYl^q`PA31WHrWBH#qS3r@c#3J1Ttz+h0u8QR zN{5@#@>m@~D`T0y599AD>atmycl1TQtE99?k{KSUn@$8T#f<8UZ!gx=oym&R7}~cv z^*yTU$b3lm893!N02LvmW$92kf^)gg8%7K$y278G<6jx1AAIISr$lsg=E*0OP*?li z_gIR#$R=pT^wRHt0kLE~Qx)> zhRWvMeqIfNo`pjUy}4_U#{u^`ZjFP#i4cf6b-EHDlj$Pp2}oCT^z^Wo)UZJcbi%^vK-L|uf3)K=hNeCkWY--*wl&}Qq`dW<4Z&9fJ%UIhfcQfZFSkBO zfxue;B0=JG%ia%y4vrY}43AKlgXZ+-m+ZvO=IW{hVl$1i%Edj0FBI4v;T%~^UeqT0L?D+i@p~&h?P@PTK;(ge2UZuCPlCzHi8(Q#_tfA zBOQ&<%@9oxfb4OE`QV)cKL3!GHV6gm5TKozI-9%F(bQ|8%W~T-ra{PmnFeXK7uazG zbTz`HUi68E!mpCw9FRYNvk&l5k083-rljmgd}_cB2)<-ISN2=gX#@>sf`k+;tL3lh z_>)U>&H+EB^aa7Cvg0I ztp_t2;xktp>Ztqog^lZMqz0J0XYyP~{>l&rhfTImGX&QE=A8Rc7|JJYu=!F_m}rj? z<*sde{R@>bibUL_4cwd6S15+{f%&f)uO)T{e@rzv^gI6GeRk0G^{~VyBX~S>@dEpRlU^ULbOVGU6QB`<2n9bRp{<=f znQ{f{Z9epnD`bkK10+w~y?`S96cnx$E>mo1o~Yz^0}O-cB1~86g(S>j_23FB2t;iN zP%9DkXQ#EHt?fgI$(w&Y%M8ETYUZp!Z2VqscppN0RrLerdkF~f8XhtRu1h2WfVu7h zR0a}&-}ka%QtSX?HVA@>z+zaAX59h>_!P7dVS09U9E1dbjs5oBJI>-L)XsE$ zX*`(Bi8wb9l}SAad@lZ(p6o3^$d64wX_k!0vg;SX#&+-bL-bvr$H^~H5J{Oeay zsIN39XcXbmK+_JVt8O`og+zKz(ci9#=otQ4<#7o~^ zE^?M&Wi9@)(}A*{f6ip?G`K_N>bUP;Un=v#AfrnTBU|PC(!1)@r*F;n+0*_g{*?OJ zw=n!K1HJ#Hp-&8LljzoL%M-<0 ztC6_>uL6yjZ%u>&7^?&j zeGN4B#6N%t2Ove`ynHz)KJgUvh9C&=x(i7kJ{Ze=<%aH+h$|4hK`l%M!JF51UVFuh zjTLyA38d2&7mT#@^eu?uzeRtHb2GD4^sUmC_kiStPUfAUrSjuAYeGn+NPJLGkmP9FsZdC0sBW4YY_y3B zNHHSSsi~i#Lr3yQMUh5v;?~!FpK|A|@en{Z_DAcAa0}!`M-yw>q3(%+bR2_PMdz#! zcA(1Pow(rQ@q~OOMfY}d%!%#h?)fagLz~WWo!Ntzq&&&Jzx~}j8hcn~n`*l|dx+PJ zRSeZxgx^%}=UuRAKGdf7Jx4A6{_dSgny`eguI{^$z87~gO>`3fQZ*j)Z5Y$-6?hSZ zr0yyG_N{(Iw)%v;?zQL{T0(NLE}}_?nv-kE2E#Rh3hDDYlI?4rR{LJy5mZ?2!0D{= zxo`#Z2|innzrQyiETe>qO??869T|Lvpc+<~Ydu@P3wRbNjXX$oAsH{Vug~Bx03996 zP&j(aa1}e~Lh*@76y#+f2blq=z_RN8=EMIE@;%v{CLF?2TR!}~4Fqlev%!HL8pRM@ z8TEF{?6YiiZhj6vyJIthGOg~Rrft8&l#LDblvcGyKu+1te9^+H-8~y#uHsqDZ4-G+x<}ytbo1UFJ zNmU9^Ffr3+$Hz~z{&vt5SSfMtlB$vj7=P@};rInpaEmiVjrY%nWlh@M5)ASv(aG5EO)J znX(uJAnEy$olW@r5sz9i$+u&u1YILR1p%0CEGK3*bk(XHwP%0@yPFYea?_$MkP^HM zZGH}ce3fM2iS^%r&U=E0rogw+(R^1zn1Kg`F&x(tgbF6?FIYolIyLsXv{>4oCBEqH zj&<0xP617(Z+4s{yfwo*y#Ro6(<2jXG@b8+TnHuC`AC@PkqT6K^I4w;?-$=cg5qqVlWAt>mRlo!=px!-2|5 zN=%O)5dh5&sm))fG-UkozlH8@G=#i%1DFOeH3Om64>(7~O}F|80=7b6eBh#2K=olS zZx(NI-Ibe{vcz^n0X~nI<-*BLG<@fc<$m{nt4DS&01QbcRFonR5x7q7EGtB+T{Ldxq{-5p=(=m>{d z9WScQ6cNoez$YO3xHdtDZZv?=OJbiXJ!iOx-^p}qTtkq*1wiNNRC)G*T0#q6dLk7z zi_M*QM;j~s+Z#bUE&+JuL^iL zxnzO;nJrsS70=6Jm)2|xa~-liiEI_4H*X?f7GhSGHL*Knh+CjQ8v`3;YPIV7Yh&XHNB}>|4V9ti ztkM1O(+tPWil*N@fpJVh$;d)b*Qr3!C;K~%@87+91?!5a53q=t5K>xoDh|+ardB7g{y;3QuCJq$Yu@)c z9+MtS7bFuE6-D%#RxeGp{N+Z}CSf0_2eN_~L=KY#4gU+uGutp#Tb$>H8ETG5eAZmj88uKET>s!oyo&cdZS=WntW0 z$MMB~9rDR&7*f7^(<`7jATX8bHFf|)sk7}0ePlS#08PdAQfl!a8X%!r^$vCj>w_0& zW@eUlw@#TC6qS_TvwnV&B659dj94wmVBia;RmmC$Rl`7OCQ$Ln{nzutGLhxRo}OPM zcf+gBL3e=)K4HrN81?ezKiH54fnEMGy%fqZFyHEPx8Y_(H_ zWbB3)BNNl7F^g~;uf=8$Dq+v>$L@ga5Y;ZkWXJ{oUTe*Lp4@)z=L$2mgE6ADw^W{X zJvmPvH#3WKWlC$F){fb~8E}4h!Y9w7l|HaneIYv_`*pun=$r13!`C;**J*EMUu`fXjCWnB@U4Kk$8c;L%sy zHbA-ILVJiloL-dwx^_TEP1>3T9*2trmG}QK^&Q|`wr~GbMv_&E>|M$TAw*`8mC7a| zn^5*DDw&0Ztb`DfmF&f6_SU)Ob>=Vx8L z51#WMI~KnMfZ%eK+Sx$FT#+Eb)&AH@d+qjVh?c|-X9XxkejJ311c;bWf@J=C9J^YF zlEQu2z#%6p1l9FRj?EVJCDYcSXDF4>VUctKxk>E4+cazrou20AY5i6>IRHal4@#Rw zH)(8|!;1sD)O<_Y2|x6hah63_5yOEF zER+fS9*JiP&pA8pXhSh{aEke%TuKSb3`=-;b=f{bP^k-UW zkDu5^jq2p`t_ceD&m2Gf;)VCx-qb4Kxn`L6h3*|HM^1`<3S}#t*wxYT9t_D26BBNV6HYmzxD5$fJ6in zCpKVXYG!GwZnc+%#nJfI_N^NIp`juD0eR3F*Jsuz28()f&u0Dsw2EPS6q?R!^FxE9 z8)*8V(cNK)YUZKy>aX7fP7CM`lFcq+9n=~?q+2j#Yii1R%(1uZ<;TXMxVP&y>n9FT z(#94_t!4_o+$v_yaC+$dX>4cFZ75DNc9i+HhILEEGI5Js8D$QLoL*dPuJgSW{IzD3 zRyg~-v+#Z8jMd=aZH?SCot0-PzW@C*92AmW`9eRGUnfM+M9cg^_*YHFq?Z2i{9OlK z-LlnNzf5=*0##-8^WxmB@7GEo8~FT7jbo#BPT>(3p-{QI{nV`wf2tTNu8wHhR37;v zQGO}s`u>d!FAruuhTGO>xEB^)qOZCHr6~j`OPFU8zyKL%T}u+Zv<;`I&2&eGKE*^_ zTpYy06x8;HG#v6D&gq=EEXnj800PS4N4X_x0)#-}6FaO{*yE6N!cUUWWDeEzJE@+; z!#AvOVIw#rh~@~Cz=kaEs=GS`hLQHKHoywlVEnQPNRjmt24iIfTn=i^7Ky`yc4z>9 ze65TFSM|_YiiR%#ydd<^XPq2)9f?Y8XLk^g55e8ABnQx8xtKpJ?Tkk zJI$)D6;Fr!sz+Dk)QjHt002S~2udFQjN=W%+}e$M;L!VPxw<|#dUfi-<)MI8vu+=7 zQz$?Qm7dp&i%O1|?4~2=M!i$2YHD4jwmPMoFx246Ez;XP$FP6PG)Un}SW8g|B4&>G zi-BuzERQt;@7TF(SE)CfNtxY^iV7(Vs*)>b9$bcRrVUJ$lgu;l(?>4Wr2u6j8Zgjj zTI6xZ)N$Suib8bK*icFBLI)38OnykhVHgZr$`$Oyx%4@jhsdwa)IEs1cSIe^4^U80 zpjHqtC&A@cf5fR(V*8ODO}f^K*ehM>p`IQ_6rOHJJXFQazbo`NDc;P6%8z=wC|&Rx zD_&8|?^|nhw7KA=e2`Sgxbz)EETznO67_xG*4_mRa;4QIGFb<#)qh zeUjAf(V zw9$|N5S%jluvU<~8Yt##qY)xZV%C)Po4;9zkA#j9^BU1wVNe(PF6-^s`D+)M#F$ORUArKFh9mtcm6gBn zDiV8+qdZuGT6_KWt(fx{3D8T;u%Hrp(Y5-mbp?!+O+YwFmFa&$XGe^luNK&YAaJkr zUlT+3KAvQo#)UQne|y;0di_==L=FV<1i2`r?#e(%{u9pr-NN6yh}YMVZy-VhBI1}O z^vFn-eL@&EK@nMD7L;)Md=G5(*+S!EgcBS0v_lIvhJ)Y|S~Q%6?6R`5ffyF^XBrCn z|8mfl9ccZsPn-S5a=@%_7|5^3f1IbegJ~{Ihv*kAsmjs^$lhc8KP|vDZ>EiSx0_|b zL0#3d2U(|o_LSZczga@37%v5b%%Px9>hXvU^2+f_64?S$FanxUXUwIjPv*5N{*>V!GuQ^H1%l!3+ zg*6FvKAFplH>rGbny^`P*h$>#HP97|CxqQjCID;79g~ARBch{)Uud5M&R6(^3ML6w zkexij!qMQO34Hxr#>itpLf}`VK|w*;N?_9eHrK~6+oE^DctMErAQHwZymBn+oUgA8 zDjflZdV6~l(^!O36~qo$5XL{1K{Z0HEcBvi74j5r2pWJs)zsF`4A*4vZjK#*#spi&^SvhRDAn#bN$D<&rPx!OwvwFmZ5%w02s)fM&^8iDMDMMh>p z*QCFlm4p!ggCRkyK*)XptX+q>2j!4p&R`TJmLldBN6#RDaQG33Tsf4Ios^UoKwt5| z690ORydp*4WY^jFcl90WA2DYU8GbNlWPEJJX6*;m3wb~G{~lati7%qHzV&xj1Rpbl zyBKI_XjJ7Zyl@G5#$)CXnmsJEQ=1Qp_6G&)3V+#>Nj&dyn^HzjrBRF~)@(uNuRPrI z4QVVG4CEY)wYusXvtj8OqbGs2yU>o-an`><_#ovFrL2`pTEFf=V$b z!Dl^T%%8ivM#8@3 z!8@eCyxPMq%75}nVd#1Ge(qZon)d(QL(8bTgY9Ox4Yf?o=D5nFmp=p+OP?3a`fl)E zcuM?`p4oF;7u%&yJhL&0YM*1ve}3vNf6Vjm^t}7`-n+Uf^U%`9Fh1c(K;W$>GIcRb zQTjRASI441A9T{3iE$UdkbCvm_~rcOHr0~ulq;sD>RNU&!OPmE6EVc z%QdUG=&I~!^|AKWkJ@|#r&_XXrP2ILY<4Dn$zij!SGoCbaj1DUH0V0IN>&Eb7k0^b-^kZfVpw6pltcXU zEk^(P+PIVKEyGDudXd*&X*rrCi7kFF)c6Ahu1{!jOf=_ym=uw!TIq5?sns-Bz-;peFB-AD-QQnR<) z(k4-wnGEi2Av>I>N4l`=UbDq=_*TyMHTR9d8`NfX>H1s$qE^PLI`iEMHGYQZih;xi z5B+U&QoqNH7#8PrH5k}mjC>;%lB15;zvA0u8!v1B?x@aZyN$2B+lXt8iGLOjo}0O_ zZ{P>x?+YDqr_!;Cwv7jGD=9MLQ4lKZDk|*UulwlWK*F^#LGsPiEh%!A@EUf9$MJoX z9X(BIl0ob5Ws>~MA8gsq*ED4c)+uZZ4Yoa5)!tnjLPr-9&0Q;HFL_cW@E|3n;6WAkGD~6)0nS49d?cK*vV}>xp=dFJaKyedvKTo6jbs(CC=-s0cmnN?zVF&N!5Jf zOG#Iw^$VMB;JdWOiZ=VT4~fXv-5u_J6nmHDQe4%qZ@14~ z`+G?$PFB*wvq(3K&%c-Z?rne;r2zN{Ed9H4vL-*GZ6RNN?7z17^5y3`+WD`QIDK97 z1_J2tb|@%1f9k&e@1O!Dx|fO!?GYrElu4Q?mUr%-uqkB@BZ=Mk7i7Fgo-*-hL~IB| zK={=x-bSe0jydWV&%w&Z6s3lis0F4KcCB|RD(pPR)%-dej269VS+)%5ymq9yFDE5g ztEpOceWxngpltT(a0y`7;1=C`b01r0*X;SD`l>e9PUaMk+lfvu>Ld!qtBdj&z9AzjOhPK9YC{UK7V5QCfwTEnvf2{g06_(l4yb8zZO9ADRN<8H7+$(D`KAz>gK_C z4M;ZF0B>%c&`bU(W>cCJwnqR>T@xBa@K#$q#c_~=rppK1JIz~gv7TK==i@2Msla3$ z*BgIli*Vf<455FTox|_!CHr>6C~JxQ0s|if{eQn=Y?ER3dJf%z@9!Ti*GaeM>Sa?Q zyO5Wcx1-|9MQZZxR&OIO{rFsbI$LjabQIzV+qtim(<`*?8Ea*(V9#NTyspB1sHUV@ zTs`{eySaz?=cyh49i&sZtaJ4HYuQ|G;e@0Sl6yj*x5ip_n^{Xd?3ksVmXL`MeI#!2 ziRCD>7T-Qr_s%2_l_Z|CSvimO^OhJ{v&44lCGE{@uwL<}+p&|T&V2er&C$Jzt|v*P zR!ivF+?kn-Z*_C5a33={6OzPY)=maTq*bzFG|0+Dw4z zn{fYL-T3p1Vc-xA`gg)XIf~Adf~tR-@qbNI$&CJ&8j8g{~JfwYmKhv;NEbtxqz7k3>=mcUrxQHTbqA$tbgj22fb?O zso;Y$a<>w9lZ){b#jssnDxSR3e3v?NGOgJ{^l|t1PRHHT9^}+AO=dSTt~pIi`iL?M z`EW!A$5^NDD03eWp$(}wq*_Z2m^m$%|#GUgK z&9e%)EqvnjtKol&Nt=ISZ$%hVQd9GCa$d)?Sb`j9Y-;Kh`YPlNOjS;$@tim@ST$qJ zim)84_%RT%mteTvfIuG#oX?-~u=c=QSwldHy>b8h6BmGXi?za(+c2%!0yyfePiM4g zPf<5EHqLvI*J;N-y;Cn!Z8yRKw29UT;6d0=06|yrTer1h>c0$=Q5(#AXW%W=aLVC@ z`GVw*Kqy3g|NARV6ip66FV5WNE(=xUsKU8r) zi$5zS?*q520L4!pZ;HYBPTfS4U?b7E(bUMdT<^?X=}s>EQkxx(ZZX{uq4!!95DD4b zbzz(Is`?dQsi&G1>A%IU&2Yt@{~hM4!p;VI?EEuR=2Y!dpc&!3-9b+NAh!@RMQ=)foBw~? zQeQW}=REr7;gZJ5Um%qD#l^*k#p3epD&;vIU{-)0AnD3W7yRwk*1VWF)`s%+^8P^8 zalNrm5aS)r};*0 zhBKA2^^V%Mvl2-G(6SN|Y1{Uc45eFCgg_f7rkLMvCnCLPT(bV(Uq>t*EP}%&uARWz z5ZI*Kb;H`BnJwuNPMarw#1#g^+C%=thMFD1vhHVGVeGLrDg~i(QwwP&-7?s=v@Xq)- zO)pO+Ll-1fo^kopa-B-x7r%hm-NWLJW3XUG|u zRoQc5c+IE+-8sgCYhFvLs;D>_6&=4FW09KQLI|*9^MpW|acF z@}vG#`#Jb1a|hvradE`!FTGG8AnyiGa&Mu2DTB>+gkbAUqCykBx7gjbS`~EDxCvpB~)Qv4=`tAFtKYgJHCpj?FB;Y8T+~D7r zpOG7k8>Tray_7{JDo>GQdr>)@Iu@L~i=X3SB<&jC=;)%gj{C)e1Fw?q`W`L6sw_X` ze_L|t#nm}a2P3c9qzR118@~sx5U~qD+!#Ryfm`2N4cdAK59>?c6+vKnhur;u1YpqJ z-hrQ!d$;q9@jw%p0eoz;z+#qg z?2QC+Mf%`#R?pK5nLtwf0j){?ymjkVCopxu-?oe#P)IibqjawG!cIKl_uCq<8Hh9Z z6&#&Q@kjLCrXg@c1-S_<{e5NMd?iu}SfyRB6L%xsxkD+xv7n3w$)d%q@A)NV zyd_uyY=O=a_Io1L0Y>D>_SDup^gOvH6~ZJS_z80jB2Mt8KBBpNkL^l?aR3P~d3H`f zGw~I`qC$$-03%z$RvhW%ntU9(9sfZZ#{tCI!ajRG`@1qIkIVwh{Z3 z$x55%siVPB2Mwu|SjGHP`G>y7Wr%*#v-%dl$R%}3IZ)V*R*7!Mk&1o(>CECY&(t(+ zPO+Zh;!iGZjhmUdx1S=9udnG9{{^OsztR~OT_(%-KNT-MwMWokJ?( z6P+MPy5D`b0aULnY+hqn@Ee4ZB5>2|W~0*-o!E~4s^Nqt9cz9G5VanTMiNjds&Fb9 zzI5f9uUU#NcYy+iK#L=)N z(JPL9`*x{T0MH(`hd6Quz&!0XlwoNDC>``q4mz?8__tV(yWhOAodRS#e1no0 zlrwM!5U&j+J0Yvs_g9n9;1V+@E^jR8GY{JIz<#=%%BhWw19b1d=HB^>3iSD;q@-&v zq_Dm5cEOC5)kz}4d|5r&MyoT~^7>4z}u?Afz0RK&r5Sb~E}Ru*9W6v*>P_7e{Y z;=#qe2BIOtmmBzZMj&~E3(i|fEJq7G!rocJ`h}SBBN&IF7A4q{rqG{K z?3(;>(p>xh8(kdMq{f(%83zi2(%a>lzK@sJ&`W%M_Uud6Wb5ZjYi6Owpc>&DMJ@rY zMdkbUwdfa2?u!@=YP!RBq-D2|>KR*1!kEiKVCR_O?!?BOf)W~Wxmb$cGx*AeWJ8_wdn_Ex%4Lut{ z_Sx9*}AXF!WFmIK|LTKeJK<L~BxT3}~lG zDxfdD!%Hro6Yc}^2KsFqSKtlUnDFgG0EIh?tZL0+ZBSBHcJuU9`-q8ws zsnw+_5I%%85Dg%OA-^;BoGLWFU~?P;;#yuqYYrs8x6S9dTs16(i3md=@+(f_T*T1) zuBRuSa74m4fG3=U9u*Hbhai#;tm7d2sU)S&%Rq>DrQC#_l$<4#JMARU+qk19&8|Qo zpo?=N5K6%JH|lZ6+eX2$0i|yt@4)mUNzZt#8;55U&G0=kj|XvfhVtnUJ5H zs|O;Fk+cK#Z?+sV$#3K1Dr<`qg?*q6>@k^Ricrkf>+feg!S>%InPy}8so1m6#eQ*h z?o2PA30bJ=;~uG_=gK}$R!F$@M*V4YVs0a^E|E}{vHx4f-T!{#1^R^cdRM1N&8gPtb84Lx+P66!Xj7~1J9+s7uX=jyl>mDBn66Ce$I7VoL=+)$ zY~)?c(Pn$)PVzI5l})Je68LI(P*v!5U_|~er>8J(mM_g~lZ$d2#v=60F)98ZS&rcH z7z9tzAroak8Ico-azQ9us*lRs`Qum*B)irr8zHWrUA-@5V`$G(o-H_FsG7 zW{N9(P0Kj_?M@nv4pVkaeS?!&#<|(frykQ1zM8jVeR$<4<1Ew;A8XiNvs2@Hr$q?o z*9%A3zuta55-xF=?n7+#i?&?%^Dkwq$EI~l&K+%U*QrZT4y-v}&1x$@_oig5FR5;V zI*9e|Xk2>~Q_s_V+BB9Qs=CHA@5=ig?Btr?xy$(w?;U_$GC`Xw`MZnv(mx$tNh}q$G7Dd=eo|7?(1!JHV|xhd73zzeR+97U#y{hy+A~ z{`1Rw1p3L#57`9heKwu|pPr3JP8iRKe7MRni!qO{g#=?IF8v zdawISKA&*cW54;Zy|ibqc?XNKP+P01J+5YP$(Bx}Rx5L0`@$O0u^`G$t=e#hzVOq( z)-R7~_lj^`tJ{0liR7L`Kum5eSNhW=ht}J!>NhDW$GWQ(Md)`7Ih_qr%(#$Pzr`T_ zB2?UVl}z40oAgS>(_0ay8`BBoFk$>zW4uzFkbu+8B*;K8o~(BrvjQQk5{#$H?9^8w zIH!rB2DPs{(f_S=Mg3K|k_}ArO5;UCCkMabmG} zNjR0A&U4S}$4*}j;1h#%6%EWg46%f0WN2sz zGFS28b*eKpzc~lc?Mwgx#j$XlPzDnJN*_Od6peh{#^M=ZI%nQ!KQq+J)`+JVN+#|h zfo4C?2PKw5SLYNTV2WxzFP?;EW`j%AtqcZq@mIHBtd!a^2-GZY zV)bdQ@!_$P9=I=ix-&<&yHaLe-#hfG(NyJwJ?a!io}7vO&%TBwe~RjyP+K(UGJcky z`77Q}JY&O8no?)#k|=#lPS_~>-YB$5t>zC!f}w>b%Jf)cf@ev)ry|^(Qkpo z?2JaCzdoq`*p((oo8d7W0k*HkUtMuGraBOUA~A&jWu&&DN9Bw;?euK_KB^a}YNl1! z!UzKzYNQ(mD8gQdbp_?lJ6B=+Ne~7rfV=oWDtvL3!zZ zfkzw7>C$cV)Y`NK272EbzA=AJ^onUs?43A1ufQl+Yl3A=VLO0c!sxztd|9$ho~CdNNVw4Y!Lu8-d*E!wQlR5->fzDk@PpPjfw{S z^G9dOEblK(fWJs~0}z?u9VhJ-oahzn6=XCd>E5*Cm|X{02=dQ^FydeLSC&2zdA&RH z6fMg6Yh(r@A9X)AlI2WSHV)5l!09GedG>yqS|>1kOqPwv2|>tjER=6l=$!5B?CRzk z->jT`SwDoKm#_dqCV5OiJB1S}0E`oGixSa`1sR!()x#E|7NSh9?p3R1hPo$64Ei2V z_!=R|116=%uxtV-89GohgKBA7=U@41HI8ZiJW>0{?Nx1i=|}fAhQ{-kNcynV`ujcS zUb`|x?HhG2DQAtx?YPYc>V^4tyH|EUl{Lu8^E*fP;ZL*w9lcZisY5|@?uzXfJ+`b< zEw=}3`YzdaX^@jqb87E-*L8FF=uVreb1+FP3;wyb!ENu!~eH zjfwYoP;6ctHWWCxcGzC1cGK@s$00-CDO&6UGk;mGgy!S-JnyAD1b%$>X>fVr=i93B z-Fzj7=y&{kTAi{I9<}`YXUo&^`kC3`P}wiCN3=|Dw>UjczOtV;_@v?6qx(Mnse0 zhcD}GG@Ii&DXAUj)YNWAQM?XmzVcHRS#&sxcD}-aMVJOrr!9eAz@1CzP2t3RcmL2i znAN;ve%z5u%3=`{eRxKJA@X#?9og+VByUrk&L@cMS9k5m^RN~yJ>AmQb}wz|GZ_V) zu2rLTTj#<;+HRl0!SG=88%HvgGJ=(blfG+~{T;nkvl?6xKg4uku*h(vD_o z?@p?iXP9{M@6Ebn^q$OvKHi)$p|p5n1CUGQdsy0^WMNjQge|x8j`|I z!AK0Ngn{aSl*U4Je!o8!gAi@g}0CXwStqF z_(=b48l~1e0I7BFhN(7umYBzl8V)iHoRQzq`i%t~H6lc5=5XVx5OZFTp zKXGaVgpiVM8Ya}F?Nz37(ewIh?ef}T^z-A-+Cf`&rmbOFFU~k;1yc*;#hI~3G``HM zROidlpbS2F=iFtNSr>86M+%PTdSeQ78qQ6I<$t-R@r4k?A$IEq*Ig;+FXyKXQFm>B4nrkqzvsaFmX@%JA>Rmy3GZX1XZ-S@IZo>- zSI^wTgb(@DzKg?(d)eMInW@PzCx}@2pOo_oX+NoNEEztKmnCrdQTur-$$dhKM)sCs z4?Ab6?Rtz$ZguVacHXp@^yK4TYO##elcSY;_DfZkg>201E;>*5ELv=6Uia?o#pS8l zy0*_jD>TRCO-`k&t{->(sr_yDo%=F~Eg^O*pq5g2sFh~-OBk6TB?{Uh;yNSfP#d2d znI9LyngJR7f1#m|J{fXc3)F5{abuuB!?CdoNV%LfY6Y1>M$(WqD9nRY(*n~m_9A2Im z&|r%)9X)yfFFDDefAxaHD^AHQr?;xEfHqT&LQS8KD?FVwPR{@S*jY2RMW$_Ou8kyj zOfMAchLSQj%Hi3#X?A@O2RQ}JxbE9atoN+MSSky(o=={)8|x2Op1Jhqz;ON3%(CU@ z>n*R9cIw`hQ^bXR@e&Q<}OA+Fd9PlZhu=tW;*)JF4 z7Q2uA<96M?a+-uvZaR`w^>^7n&Un2$e&=~)P|Qv-O{TD(ua9Fh`QEAc6}{Mq3w-&% zOZcoRMM%R*fXYa^0@&02@G<^@!(z;n*a~2V`-U*4 zK(7dXy28vyPcL?G1q{IB?iqx|>HAea4(oS9e#;w|R4Pyl>#t1ra_dm!{gtS89C|#x za2nLIm`Yr1@b~xe{(DZal=+r)mz;f@6Z`OZR1c>Y=BBm_(C*&w;47RsA9ft$Df`+mMaf z`xolCik(4ZWK%tVVBk^L(W$6(5lce;Vg~R&$$)wVfw!22>^?uwXsfL>7xz)6gJKb# zeW+3fp-=bRBoHD%9OTb$SH0gIAK!`{fp(GP$g>YX`z13<@RrmvJGGq%m~Bl2E{?Ws zIws+AYhORT*6W`!uPc1m-t)QOb7RA@vSz`8yl-5ZxAQi~xV~TN6M3IJ@|!rqTAjam zw-?fXOPZo8k9YpiU^E)3^87=(++}u^NtI&x%iGb6pZ!=aJXP9Ws#+X%>4x(3k9Dfn zIQhO`mg$HCLca?JpsBzh}6Eq4IUi4l}u+?1D=cCxgtF?b>(tsCypr z>K1t<5U{!aT+rtjl^KpFIE#!DN{d&Rt@%dBmAIPO7B;~lT& zvio>4bG!Z<8geq4tm=yf4+NfZUr2Wdkd&vX%ldvULQWf?$0)*wKEH5I_84@9MiDF{ z9JPI|&9rW6(^ zq$Gs#oN$r9ta!!-EC$qR@z5R?mR9f>A2Fd(1toIpNZ7B2(YWXtutp^1oX+x=tFC^k zBmMp6j*=^1pKAp=NoKZp-SZV_V~{z-Ly=iWeUp3-)3Ta`*Ddm~0LxJ3j%JPCqS&zA zoQV_7=NV(B!}mqhaL(1DF8R?i{B@}exheu1fc(@$ARdXfd6De z!qB^pgb@|o6aeNH6n8OL&DpR%Fw0`N`^s_pDKSYq_8!4rhKekt;S~VLN(89mX9V0u zBuHVPnd!&~>A7txYzsRJ1ntFvn*=-73MsPpw_E#3&e-MRff9Tv385~52ET9>Pjujj z;q&WAdBIf9;R>pUz?&d8A}AVSE+PSMAunWHHUeN{0_-RuCMM$F|0mw{HuU%Kjo3oO zLO9RyEr|>OJb_Fa5YL+IE#F7Vo4@ zqS)MA=Z=%^7^tTh9yULD^{&lJ^+8HEs*EVx!P{CtoWOPZ;$YMWM|v%qR9^ZCg_nJY~{1T>W{1G?j%1n{GByd?((Yd zYGLb}f>zZ7!<~^fgWWAp=CqFI34Zb0OkZv)tCrP{>whgdL8G@~PfdFuJoH2Aez}KNH9fXb zghgsZGyZzuBYwnA)a{EmM771yOUGB$Oq;88c?qITbqr8Rme4c9 z3qxd9fRltszsP@n5cZENwJZA|kr^-VGq-}0%^v|{i0rsa&~3niWjwyWo=OkW#z7C3 z&IV$Cgvb!;I@mOtPH_ZS_qklVCV^x82}J$GXTvBR24W{eKOY=D8Q!S{vOaqMGKAg4 znqL4}D~?12;$jJW!S^^NT9Ebv7u#*Eb?m^)K#_@Pegku#Tg0+N+MlBjIAv*wQBaSi zz`cwS-xEy^~gi;^OYE?XN*w8V2jJ6&9rcYvB4 zRB0pNRm$DF-?X+yV4HI#5WP6f3E$=Cvs0KD35gk8!u00m=91f*kvvq8Gk5I3ZeZlr zfEob8jvx^iZVlr102{l`$3DH8Gt&_J`ep9U2K1{0BTtCYP?LdN@Wh;k7@NsBrx7{!4wIdx|Vm{GpGFv)(sG)qhuC%21|!-*4DE=shZ3@srK6eIxdYQFY9_U&6yR z=YL4pb&x<5OT#8;{)aiHi#Ye!3ndKaIYG*2XyIz{Bv zjYgxdfju(crrwzN_v6ffj>z!bAL&2F1A%Hh|nY#RJQl+c8kTj4c@U`RE z@eCQ&D*32OipuxSu!c2P2DdmhYJGo;-fa0d489*qo5%cP`z8B!)k-$aRYe#u%G7e0 zk)L4j>CJsv!dOwdC+U+b?{JN1`2vf^4XALyRQ+hI-Ist@_X{KpVKGb&F{?x*J5hMB zA>fEEznvPuK)F$Ryv2c!LyEoWhs~bWDp~F|*E3fmmW*DUWlFnx<+SR%aO~18D3}(Q zjwWkbn^%Vm4~lhmSUaB0|1@2$s?cfjFj3Ry1OFcKkUYaR=4HJT2^$`&?ydn$#iU2p zSh5aWWDk=n0dBB**<=p}mZN!`lmaXm1Hum z4k+F!&da@canybf+tY%*OAB{Ck4X;J5!3i157 zKCy!qBqT|1HG6ZMvdV1_x7(y;-whJ64&Hq7Ir|4?q|NsJx2y~vUen+Dwo@po1^=cw z`jF{BzGQSbDR_c+8dGuF2~k5k#Ux0vhlo?|IbGbFOQK5rg( zjGf)U%I8;2kJNM>xh?Zh{$K6AUG81f_9=R)8HKio_4$h?qaJ-5PyD36J`gYK-BP{1 zoxv7=Hw&Xl?ua2xZOh5aQ}56FzOre9ZUsZ=vdsy%F9XZJX2Z_ki^)1Os?mPzp`U~m zbSdV$Sk;~!jSvcVMkQ^0d%taYVK!Lvo|WjVQF=)jUCw)J=F&Wjv+mJ~w41&Oj8L;J z<{sMBnNdc>Ujh;Je$+RD#s*1ayP=8%`6*Y|&PZs8*KXRTVUr9^8vD(^Ku3WiVQcr| zuruQ!j}K}*fSNN}n@^@@bLB)T2_&^yNH85e5FsEjt|}m8y1ki^MdOHcVm2V=dra)# z$aDK6H3}QM-)v7(**G{34;hNHVO)iIL6}CSZQsOzlYCIbFGp?B(T}3$xeO=QA2yWb zGrx}L+2$4)T*H}0+`^2O@J!ge*R^X|P%FNv!t{UEm5$zrDu!xgl2fg9qbRd0>9yTd z_6KzDM$aus;Vy=%0*5>#79-FgTcIMr%DnSB6b{dw2P9#|-EKH~ItX`?^m>lFABP>f zx$iC1Z-)58Xe(6(<1NLK?2t%`|K}0#en}6E1corMgU$l5C1FzvW?DuqAVLVTXmILI${zpyVH&8+fVl^XU)x{wZaR3Xby(yrEB}mOP2j|y1a7x&WAATnbgZ) zyWY}|o==+L;^5&N5n6iu*oy3}N;HpgK-|IySN`+ zOLeondE*4Hq9`8YJ*_dzqWI)=P9Hinx$g3_+bFudia7(h7}*XVl~}lNpjyGUyi&FJ zrwdc|`7b^d8RfQT{;sc*aSBe!)C&j`-_7j(pysYMrZ7$*gNnzdp7y$>eUN)Y!8qJB z%ynInbh|W0(c;KG8xNl+(&cDS?`G9duJHS&1fVc2kg&R<-s6Tl_ZwS0MDR7*xIDwN za;d_4a(c^D$@LS#rh8f0w~uPiTD@`-z2~+<)gPu&(pAb){aN>blCpVON$`NTT`}n^ z-ct`4BQ5-w=iF`L!Xlj*)1Liq3@MIOGCPsyEqz>6w2W%Ia{eQ!htjO^&2x9WJx_hL zX`fqa=4D8u{goL|XklzX%Uu>LG9+xN#|}^+=9KycT4oBr_|3`G)BRnpTHG31oeb6C zd)x7S+XT}68&TQqzm(;z{Dn7yN#CJGw9Ta)(ven0ggkHIdF#t>Q^F>Kr>0~7HqmZkuFAv#4w;5y>Md2?J)B`mHE~-18K4#bmfZ1vL z4t|5bB&0W_wOB7PzaxnVZJ78$IYbp0b!Dus^owzNO;}psgBl&G+E}-db@!j%6(c zeo_&Od|EAerBuK}Bw*9e#0g(dsh5w0CTHu+Q~4;>@_n3~_iRqv+CJsG^wZgSTaI{> zk-6=m3AWbwk$8x?d>EKd_(}lMB_xFefQ_y=31V%m0eA-pV+L_4A>PRT{^W})J2(gL z-RR9KubdPSG2Xi~CRBMZ8~ah45K`4YK4LC8kGiL+>NCoIJgiL4Y%u)p!!6O}zpp%) zg!Zr`NpSmadry)^s}F82lF7Og)}s^O!&F|{_VI?(kd1Q2FX;VruQ@*;*7>pbfb_n& z4|Ruk{;2m2Jzg!0twH(w8W*-eM*K#uL3>11gZZajzlFz!BK~79?%Wx_tDR#-BRgFHo&+@7SuSxY#{D{ zG2|fNw}*D6kN`orMO-P46qUl`Nf6zsppGELmfWJHKaRLr4(VjY|I(7_$}qhX_z>8w zZm8uks{VK#g8_oLDHB8L8q6+*#ic|B3+{D6x;c-y_@3*q5p%~4n&>}63LnB0iMyyE ze7{_44(9>5F=gx=T#!tO5RB8jm#6YPcsDO5*E9O^C6w~mO%C1XnYpCjK>F%V7-XvU z=h^Y($k~Ku&k5~hN14*-Jx}A#FBUcTe<;;&;7SPUqV4Q1 zk`)XIKiuEv^j_H7Ovzbq4ELm$W=rNBk8uk(|93(@x;&ulf9f9fy@=2k>=fv7DUY&5@v_QB2y!u( z&iqP|7eAm01os2Kw?r7-Nug>f(uzr^gJiT+>d_*4g|v5{4%0STi^axdHn#TcAw45z zl&L6p)bPA1+vOV5YF3e9MlSbjt!&B6Hzy|+9BW{Y2IB0vTslrj(}Jz+A094MB~jF(uRA)sXF_wIcS zMdnf=aa|Rm+Cs>u8j_R=#JK!JYwIq+#ZdHvcB%3Z%D6^c6a@5`S=8csxyz7q%na-Q zX#tu}BFyAPapyBLS!hO-09X(gYVDf+12hd#C?VE1D`_V-Gp6v#kLfz-M4Xirftcst z6LWhUU^)vJq@&8i5j@Ll44d8)@doUFYS{VeXzEN2hL<#AI7bebdWU$+diVFNJ}{D4(UHJkIs}m@HsvB>weWh*_H0$>W{4# z=S&`HT}j#$U-5W8{_$$8^eEpFf6qM!p``~t-?%+qeEyJApSD&8`R?NKIe0xF@~thI zyWK3V5jU910=vYsUs5n5qsVaHgLGLaIl^{q8^YG!%Ng}>m$+1|l@|1ARgwYy^z6Sq{j zzX22)#YG6|D-uD%Xb*)&Pbr@zt|{o4xdwju>s#58K#0(8m={fjas)mn$T8`{YK4w=3X9`9U{=q| zU?5d9bVs0H1U5^>%7W}|ibbIy(;73E9cf*DYa+huE03REWhALu^UQv5X1Ajem{i}j zRT55Atz7pjT(#Bre`8l~y}w)B%f=|Tt*<(Hf&{fi{K?mY?_1}0?rA%bmQ!eOm_%vt zVaJyxRmY@?4-tJKp`;_6(xQ1?-YLBMVj3=d|L{6|tZ=BmX)~v&PwhcE4`@rX4UOp zGbs~O$6fbD&ScMviw+d8@^5UP=O22}@#(~UnjvRWg6;TOJMcMjshqP_Iy?A&3qIh8 zTgjIcQ~Pat4U5dcGw;2U%h6LFWHCuk|F3AAR%H4>-nBd<*$2c@TI03u$ldLo-5K}c zOkz5wR%T@6-IE)9&Mts8CDqlnZuPb#b0m|5%(deVWlxWkB^{p@EA6^=Yl3s-> zt~0XJ?)7q_)Y~?OJ-$1$NBb@8b#F_EGlg}=l zdxGW**3uB<6vn^Ygrk&XWXwu|@e@}6x0LZhb8PS*kR}w z_2)x3>IB^1?7~Gp1`i(C>})+(=kHTpGFUC-wz`SPcsR%E0=i~}uGVe5j((@^oM60h zVO~euHtm~Iuj*R^R$HN~aaAR`z1yN~SjY4{ckqM^zn+=9E&XcY5!ZS5g6nS!o)p@$ zq%7W{jv8b+S~;rXVt=LcYv%D4(^*c=1=~Tsloq*n|H^_A^?V+CD4TJgPCVz#4}PmZ z?nQ!EYgB{sq>FPAq-07Cp~#w0#2E0#%(tkZ#_IUw`Q#Q%A78x7`9IYbAJZ(%`4Gof z9#mYGT$P|6&u{pSlT%b^oX2;4)gs?g*6Mw|)mYd<@Tsf+3QF%s9ZL2&=E`aA^L~SE zBgw>Ba#l~Wqdi|p3#;heD9_TjyI<<*(>&5nj6EG|04zD9Ml243l|2nX_?P zJL>&@E==CJ#<;Yg-4b~ArRzZ#<*d_sOEA-1&2P7ku#|PKHR*m`cvMf3x){oII>$^e zcWs~Sg6d!CTxQAjURkI6;|k6+-25r`?xIW3Bc<_9&TyLSJCe!~ijF5I4>t7-?=slh zFq7vKJLE29JMPl{lI*jv^ar&)PgTg0cD9W3W?MABFL7B&jD6e%YKW}S1pXPdcq6~3)pcn{vyl)u4%U$70un49(tvRio6A~}VH+p99B zpn$-MQjqf$2qc^a2nG>%_;e7fCp0duJLF-2HE>Wj=!zkn5rORMr%EURxTEM1SDGe& zUbq`fkTGD}i2QZ$rQY?~{D zHjb(5EsSFDf#7s(t8j+=0FooX%R+iv9DHeFwC0O0I>)923$)E!!Q1!h=DRCu%ND)h z{T}PIBh-{(MV*>@+BP9#?);=ZZ}6)-BT~-KWHnN}JD145SM}Ol?z)KaG1Bz8H!bC9 z@F9t(J@(FtNy(ZRloWOJ6;(uyfIRC-C=ouIF}mIxIHfAwY(Li-`TuzO3aBjGtX)Ku zQczM#0qGKzE(Ii&?o^Nl3F&T88YHA!y1Pp{rAxY7K#)++erEnT-;y;mpQCR)ckI2d zs$;9vfZ=32D*DP72`7`q8Lxca2&cJ8wicNEoSYt*KIHMDy>?O>{Mk+2RR1p*Z79c? zQ?lk@6!dtJ`FmssvsuG1JeDBPT-i9NSAd;ukNpHf-Llqz`ZPi za?+~{FwD^Qr8m&e8&lZ*MiK``vakWDx$WczSRld!<6pE{YD*j4La=%S$^&KBTL9wc z|5aHn3<7{S4WH8u&)|O(=E3&3XrnJkV$yVr;`HX7l zf-y^1zQn?7tk5|le}uzjZ?aS| zBk{%^Q{U9h- zLh3z2+EJTa+hR}^@qQVF0)K}4L>ZP3TH1_=sGun`Dr+zk0Tu%LN7BCh4PSgMbRHlt z({q>m&J1`n_&+wE!c6h_yTk*VKGy2QU-#e=rG+S&xfKAVksDRpc7W4ZFvj{XpYst7 z_;?@)GN3X~;gEyJi-r&$Nw_V^f!g5)IpIKLsU6t?8#!0W$Zwzr`lwk*_xN!HLNa)z zWmh++E%&`0Oe)gTuY!(MJ9Hd>bb!V2vl%G;K;`h5H2*J2hh;UQM}mPU;!lPIR;uPl zgLxAo4}=T?Quf;#P$?qW(T%3_gY*OTnA$`hj0*;jR=?JKn)hn+8uqYYnE$2{A=>D! z#DczQa;U@pTB`ylOQgea$(M zKKpWJ^UcL?=WS8p(H{~Oak@@f1L08m!#f2>R{|B2)o&H{9fgF^^9@9cIesU4PPu&9 z%iUyxvg zVvDCKzZvAhC(yXu!lcRGkRp9olAP)NV~fv}2J`x5>TT?72eo~|N6F_~WcTo!rd z(_5*}O#wFm?_kN2KZlmGt0|rj;{UTkSma#Aeqr_|yXeB@l9iS9>q+tR-uzi41sl=1 zx}5F1PfWriU0`-&1@A8B-O?`5RJLn`&?YXx%+P5)&KPx{4e<+ygX20SC+GNv`t*=$ z>e*2Czd0z!aJ-_T0&U;LQasvutpf=oZZM2S5%~EH|JrpNoPhR?9l)W-wC0?Fr~-X# zQT;xjYLRvi%nu~NBNS!>Fcr(1TgU=i7}Vq>Qg6klNkYv4MvraIZ!0zwk*JB`EaexR z&-({T$`IUOTS(oUA8EAXO3uqI%{!Dk$)miPta_*J302-5x-J<5-3hmTOYB9)oPy`4 z`WL3OrK;y-yzR$kex$^0%IW~RS4ht5n z)6yy7N}i5NESJ+Nb+I;&C)Hd_>C_Ej&Gon?Rib?EJ3e?d%}H}U=5G1;U7D52gM-O> z=`-soSV#Jua7chrF&foq#Mb15@@&O5qxVaA!U1OQqxcwjw7q8c>8Ke|-<8ngW>>ZZ ziWduK<^J$djZ`{vRVtQe`eYg_nO!O>Rrj89rdyIA+M>8ZUc{OA?ovk*GOg=r(ctYavUXy&cHeuH~C7&Y&yO>Wnzp8z!u zgeo}2h2&UG6^weFreQVzeZA zAu|XGQ=Z&$UX742v%}TU(3o$LO1;46N*H)_s8_1qAeI~y7>FeAX=wbd4QGBoZ5V3< zUg4;RU?~|jdjOt+xu=yoFY({u7lo1zs!cCA5ig6X=n>QgUi*6+vwNQ4RWsq0Y$ji% z5S4pxqmlD1>FeAbGDgCKBGNySZ=8pAhVDLnaCpjSM~Igeib6RZ^{VP&Kz@eA*zdm; z(iGg>as82VSp;dOo50;=UcJoGi<8*1w>8u>w9O1@P0oq+r zy*jTx^`SkFT^!tfoPzagjnDSegwx2?JMKNo(TG+~1+_`1SMczb+SVdyn=JubBG(>ydWf#l{?9`{ zIzM`jG@6I8_z8}BkC^hek_jg=4)L52CE0W?kw1Xz&egb)Lr7roI@K#vx}{Z0l5+ShM7B}@MW zK@QN`$ZBhA0n*WRYJDasY-95nlp$1rf`TGnn~fNul|z>%banwbR!H75*vrD~bpz}w zm*D*y02H>mzK*y7T*nnybZ?aNq*wsz8U%d8G@(YD4V?6Q0VYP=V}Js{d@#!}=8gWq zEgT{}oAvv^*n=4`Di}AOSDbB+V}s2p%m&=Bo)v>%Jh%<=F5F@=QVCl$o091;)$P9fVDsHxUsu;sO91 zkjB<9@YA$n&jOQOcyEX!LbRIhmOBM>u*ksKVmcCD{vxDUp5ZsRCLlsRBx@9|&Lfq_8e|LJ6Ab%D+;R( z#kn|ji>>!7YSk%`B)TO%WSKZXtHoabl7z^4TqBGT4j1h&WfarYl_R{*(J1m-^c>{l zH3Ah2Y2YZ!S|1D0*NA*<7>^NxTeyhDxlzG`iB&9MsAN)pkFCxUWT`#(V6P%g zlsiw6_;+(Ds->L$4_H#Zd6|q@Qm*qi$Wc~3gVd*zxc<&G#_Rnkev!fU41)$EB)Z1{ zV!|^!H#SyR!Fe*cl@is_NB%VvHXjc;4xm9dTYz+}kSkEn31zYdG9&=cVZ62~*R5sMCowJ)6guUnh6so_PuZ z*M1o`QmYBa6VS{A1_hl%oD|rRAb1AI5{|)YY{i-f21n^op?YPhXx+pxV$! z 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%0AP3q`gg*t0bir_>^ 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ɱ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 **Datatype:** positive float (typically below 1). | `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA.
**Datatype:** boolean. | `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points.
**Datatype:** boolean. -| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. E.g. `nu` *Very* broadly, is the percentage of data points that should be considered outliers. `shuffle` is by default false to maintain reprodicibility. But these and all others can be added/changed in this dictionary.
**Datatype:** dictionary. +| `svm_nu` | The `nu` parameter for the support vector machine. *Very* broadly, this is the percentage of data points that should be considered outliers.
**Datatype:** float between 0 and 1. | `stratify_training_data` | This value is used to indicate the stratification of the data. e.g. 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing.
**Datatype:** positive integer. | `indicator_max_period_candles` | The maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN
**Datatype:** positive integer. | `indicator_periods_candles` | A list of integers used to duplicate all indicators according to a set of periods and add them to the feature set.
**Datatype:** list of positive integers. @@ -110,9 +111,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `n_estimators` | A common parameter among regressors which sets the number of boosted trees to fit
**Datatype:** integer. | `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. | `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. -| | **Extraneous parameters** -| `keras` | If your model makes use of keras (typical of Tensorflow based prediction models), activate this flag so that the model save/loading follows keras standards. Default value `false`
**Datatype:** boolean. -| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for `shift` by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. Default value, 2
**Datatype:** integer. ### Important FreqAI dataframe key patterns From 09e5fb2f55cf368b8a3126fb1f00be2f8eb1f613 Mon Sep 17 00:00:00 2001 From: rzrymiak <106121613+rzrymiak@users.noreply.github.com> Date: Sat, 30 Jul 2022 22:37:46 +0000 Subject: [PATCH 025/132] Removed description header --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 828a3d1f9..aa446ad54 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) -## Description - Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) From e6ebc0443ee3d738b29f78ec41f2c8d1c9e82127 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 31 Jul 2022 13:08:43 +0200 Subject: [PATCH 026/132] make single generalized config for freqai. update docs to reflect that. --- .gitignore | 3 +- ...xample.json => config_freqai.example.json} | 6 +- .../config_freqai_spot.example.json | 96 ------------------- docs/freqai.md | 9 +- 4 files changed, 9 insertions(+), 105 deletions(-) rename config_examples/{config_freqai_futures.example.json => config_freqai.example.json} (94%) delete mode 100644 config_examples/config_freqai_spot.example.json diff --git a/.gitignore b/.gitignore index 4498e42ac..73a11b47c 100644 --- a/.gitignore +++ b/.gitignore @@ -111,5 +111,4 @@ target/ !config_examples/config_ftx.example.json !config_examples/config_full.example.json !config_examples/config_kraken.example.json -!config_examples/config_freqai_futures.example.json -!config_examples/config_freqai_spot.example.json +!config_examples/config_freqai.example.json diff --git a/config_examples/config_freqai_futures.example.json b/config_examples/config_freqai.example.json similarity index 94% rename from config_examples/config_freqai_futures.example.json rename to config_examples/config_freqai.example.json index 60e1cce82..e9fc50a4a 100644 --- a/config_examples/config_freqai_futures.example.json +++ b/config_examples/config_freqai.example.json @@ -57,8 +57,7 @@ "train_period_days": 15, "backtest_period_days": 7, "live_retrain_hours": 0, - "identifier": "uniqe-id6", - "live_trained_timestamp": 0, + "identifier": "uniqe-id", "feature_parameters": { "include_timeframes": [ "3m", @@ -84,8 +83,7 @@ "random_state": 1 }, "model_training_parameters": { - "n_estimators": 1000, - "task_type": "CPU" + "n_estimators": 1000 } }, "bot_name": "", diff --git a/config_examples/config_freqai_spot.example.json b/config_examples/config_freqai_spot.example.json deleted file mode 100644 index 1ea6ddecb..000000000 --- a/config_examples/config_freqai_spot.example.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "max_open_trades": 1, - "stake_currency": "USDT", - "stake_amount": 900, - "tradable_balance_ratio": 1, - "fiat_display_currency": "USD", - "dry_run": true, - "timeframe": "5m", - "dry_run_wallet": 4000, - "dataformat_ohlcv": "json", - "cancel_open_orders_on_exit": true, - "unfilledtimeout": { - "entry": 10, - "exit": 30 - }, - "exchange": { - "name": "binance", - "key": "", - "secret": "", - "ccxt_config": { - "enableRateLimit": true - }, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, - "pair_whitelist": [ - "BTC/USDT", - "ETH/USDT" - ], - "pair_blacklist": [] - }, - "entry_pricing": { - "price_side": "same", - "use_order_book": true, - "order_book_top": 1, - "price_last_balance": 0.0, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "exit_pricing": { - "price_side": "other", - "use_order_book": true, - "order_book_top": 1 - }, - "pairlists": [ - { - "method": "StaticPairList" - } - ], - "freqai": { - "startup_candles": 10000, - - "train_period_days": 30, - "backtest_period_days": 7, - "live_retrain_hours": 1, - "identifier": "example", - "live_trained_timestamp": 0, - "feature_parameters": { - "include_timeframes": [ - "5m", - "15m", - "4h" - ], - "include_corr_pairlist": [ - "BTC/USDT", - "ETH/USDT" - ], - "label_period_candles": 500, - "include_shifted_candles": 1, - "DI_threshold": 0, - "weight_factor": 0.9, - "principal_component_analysis": false, - "use_SVM_to_remove_outliers": false, - "stratify_training_data": 0, - "indicator_max_period_candles": 50, - "indicator_periods_candles": [10, 20] - }, - "data_split_parameters": { - "test_size": 0.33, - "random_state": 1 - }, - "model_training_parameters": { - "n_estimators": 1000, - "task_type": "CPU" - } - }, - "bot_name": "", - "initial_state": "running", - "forcebuy_enable": false, - "internals": { - "process_throttle_secs": 5 - } -} diff --git a/docs/freqai.md b/docs/freqai.md index bffa12e5d..de321b787 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -60,7 +60,7 @@ pip install -r requirements-freqai.txt An example strategy, an example prediction model, and example config can all be found in `freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMPredictionModel.py`, -`config_examples/config_freqai_futures.example.json`, respectively. +`config_examples/config_freqai.example.json`, respectively. Assuming the user has downloaded the necessary data, Freqai can be executed from these templates with: @@ -98,7 +98,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](##controlling-the-model-learning-process).
**Datatype:** positive float (typically below 1). | `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA.
**Datatype:** boolean. | `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points.
**Datatype:** boolean. -| `svm_nu` | The `nu` parameter for the support vector machine. *Very* broadly, this is the percentage of data points that should be considered outliers.
**Datatype:** float between 0 and 1. +| `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. E.g. `nu` *Very* broadly, is the percentage of data points that should be considered outliers. `shuffle` is by default false to maintain reprodicibility. But these and all others can be added/changed in this dictionary.
**Datatype:** dictionary. | `stratify_training_data` | This value is used to indicate the stratification of the data. e.g. 2 would set every 2nd data point into a separate dataset to be pulled from during training/testing.
**Datatype:** positive integer. | `indicator_max_period_candles` | The maximum *period* used in `populate_any_indicators()` for indicator creation. FreqAI uses this information in combination with the maximum timeframe to calculate how many data points it should download so that the first data point does not have a NaN
**Datatype:** positive integer. | `indicator_periods_candles` | A list of integers used to duplicate all indicators according to a set of periods and add them to the feature set.
**Datatype:** list of positive integers. @@ -111,6 +111,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `n_estimators` | A common parameter among regressors which sets the number of boosted trees to fit
**Datatype:** integer. | `learning_rate` | A common parameter among regressors which sets the boosting learning rate.
**Datatype:** float. | `n_jobs`, `thread_count`, `task_type` | Different libraries use different parameter names to control the number of threads used for parallel processing or whether or not it is a `task_type` of `gpu` or `cpu`.
**Datatype:** float. +| | **Extraneous parameters** +| `keras` | If your model makes use of keras (typical of Tensorflow based prediction models), activate this flag so that the model save/loading follows keras standards. Default value `false`
**Datatype:** boolean. +| `conv_width` | The width of a convolutional neural network input tensor. This replaces the need for `shift` by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction. Default value, 2
**Datatype:** integer. ### Important FreqAI dataframe key patterns @@ -349,7 +352,7 @@ and adding this to the `train_period_days`. The units need to be in the base can The freqai training/backtesting module can be executed with the following command: ```bash -freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai_futures.example.json --freqaimodel LightGBMPredictionModel --timerange 20210501-20210701 +freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMPredictionModel --timerange 20210501-20210701 ``` If this command has never been executed with the existing config file, then it will train a new model From 61693f6c8bfc5c3e7eb4dc345fea4534ea5edb0a Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 31 Jul 2022 13:20:11 +0200 Subject: [PATCH 027/132] fix tests after changing config_example file --- tests/freqai/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index ff70a40a4..ecd8b2d57 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -49,7 +49,7 @@ def freqai_conf(default_conf, tmpdir): "data_split_parameters": {"test_size": 0.33, "random_state": 1}, "model_training_parameters": {"n_estimators": 100, "verbosity": 0}, }, - "config_files": [Path('config_examples', 'config_freqai_futures.example.json')] + "config_files": [Path('config_examples', 'config_freqai.example.json')] } ) freqaiconf['exchange'].update({'pair_whitelist': ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC']}) @@ -57,7 +57,6 @@ def freqai_conf(default_conf, tmpdir): def get_patched_data_kitchen(mocker, freqaiconf): - # dd = mocker.patch('freqtrade.freqai.data_drawer', MagicMock()) dk = FreqaiDataKitchen(freqaiconf) return dk From a4bada3ebe5ff927503b067545260ef5fe608c87 Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Sun, 31 Jul 2022 17:49:04 +0530 Subject: [PATCH 028/132] Partial exit using average price (#6545) Introduce Partial exits --- docs/strategy-callbacks.md | 67 ++- freqtrade/enums/exittype.py | 1 + freqtrade/exchange/exchange.py | 44 +- freqtrade/freqtradebot.py | 194 +++++--- freqtrade/optimize/backtesting.py | 115 +++-- freqtrade/persistence/migrations.py | 10 +- freqtrade/persistence/trade_model.py | 160 +++++-- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 101 +++-- freqtrade/strategy/interface.py | 19 +- .../subtemplates/strategy_methods_advanced.j2 | 26 +- tests/conftest.py | 3 +- tests/exchange/test_exchange.py | 199 ++++++--- .../test_backtesting_adjust_position.py | 86 ++++ tests/rpc/test_rpc.py | 5 +- tests/rpc/test_rpc_telegram.py | 102 ++++- tests/strategy/strats/strategy_test_v3.py | 9 +- tests/test_freqtradebot.py | 414 ++++++++++++++++-- tests/test_integration.py | 59 ++- tests/test_persistence.py | 193 +++++++- 20 files changed, 1462 insertions(+), 347 deletions(-) mode change 100755 => 100644 freqtrade/optimize/backtesting.py diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 59d221bfc..18de3513b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -629,7 +629,7 @@ class AwesomeStrategy(IStrategy): The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy. For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. -`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). +`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions. `max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys. @@ -637,10 +637,13 @@ The strategy is expected to return a stake_amount (in stake currency) between `m If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. -This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. +This callback is **not** called when there is an open order (either buy or sell) waiting for execution. + `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible. +Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. + +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. @@ -649,12 +652,12 @@ Position adjustments will always be applied in the direction of the trade, so a !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. + Regular stoploss rules still apply (cannot move down). -!!! Warning "/stopbuy" While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. !!! Warning "Backtesting" - During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected. + During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. ``` python from freqtrade.persistence import Trade @@ -675,7 +678,7 @@ class DigDeeperStrategy(IStrategy): max_dca_multiplier = 5.5 # This is called when placing the initial order (opening trade) -def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: @@ -685,22 +688,41 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f return proposed_stake / self.max_dca_multiplier def adjust_trade_position(self, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, min_stake: Optional[float], - max_stake: float, **kwargs): + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. + Custom trade adjustment logic, returning the stake amount that a trade should be + increased or decreased. + This means extra buy or sell orders with additional fees. + Only called when `position_adjustment_enable` is set to True. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param min_stake: Minimal stake size allowed by exchange. - :param max_stake: Balance available for trading. + :param min_stake: Minimal stake size allowed by exchange (for both entries and exits) + :param max_stake: Maximum stake allowed (either through balance, or by exchange limits). + :param current_entry_rate: Current rate using entry pricing. + :param current_exit_rate: Current rate using exit pricing. + :param current_entry_profit: Current profit using entry pricing. + :param current_exit_profit: Current profit using exit pricing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: Stake amount to adjust your trade + :return float: Stake amount to adjust your trade, + Positive values to increase position, Negative values to decrease position. + Return None for no action. """ + if current_profit > 0.05 and trade.nr_of_successful_exits == 0: + # Take half of the profit at +5% + return -(trade.amount / 2) + if current_profit > -0.05: return None @@ -735,6 +757,25 @@ def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: f ``` +### Position adjust calculations + +* Entry rates are calculated using weighted averages. +* Exits will not influence the average entry rate. +* Partial exit relative profit is relative to the average entry price at this point. +* Final exit relative profit is calculated based on the total invested capital. (See example below) + +??? example "Calculation example" + *This example assumes 0 fees for simplicity, and a long position on an imaginary coin.* + + * Buy 100@8\$ + * Buy 100@9\$ -> Avg price: 8.5\$ + * Sell 100@10\$ -> Avg price: 8.5\$, realized profit 150\$, 17.65% + * Buy 150@11\$ -> Avg price: 10\$, realized profit 150\$, 17.65% + * Sell 100@12\$ -> Avg price: 10\$, total realized profit 350\$, 20% + * Sell 150@14\$ -> Avg price: 10\$, total realized profit 950\$, 40% + + The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`). + ## Adjust Entry Price The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles. diff --git a/freqtrade/enums/exittype.py b/freqtrade/enums/exittype.py index 1e15e70cd..b025230ba 100644 --- a/freqtrade/enums/exittype.py +++ b/freqtrade/enums/exittype.py @@ -14,6 +14,7 @@ class ExitType(Enum): FORCE_EXIT = "force_exit" EMERGENCY_EXIT = "emergency_exit" CUSTOM_EXIT = "custom_exit" + PARTIAL_EXIT = "partial_exit" NONE = "" def __str__(self): diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e180c90b2..b6996211f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1507,7 +1507,8 @@ class Exchange: return price_side def get_rate(self, pair: str, refresh: bool, - side: EntryExit, is_short: bool) -> float: + side: EntryExit, is_short: bool, + order_book: Optional[dict] = None, ticker: Optional[dict] = None) -> float: """ Calculates bid/ask target bid rate - between current ask price and last price @@ -1539,22 +1540,24 @@ class Exchange: if conf_strategy.get('use_order_book', False): order_book_top = conf_strategy.get('order_book_top', 1) - order_book = self.fetch_l2_order_book(pair, order_book_top) + if order_book is None: + order_book = self.fetch_l2_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) # top 1 = index 0 try: rate = order_book[f"{price_side}s"][order_book_top - 1][0] except (IndexError, KeyError) as e: logger.warning( - f"{name} Price at location {order_book_top} from orderbook could not be " - f"determined. Orderbook: {order_book}" + f"{pair} - {name} Price at location {order_book_top} from orderbook " + f"could not be determined. Orderbook: {order_book}" ) raise PricingError from e - logger.debug(f"{name} price from orderbook {price_side_word}" + logger.debug(f"{pair} - {name} price from orderbook {price_side_word}" f"side - top {order_book_top} order book {side} rate {rate:.8f}") else: logger.debug(f"Using Last {price_side_word} / Last Price") - ticker = self.fetch_ticker(pair) + if ticker is None: + ticker = self.fetch_ticker(pair) ticker_rate = ticker[price_side] if ticker['last'] and ticker_rate: if side == 'entry' and ticker_rate > ticker['last']: @@ -1571,6 +1574,33 @@ class Exchange: return rate + def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]: + entry_rate = None + exit_rate = None + if not refresh: + entry_rate = self._entry_rate_cache.get(pair) + exit_rate = self._exit_rate_cache.get(pair) + if entry_rate: + logger.debug(f"Using cached buy rate for {pair}.") + if exit_rate: + logger.debug(f"Using cached sell rate for {pair}.") + + entry_pricing = self._config.get('entry_pricing', {}) + exit_pricing = self._config.get('exit_pricing', {}) + order_book = ticker = None + if not entry_rate and entry_pricing.get('use_order_book', False): + order_book_top = max(entry_pricing.get('order_book_top', 1), + exit_pricing.get('order_book_top', 1)) + order_book = self.fetch_l2_order_book(pair, order_book_top) + entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book) + elif not entry_rate: + ticker = self.fetch_ticker(pair) + entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker) + if not exit_rate: + exit_rate = self.get_rate(pair, refresh, 'exit', + is_short, order_book=order_book, ticker=ticker) + return entry_rate, exit_rate + # Fee handling @retrier @@ -1989,7 +2019,7 @@ class Exchange: else: logger.debug( "Fetching trades for pair %s, since %s %s...", - pair, since, + pair, since, '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else '' ) trades = await self._api_async.fetch_trades(pair, since=since, limit=1000) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 50cfb9d7b..757449c8c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -5,6 +5,7 @@ import copy import logging import traceback from datetime import datetime, time, timedelta, timezone +from decimal import Decimal from math import isclose from threading import Lock from typing import Any, Dict, List, Optional, Tuple @@ -525,39 +526,61 @@ class FreqtradeBot(LoggingMixin): If the strategy triggers the adjustment, a new order gets issued. Once that completes, the existing trade is modified to match new data. """ - if self.strategy.max_entry_position_adjustment > -1: - count_of_buys = trade.nr_of_successful_entries - if count_of_buys > self.strategy.max_entry_position_adjustment: - logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") - return - else: - logger.debug("Max adjustment entries is set to unlimited.") - current_rate = self.exchange.get_rate( - trade.pair, side='entry', is_short=trade.is_short, refresh=True) - current_profit = trade.calc_profit_ratio(current_rate) + current_entry_rate, current_exit_rate = self.exchange.get_rates( + trade.pair, True, trade.is_short) - min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, - current_rate, - self.strategy.stoploss) - max_stake_amount = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) + current_entry_profit = trade.calc_profit_ratio(current_entry_rate) + current_exit_profit = trade.calc_profit_ratio(current_exit_rate) + + min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, + current_entry_rate, + self.strategy.stoploss) + min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, + current_exit_rate, + self.strategy.stoploss) + max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate) stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( - trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate, - current_profit=current_profit, min_stake=min_stake_amount, - max_stake=min(max_stake_amount, stake_available)) + trade=trade, + current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, + current_profit=current_entry_profit, min_stake=min_entry_stake, + max_stake=min(max_entry_stake, stake_available), + current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate, + current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit + ) if stake_amount is not None and stake_amount > 0.0: # We should increase our position - self.execute_entry(trade.pair, stake_amount, price=current_rate, + if self.strategy.max_entry_position_adjustment > -1: + count_of_entries = trade.nr_of_successful_entries + if count_of_entries > self.strategy.max_entry_position_adjustment: + logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") + return + else: + logger.debug("Max adjustment entries is set to unlimited.") + self.execute_entry(trade.pair, stake_amount, price=current_entry_rate, trade=trade, is_short=trade.is_short) if stake_amount is not None and stake_amount < 0.0: # We should decrease our position - # TODO: Selling part of the trade not implemented yet. - logger.error(f"Unable to decrease trade position / sell partially" - f" for pair {trade.pair}, feature not implemented.") + amount = abs(float(Decimal(stake_amount) / Decimal(current_exit_rate))) + if amount > trade.amount: + # This is currently ineffective as remaining would become < min tradable + # Fixing this would require checking for 0.0 there - + # if we decide that this callback is allowed to "fully exit" + logger.info( + f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}") + amount = trade.amount + + remaining = (trade.amount - amount) * current_exit_rate + if remaining < min_exit_stake: + logger.info(f'Remaining amount of {remaining} would be too small.') + return + + self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple( + exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount) def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool: """ @@ -731,7 +754,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_enter(trade, order, order_type) + self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust) if pos_adjust: if order_status == 'closed': @@ -740,8 +763,8 @@ class FreqtradeBot(LoggingMixin): else: logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") - # Update fees if order is closed - if order_status == 'closed': + # Update fees if order is non-opened + if order_status in constants.NON_OPEN_EXCHANGE_STATES: self.update_trade_state(trade, order_id, order) return True @@ -830,13 +853,14 @@ class FreqtradeBot(LoggingMixin): return enter_limit_requested, stake_amount, leverage - def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, - fill: bool = False) -> None: + def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None, + fill: bool = False, sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order occurred. """ msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY - open_rate = safe_value_fallback(order, 'average', 'price') + open_rate = order.safe_price + if open_rate is None: open_rate = trade.open_rate @@ -860,15 +884,17 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, + 'amount': order.safe_amount_after_fee, 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': current_rate, + 'sub_trade': sub_trade, } # Send the message self.rpc.send_msg(msg) - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str, + sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order cancel occurred. """ @@ -893,6 +919,7 @@ class FreqtradeBot(LoggingMixin): 'open_date': trade.open_date, 'current_rate': current_rate, 'reason': reason, + 'sub_trade': sub_trade, } # Send the message @@ -1366,16 +1393,22 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = None trade.exit_reason = None cancelled = True + self.wallets.update() else: # TODO: figure out how to handle partially complete sell orders reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] cancelled = False - self.wallets.update() + order_obj = trade.select_order_by_order_id(order['id']) + if not order_obj: + raise DependencyException( + f"Order_obj not found for {order['id']}. This should not have happened.") + + sub_trade = order_obj.amount != trade.amount self._notify_exit_cancel( trade, order_type=self.strategy.order_types['exit'], - reason=reason + reason=reason, order=order_obj, sub_trade=sub_trade ) return cancelled @@ -1416,6 +1449,7 @@ class FreqtradeBot(LoggingMixin): *, exit_tag: Optional[str] = None, ordertype: Optional[str] = None, + sub_trade_amt: float = None, ) -> bool: """ Executes a trade exit for the given trade and limit @@ -1439,7 +1473,7 @@ class FreqtradeBot(LoggingMixin): # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if (self.config['dry_run'] and exit_type == 'stoploss' - and self.strategy.order_types['stoploss_on_exchange']): + and self.strategy.order_types['stoploss_on_exchange']): limit = trade.stoploss_or_liquidation # set custom_exit_price if available @@ -1462,15 +1496,17 @@ class FreqtradeBot(LoggingMixin): # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergency_exit", "market") - amount = self._safe_exit_amount(trade.pair, trade.amount) + amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount) time_in_force = self.strategy.order_time_in_force['exit'] - if (exit_check.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper( - self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, exit_reason=exit_reason, - sell_reason=exit_reason, # sellreason -> compatibility - current_time=datetime.now(timezone.utc))): + if (exit_check.exit_type != ExitType.LIQUIDATION + and not sub_trade_amt + and not strategy_safe_wrapper( + self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, + time_in_force=time_in_force, exit_reason=exit_reason, + sell_reason=exit_reason, # sellreason -> compatibility + current_time=datetime.now(timezone.utc))): logger.info(f"User denied exit for {trade.pair}.") return False @@ -1504,7 +1540,7 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_exit(trade, order_type) + self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj) # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): self.update_trade_state(trade, trade.open_order_id, order) @@ -1512,16 +1548,27 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: + def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, + sub_trade: bool = False, order: Order = None) -> None: """ Sends rpc notification when a sell occurred. """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) + + # second condition is for mypy only; order will always be passed during sub trade + if sub_trade and order is not None: + amount = order.safe_filled if fill else order.amount + profit_rate = order.safe_price + + profit = trade.calc_profit(rate=profit_rate, amount=amount, open_rate=trade.open_rate) + profit_ratio = trade.calc_profit_ratio(profit_rate, amount, trade.open_rate) + else: + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit = trade.calc_profit(rate=profit_rate) + trade.realized_profit + profit_ratio = trade.calc_profit_ratio(profit_rate) + amount = trade.amount gain = "profit" if profit_ratio > 0 else "loss" msg = { @@ -1535,11 +1582,11 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': profit_rate, 'order_type': order_type, - 'amount': trade.amount, + 'amount': amount, 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, + 'close_rate': profit_rate, 'current_rate': current_rate, - 'profit_amount': profit_trade, + 'profit_amount': profit, 'profit_ratio': profit_ratio, 'buy_tag': trade.enter_tag, 'enter_tag': trade.enter_tag, @@ -1547,19 +1594,18 @@ class FreqtradeBot(LoggingMixin): 'exit_reason': trade.exit_reason, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), + 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency'), + 'sub_trade': sub_trade, + 'cumulative_profit': trade.realized_profit, } - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - # Send the message self.rpc.send_msg(msg) - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str, + order: Order, sub_trade: bool = False) -> None: """ Sends rpc notification when a sell cancel occurred. """ @@ -1585,7 +1631,7 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': profit_rate or 0, 'order_type': order_type, - 'amount': trade.amount, + 'amount': order.safe_amount_after_fee, 'open_rate': trade.open_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, @@ -1599,6 +1645,8 @@ class FreqtradeBot(LoggingMixin): 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, + 'sub_trade': sub_trade, + 'stake_amount': trade.stake_amount, } if 'fiat_display_currency' in self.config: @@ -1653,14 +1701,18 @@ class FreqtradeBot(LoggingMixin): self.handle_order_fee(trade, order_obj, order) trade.update_trade(order_obj) - # TODO: is the below necessary? it's already done in update_trade for filled buys - trade.recalc_trade_from_orders() Trade.commit() - if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: + if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES: # If a entry order was closed, force update on stoploss on exchange if order.get('side') == trade.entry_side: trade = self.cancel_stoploss_on_exchange(trade) + if not self.edge: + # TODO: should shorting/leverage be supported by Edge, + # then this will need to be fixed. + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) + if order.get('side') == trade.entry_side or trade.amount > 0: + # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() trade.set_liquidation_price(self.exchange.get_liquidation_price( @@ -1670,24 +1722,30 @@ class FreqtradeBot(LoggingMixin): open_rate=trade.open_rate, is_short=trade.is_short )) - if not self.edge: - # TODO: should shorting/leverage be supported by Edge, - # then this will need to be fixed. - trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) # Updating wallets when order is closed self.wallets.update() - if not trade.is_open: - if send_msg and not stoploss_order and not trade.open_order_id: - self._notify_exit(trade, '', True) - self.handle_protections(trade.pair, trade.trade_direction) - elif send_msg and not trade.open_order_id and not stoploss_order: - # Enter fill - self._notify_enter(trade, order, fill=True) + self.order_close_notify(trade, order_obj, stoploss_order, send_msg) return False + def order_close_notify( + self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool): + """send "fill" notifications""" + + sub_trade = not isclose(order.safe_amount_after_fee, + trade.amount, abs_tol=constants.MATH_CLOSE_PREC) + if order.ft_order_side == trade.exit_side: + # Exit notification + if send_msg and not stoploss_order and not trade.open_order_id: + self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order) + if not trade.is_open: + self.handle_protections(trade.pair, trade.trade_direction) + elif send_msg and not trade.open_order_id and not stoploss_order: + # Enter fill + self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) + def handle_protections(self, pair: str, side: LongShort) -> None: prot_trig = self.protections.stop_per_pair(pair, side=side) if prot_trig: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py old mode 100755 new mode 100644 index 2c6cfb0e9..46774e8a5 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -287,8 +287,8 @@ class Backtesting: if unavailable_pairs: raise OperationalException( - f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. " - "It is therefore impossible to backtest with this pair at the moment.") + f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. " + "It is therefore impossible to backtest with this pair at the moment.") else: self.futures_data = {} @@ -503,16 +503,20 @@ class Backtesting: def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple ) -> LocalTrade: - current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) - min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1) - max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, row[OPEN_IDX]) + current_rate = row[OPEN_IDX] + current_date = row[DATE_IDX].to_pydatetime() + current_profit = trade.calc_profit_ratio(current_rate) + min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1) + max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( trade=trade, # type: ignore[arg-type] - current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], + current_time=current_date, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake, - max_stake=min(max_stake, stake_available)) + max_stake=min(max_stake, stake_available), + current_entry_rate=current_rate, current_exit_rate=current_rate, + current_entry_profit=current_profit, current_exit_profit=current_profit) # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: @@ -523,6 +527,24 @@ class Backtesting: self.wallets.update() return pos_trade + if stake_amount is not None and stake_amount < 0.0: + amount = abs(stake_amount) / current_rate + if amount > trade.amount: + # This is currently ineffective as remaining would become < min tradable + amount = trade.amount + remaining = (trade.amount - amount) * current_rate + if remaining < min_stake: + # Remaining stake is too low to be sold. + return trade + pos_trade = self._exit_trade(trade, row, current_rate, amount) + if pos_trade is not None: + order = pos_trade.orders[-1] + if self._get_order_filled(order.price, row): + order.close_bt_order(current_date, trade) + trade.recalc_trade_from_orders() + self.wallets.update() + return pos_trade + return trade def _get_order_filled(self, rate: float, row: Tuple) -> bool: @@ -602,7 +624,7 @@ class Backtesting: self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, # type: ignore[arg-type] - order_type='limit', + order_type=order_type, amount=trade.amount, rate=close_rate, time_in_force=time_in_force, @@ -613,32 +635,38 @@ class Backtesting: trade.exit_reason = exit_reason - self.order_id_counter += 1 - order = Order( - id=self.order_id_counter, - ft_trade_id=trade.id, - order_date=exit_candle_time, - order_update_date=exit_candle_time, - ft_is_open=True, - ft_pair=trade.pair, - order_id=str(self.order_id_counter), - symbol=trade.pair, - ft_order_side=trade.exit_side, - side=trade.exit_side, - order_type=order_type, - status="open", - price=close_rate, - average=close_rate, - amount=trade.amount, - filled=0, - remaining=trade.amount, - cost=trade.amount * close_rate, - ) - trade.orders.append(order) - return trade - + return self._exit_trade(trade, row, close_rate, trade.amount) return None + def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, + close_rate: float, amount: float = None) -> Optional[LocalTrade]: + self.order_id_counter += 1 + exit_candle_time = sell_row[DATE_IDX].to_pydatetime() + order_type = self.strategy.order_types['exit'] + amount = amount or trade.amount + order = Order( + id=self.order_id_counter, + ft_trade_id=trade.id, + order_date=exit_candle_time, + order_update_date=exit_candle_time, + ft_is_open=True, + ft_pair=trade.pair, + order_id=str(self.order_id_counter), + symbol=trade.pair, + ft_order_side=trade.exit_side, + side=trade.exit_side, + order_type=order_type, + status="open", + price=close_rate, + average=close_rate, + amount=amount, + filled=0, + remaining=amount, + cost=amount * close_rate, + ) + trade.orders.append(order) + return trade + def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() @@ -865,6 +893,8 @@ class Backtesting: # Ignore trade if entry-order did not fill yet continue exit_row = data[pair][-1] + self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount) + trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade) trade.close_date = exit_row[DATE_IDX].to_pydatetime() trade.exit_reason = ExitType.FORCE_EXIT.value @@ -1006,7 +1036,7 @@ class Backtesting: return None return row - def backtest(self, processed: Dict, + def backtest(self, processed: Dict, # noqa: max-complexity: 13 start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, enable_protections: bool = False) -> Dict[str, Any]: @@ -1108,14 +1138,19 @@ class Backtesting: if order and self._get_order_filled(order.price, row): order.close_bt_order(current_time, trade) trade.open_order_id = None - trade.close_date = current_time - trade.close(order.price, show_msg=False) + sub_trade = order.safe_amount_after_fee != trade.amount + if sub_trade: + order.close_bt_order(current_time, trade) + trade.recalc_trade_from_orders() + else: + trade.close_date = current_time + trade.close(order.price, show_msg=False) - # logger.debug(f"{pair} - Backtesting exit {trade}") - open_trade_count -= 1 - open_trades[pair].remove(trade) - LocalTrade.close_bt_trade(trade) - trades.append(trade) + # logger.debug(f"{pair} - Backtesting exit {trade}") + open_trade_count -= 1 + open_trades[pair].remove(trade) + LocalTrade.close_bt_trade(trade) + trades.append(trade) self.wallets.update() self.run_protections( enable_protections, pair, current_time, trade.trade_direction) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 2a8e34cdf..81757a7de 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -95,6 +95,7 @@ def migrate_trades_and_orders_table( exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) strategy = get_column_def(cols, 'strategy', 'null') enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null')) + realized_profit = get_column_def(cols, 'realized_profit', '0.0') trading_mode = get_column_def(cols, 'trading_mode', 'null') @@ -155,7 +156,7 @@ def migrate_trades_and_orders_table( max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, liquidation_price, is_short, - interest_rate, funding_fees + interest_rate, funding_fees, realized_profit ) select id, lower(exchange), pair, {base_currency} base_currency, {stake_currency} stake_currency, @@ -181,7 +182,7 @@ def migrate_trades_and_orders_table( {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price, {is_short} is_short, {interest_rate} interest_rate, - {funding_fees} funding_fees + {funding_fees} funding_fees, {realized_profit} realized_profit from {trade_back_name} """)) @@ -297,8 +298,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # Check if migration necessary # Migrates both trades and orders table! - if not has_column(cols_orders, 'stop_price'): - # if not has_column(cols_trades, 'base_currency'): + # if ('orders' not in previous_tables + # or not has_column(cols_orders, 'stop_price')): + if not has_column(cols_trades, 'realized_profit'): logger.info(f"Running database migration for trades - " f"backup: {table_back_name}, {order_table_bak_name}") migrate_trades_and_orders_table( diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 44e148a0c..fcb84a59a 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -4,13 +4,15 @@ This module contains the class to persist trades into SQLite import logging from datetime import datetime, timedelta, timezone from decimal import Decimal +from math import isclose from typing import Any, Dict, List, Optional from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func) from sqlalchemy.orm import Query, lazyload, relationship -from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort +from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, + BuySell, LongShort) from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest @@ -176,10 +178,9 @@ class Order(_DECL_BASE): self.remaining = 0 self.status = 'closed' self.ft_is_open = False - if (self.ft_order_side == trade.entry_side - and len(trade.select_filled_orders(trade.entry_side)) == 1): + if (self.ft_order_side == trade.entry_side): trade.open_rate = self.price - trade.recalc_open_trade_value() + trade.recalc_trade_from_orders() trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) @staticmethod @@ -237,6 +238,7 @@ class LocalTrade(): trades: List['LocalTrade'] = [] trades_open: List['LocalTrade'] = [] total_profit: float = 0 + realized_profit: float = 0 id: int = 0 @@ -447,6 +449,7 @@ class LocalTrade(): if self.close_date else None), 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, + 'realized_profit': self.realized_profit or 0.0, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, 'close_profit': self.close_profit, # Deprecated @@ -596,14 +599,28 @@ class LocalTrade(): if self.is_open: payment = "SELL" if self.is_short else "BUY" logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') - self.open_order_id = None + # condition to avoid reset value when updating fees + if self.open_order_id == order.order_id: + self.open_order_id = None + else: + logger.warning( + f'Got different open_order_id {self.open_order_id} != {order.order_id}') self.recalc_trade_from_orders() elif order.ft_order_side == self.exit_side: if self.is_open: payment = "BUY" if self.is_short else "SELL" # * On margin shorts, you buy a little bit more than the amount (amount + interest) logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') - self.close(order.safe_price) + # condition to avoid reset value when updating fees + if self.open_order_id == order.order_id: + self.open_order_id = None + else: + logger.warning( + f'Got different open_order_id {self.open_order_id} != {order.order_id}') + if isclose(order.safe_amount_after_fee, self.amount, abs_tol=MATH_CLOSE_PREC): + self.close(order.safe_price) + else: + self.recalc_trade_from_orders() elif order.ft_order_side == 'stoploss': self.stoploss_order_id = None self.close_rate_requested = self.stop_loss @@ -622,11 +639,11 @@ class LocalTrade(): """ self.close_rate = rate self.close_date = self.close_date or datetime.utcnow() - self.close_profit = self.calc_profit_ratio(rate) - self.close_profit_abs = self.calc_profit(rate) + self.close_profit_abs = self.calc_profit(rate) + self.realized_profit self.is_open = False self.exit_order_status = 'closed' self.open_order_id = None + self.recalc_trade_from_orders(is_closing=True) if show_msg: logger.info( 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', @@ -672,12 +689,12 @@ class LocalTrade(): """ return len([o for o in self.orders if o.ft_order_side == self.exit_side]) - def _calc_open_trade_value(self) -> float: + def _calc_open_trade_value(self, amount: float, open_rate: float) -> float: """ Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - open_trade = Decimal(self.amount) * Decimal(self.open_rate) + open_trade = Decimal(amount) * Decimal(open_rate) fees = open_trade * Decimal(self.fee_open) if self.is_short: return float(open_trade - fees) @@ -689,7 +706,7 @@ class LocalTrade(): Recalculate open_trade_value. Must be called whenever open_rate, fee_open is changed. """ - self.open_trade_value = self._calc_open_trade_value() + self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate) def calculate_interest(self) -> Decimal: """ @@ -721,7 +738,7 @@ class LocalTrade(): else: return close_trade - fees - def calc_close_trade_value(self, rate: float) -> float: + def calc_close_trade_value(self, rate: float, amount: float = None) -> float: """ Calculate the Trade's close value including fees :param rate: rate to compare with. @@ -730,96 +747,143 @@ class LocalTrade(): if rate is None and not self.close_rate: return 0.0 - amount = Decimal(self.amount) + amount1 = Decimal(amount or self.amount) trading_mode = self.trading_mode or TradingMode.SPOT if trading_mode == TradingMode.SPOT: - return float(self._calc_base_close(amount, rate, self.fee_close)) + return float(self._calc_base_close(amount1, rate, self.fee_close)) elif (trading_mode == TradingMode.MARGIN): total_interest = self.calculate_interest() if self.is_short: - amount = amount + total_interest - return float(self._calc_base_close(amount, rate, self.fee_close)) + amount1 = amount1 + total_interest + return float(self._calc_base_close(amount1, rate, self.fee_close)) else: # Currency already owned for longs, no need to purchase - return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest) + return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest) elif (trading_mode == TradingMode.FUTURES): funding_fees = self.funding_fees or 0.0 # Positive funding_fees -> Trade has gained from fees. # Negative funding_fees -> Trade had to pay the fees. if self.is_short: - return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees + return float(self._calc_base_close(amount1, rate, self.fee_close)) - funding_fees else: - return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees + return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees else: raise OperationalException( f"{self.trading_mode.value} trading is not yet available using freqtrade") - def calc_profit(self, rate: float) -> float: + def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade :param rate: close rate to compare with. + :param amount: Amount to use for the calculation. Falls back to trade.amount if not set. + :param open_rate: open_rate to use. Defaults to self.open_rate if not provided. :return: profit in stake currency as float """ - close_trade_value = self.calc_close_trade_value(rate) + close_trade_value = self.calc_close_trade_value(rate, amount) + if amount is None or open_rate is None: + open_trade_value = self.open_trade_value + else: + open_trade_value = self._calc_open_trade_value(amount, open_rate) if self.is_short: - profit = self.open_trade_value - close_trade_value + profit = open_trade_value - close_trade_value else: - profit = close_trade_value - self.open_trade_value + profit = close_trade_value - open_trade_value return float(f"{profit:.8f}") - def calc_profit_ratio(self, rate: float) -> float: + def calc_profit_ratio( + self, rate: float, amount: float = None, open_rate: float = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with. + :param amount: Amount to use for the calculation. Falls back to trade.amount if not set. + :param open_rate: open_rate to use. Defaults to self.open_rate if not provided. :return: profit ratio as float """ - close_trade_value = self.calc_close_trade_value(rate) + close_trade_value = self.calc_close_trade_value(rate, amount) + + if amount is None or open_rate is None: + open_trade_value = self.open_trade_value + else: + open_trade_value = self._calc_open_trade_value(amount, open_rate) short_close_zero = (self.is_short and close_trade_value == 0.0) - long_close_zero = (not self.is_short and self.open_trade_value == 0.0) + long_close_zero = (not self.is_short and open_trade_value == 0.0) leverage = self.leverage or 1.0 if (short_close_zero or long_close_zero): return 0.0 else: if self.is_short: - profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage + profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage else: - profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage + profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage return float(f"{profit_ratio:.8f}") - def recalc_trade_from_orders(self): + def recalc_trade_from_orders(self, is_closing: bool = False): + + current_amount = 0.0 + current_stake = 0.0 + total_stake = 0.0 # Total stake after all buy orders (does not subtract!) + avg_price = 0.0 + close_profit = 0.0 + close_profit_abs = 0.0 - total_amount = 0.0 - total_stake = 0.0 for o in self.orders: - if (o.ft_is_open or - (o.ft_order_side != self.entry_side) or - (o.status not in NON_OPEN_EXCHANGE_STATES)): + if o.ft_is_open or not o.filled: continue tmp_amount = o.safe_amount_after_fee - tmp_price = o.average or o.price - if tmp_amount > 0.0 and tmp_price is not None: - total_amount += tmp_amount - total_stake += tmp_price * tmp_amount + tmp_price = o.safe_price - if total_amount > 0: + is_exit = o.ft_order_side != self.entry_side + side = -1 if is_exit else 1 + if tmp_amount > 0.0 and tmp_price is not None: + current_amount += tmp_amount * side + price = avg_price if is_exit else tmp_price + current_stake += price * tmp_amount * side + + if current_amount > 0: + avg_price = current_stake / current_amount + + if is_exit: + # Process partial exits + exit_rate = o.safe_price + exit_amount = o.safe_amount_after_fee + profit = self.calc_profit(rate=exit_rate, amount=exit_amount, open_rate=avg_price) + close_profit_abs += profit + close_profit = self.calc_profit_ratio( + exit_rate, amount=exit_amount, open_rate=avg_price) + if current_amount <= 0: + profit = close_profit_abs + else: + total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) + + if close_profit: + self.close_profit = close_profit + self.realized_profit = close_profit_abs + self.close_profit_abs = profit + + if current_amount > 0: + # Trade is still open # Leverage not updated, as we don't allow changing leverage through DCA at the moment. - self.open_rate = total_stake / total_amount - self.stake_amount = total_stake / (self.leverage or 1.0) - self.amount = total_amount - self.fee_open_cost = self.fee_open * total_stake + self.open_rate = current_stake / current_amount + self.stake_amount = current_stake / (self.leverage or 1.0) + self.amount = current_amount + self.fee_open_cost = self.fee_open * current_stake self.recalc_open_trade_value() if self.stop_loss_pct is not None and self.open_rate is not None: self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) + elif is_closing and total_stake > 0: + # Close profit abs / maximum owned + # Fees are considered as they are part of close_profit_abs + self.close_profit = (close_profit_abs / total_stake) * self.leverage def select_order_by_order_id(self, order_id: str) -> Optional[Order]: """ @@ -841,7 +905,7 @@ class LocalTrade(): """ orders = self.orders if order_side: - orders = [o for o in self.orders if o.ft_order_side == order_side] + orders = [o for o in orders if o.ft_order_side == order_side] if is_open is not None: orders = [o for o in orders if o.ft_is_open == is_open] if len(orders) > 0: @@ -856,9 +920,9 @@ class LocalTrade(): :return: array of Order objects """ return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None)) - and o.ft_is_open is False and - (o.filled or 0) > 0 and - o.status in NON_OPEN_EXCHANGE_STATES] + and o.ft_is_open is False + and o.filled + and o.status in NON_OPEN_EXCHANGE_STATES] def select_filled_or_open_orders(self) -> List['Order']: """ @@ -1023,6 +1087,7 @@ class Trade(_DECL_BASE, LocalTrade): open_trade_value = Column(Float) close_rate: Optional[float] = Column(Float) close_rate_requested = Column(Float) + realized_profit = Column(Float, default=0.0) close_profit = Column(Float) close_profit_abs = Column(Float) stake_amount = Column(Float, nullable=False) @@ -1068,6 +1133,7 @@ class Trade(_DECL_BASE, LocalTrade): def __init__(self, **kwargs): super().__init__(**kwargs) + self.realized_profit = 0 self.recalc_open_trade_value() def delete(self) -> None: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e6948c9e2..9d6696803 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -201,7 +201,7 @@ class RPC: trade_dict = trade.to_json() trade_dict.update(dict( - close_profit=trade.close_profit if trade.close_profit is not None else None, + close_profit=trade.close_profit if not trade.is_open else None, current_rate=current_rate, current_profit=current_profit, # Deprecated current_profit_pct=round(current_profit * 100, 2), # Deprecated diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 121324d90..66192fb16 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -274,7 +274,7 @@ class Telegram(RPCHandler): f"{emoji} *{self._exchange_from_msg(msg)}:*" f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}" f" (#{msg['trade_id']})\n" - ) + ) message += self._add_analyzed_candle(msg['pair']) message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" message += f"*Amount:* `{msg['amount']:.8f}`\n" @@ -315,20 +315,36 @@ class Telegram(RPCHandler): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) msg['profit_extra'] = ( - f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" - f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})") + f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}") else: msg['profit_extra'] = '' + msg['profit_extra'] = ( + f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" + f"{msg['profit_extra']})") is_fill = msg['type'] == RPCMessageType.EXIT_FILL + is_sub_trade = msg.get('sub_trade') + is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') + profit_prefix = ('Sub ' if is_sub_profit + else 'Cumulative ') if is_sub_trade else '' + cp_extra = '' + if is_sub_profit and is_sub_trade: + if self._rpc._fiat_converter: + cp_fiat = self._rpc._fiat_converter.convert_amount( + msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) + cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" + else: + cp_extra = '' + cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \ + f"{msg['stake_currency']}{cp_extra}`)\n" message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{self._add_analyzed_candle(msg['pair'])}" - f"*{'Profit' if is_fill else 'Unrealized Profit'}:* " + f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" + f"{cp_extra}" f"*Enter Tag:* `{msg['enter_tag']}`\n" f"*Exit Reason:* `{msg['exit_reason']}`\n" - f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" f"*Direction:* `{msg['direction']}`\n" f"{msg['leverage_text']}" f"*Amount:* `{msg['amount']:.8f}`\n" @@ -336,11 +352,25 @@ class Telegram(RPCHandler): ) if msg['type'] == RPCMessageType.EXIT: message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - f"*Close Rate:* `{msg['limit']:.8f}`") + f"*Exit Rate:* `{msg['limit']:.8f}`") elif msg['type'] == RPCMessageType.EXIT_FILL: - message += f"*Close Rate:* `{msg['close_rate']:.8f}`" + message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" + if msg.get('sub_trade'): + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + rem = round_coin_value(msg['stake_amount'], msg['stake_currency']) + message += f"\n*Remaining:* `({rem}" + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + + message += ")`" + else: + message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`" return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: @@ -353,7 +383,8 @@ class Telegram(RPCHandler): elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL): msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit' message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* " - f"Cancelling {msg['message_side']} Order for {msg['pair']} " + f"Cancelling {'partial ' if msg.get('sub_trade') else ''}" + f"{msg['message_side']} Order for {msg['pair']} " f"(#{msg['trade_id']}). Reason: {msg['reason']}.") elif msg_type == RPCMessageType.PROTECTION_TRIGGER: @@ -424,7 +455,7 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool): + def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_open: bool): """ Prepare details of trade with entry adjustment enabled """ @@ -433,44 +464,51 @@ class Telegram(RPCHandler): first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): - if not order['ft_is_entry'] or order['is_open'] is True: + if order['is_open'] is True: continue + wording = 'Entry' if order['ft_is_entry'] else 'Exit' + cur_entry_datetime = arrow.get(order["order_filled_date"]) - cur_entry_amount = order["amount"] + cur_entry_amount = order["filled"] or order["amount"] cur_entry_average = order["safe_price"] lines.append(" ") if x == 0: - lines.append(f"*Entry #{x+1}:*") + lines.append(f"*{wording} #{x+1}:*") lines.append( - f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") - lines.append(f"*Average Entry Price:* {cur_entry_average}") + f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + lines.append(f"*Average Price:* {cur_entry_average}") else: sumA = 0 sumB = 0 for y in range(x): - sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"]) - sumB += filled_orders[y]["amount"] + amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] + sumA += amount * filled_orders[y]["safe_price"] + sumB += amount prev_avg_price = sumA / sumB + # TODO: This calculation ignores fees. price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) minus_on_entry = 0 if prev_avg_price: minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price - dur_entry = cur_entry_datetime - arrow.get( - filled_orders[x - 1]["order_filled_date"]) - days = dur_entry.days - hours, remainder = divmod(dur_entry.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit") + lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit") if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append( - f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") - lines.append(f"*Average Entry Price:* {cur_entry_average} " + f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + lines.append(f"*Average {wording} Price:* {cur_entry_average} " f"({price_to_1st_entry:.2%} from 1st entry rate)") - lines.append(f"*Order filled at:* {order['order_filled_date']}") - lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)") + lines.append(f"*Order filled:* {order['order_filled_date']}") + + # TODO: is this really useful? + # dur_entry = cur_entry_datetime - arrow.get( + # filled_orders[x - 1]["order_filled_date"]) + # days = dur_entry.days + # hours, remainder = divmod(dur_entry.seconds, 3600) + # minutes, seconds = divmod(remainder, 60) + # lines.append( + # f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})") return lines @authorized_only @@ -486,7 +524,14 @@ class Telegram(RPCHandler): if context.args and 'table' in context.args: self._status_table(update, context) return + else: + self._status_msg(update, context) + def _status_msg(self, update: Update, context: CallbackContext) -> None: + """ + handler for `/status` and `/status `. + + """ try: # Check if there's at least one numerical ID provided. @@ -529,6 +574,8 @@ class Telegram(RPCHandler): ]) if r['is_open']: + if r.get('realized_profit'): + lines.append("*Realized Profit:* `{realized_profit:.8f}`") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss @@ -546,7 +593,7 @@ class Telegram(RPCHandler): else: lines.append("*Open Order:* `{open_order}`") - lines_detail = self._prepare_entry_details( + lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 824f31258..5e0aba2fe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -463,10 +463,13 @@ class IStrategy(ABC, HyperStrategyMixin): def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. + Custom trade adjustment logic, returning the stake amount that a trade should be + increased or decreased. + This means extra buy or sell orders with additional fees. Only called when `position_adjustment_enable` is set to True. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -477,10 +480,16 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param min_stake: Minimal stake size allowed by exchange. - :param max_stake: Balance available for trading. + :param min_stake: Minimal stake size allowed by exchange (for both entries and exits) + :param max_stake: Maximum stake allowed (either through balance, or by exchange limits). + :param current_entry_rate: Current rate using entry pricing. + :param current_exit_rate: Current rate using exit pricing. + :param current_entry_profit: Current profit using entry pricing. + :param current_exit_profit: Current profit using exit pricing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: Stake amount to adjust your trade + :return float: Stake amount to adjust your trade, + Positive values to increase position, Negative values to decrease position. + Return None for no action. """ return None diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 989f1d37a..488ca2fd7 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -247,12 +247,16 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order', """ return False -def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', - current_rate: float, current_profit: float, min_stake: Optional[float], - max_stake: float, **kwargs) -> 'Optional[float]': +def adjust_trade_position(self, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. + Custom trade adjustment logic, returning the stake amount that a trade should be + increased or decreased. + This means extra buy or sell orders with additional fees. Only called when `position_adjustment_enable` is set to True. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -263,10 +267,16 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime', :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param min_stake: Minimal stake size allowed by exchange. - :param max_stake: Balance available for trading. + :param min_stake: Minimal stake size allowed by exchange (for both entries and exits) + :param max_stake: Maximum stake allowed (either through balance, or by exchange limits). + :param current_entry_rate: Current rate using entry pricing. + :param current_exit_rate: Current rate using exit pricing. + :param current_entry_profit: Current profit using entry pricing. + :param current_exit_profit: Current profit using exit pricing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: Stake amount to adjust your trade + :return float: Stake amount to adjust your trade, + Positive values to increase position, Negative values to decrease position. + Return None for no action. """ return None diff --git a/tests/conftest.py b/tests/conftest.py index ff3e1007f..a02fc4566 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1627,8 +1627,8 @@ def limit_buy_order_open(): 'timestamp': arrow.utcnow().int_timestamp * 1000, 'datetime': arrow.utcnow().isoformat(), 'price': 0.00001099, + 'average': 0.00001099, 'amount': 90.99181073, - 'average': None, 'filled': 0.0, 'cost': 0.0009999, 'remaining': 90.99181073, @@ -2817,6 +2817,7 @@ def limit_buy_order_usdt_open(): 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp * 1000, 'price': 2.00, + 'average': 2.00, 'amount': 30.0, 'filled': 0.0, 'cost': 60.0, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e968b12c2..d73e26683 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -27,6 +27,57 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio'] +get_entry_rate_data = [ + ('other', 20, 19, 10, 0.0, 20), # Full ask side + ('ask', 20, 19, 10, 0.0, 20), # Full ask side + ('ask', 20, 19, 10, 1.0, 10), # Full last side + ('ask', 20, 19, 10, 0.5, 15), # Between ask and last + ('ask', 20, 19, 10, 0.7, 13), # Between ask and last + ('ask', 20, 19, 10, 0.3, 17), # Between ask and last + ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask + ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask + ('ask', 20, 19, 10, None, 20), # price_last_balance missing + ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask + ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask + ('ask', 4, 5, None, 1, 4), # last not available - uses ask + ('ask', 4, 5, None, 0, 4), # last not available - uses ask + ('same', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 0.0, 20), # Full bid side + ('bid', 21, 20, 10, 1.0, 10), # Full last side + ('bid', 21, 20, 10, 0.5, 15), # Between bid and last + ('bid', 21, 20, 10, 0.7, 13), # Between bid and last + ('bid', 21, 20, 10, 0.3, 17), # Between bid and last + ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 21, 20, 10, None, 20), # price_last_balance missing + ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid + ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid + ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid + ('bid', 6, 5, None, 1, 5), # last not available - uses bid + ('bid', 6, 5, None, 0, 5), # last not available - uses bid +] + +get_sell_rate_data = [ + ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side + ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side + ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat + ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid + ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid + ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid + ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('bid', 0.003, 0.002, 0.005, None, 0.002), + ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side + ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side + ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat + ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask + ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask + ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask + ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), + ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), + ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), + ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), + ('ask', 0.006, 1.0, 11.0, None, 0.006), +] + def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): @@ -2360,34 +2411,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): exchange.fetch_l2_order_book(pair='ETH/BTC', limit=50) -@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ - ('other', 20, 19, 10, 0.0, 20), # Full ask side - ('ask', 20, 19, 10, 0.0, 20), # Full ask side - ('ask', 20, 19, 10, 1.0, 10), # Full last side - ('ask', 20, 19, 10, 0.5, 15), # Between ask and last - ('ask', 20, 19, 10, 0.7, 13), # Between ask and last - ('ask', 20, 19, 10, 0.3, 17), # Between ask and last - ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask - ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask - ('ask', 20, 19, 10, None, 20), # price_last_balance missing - ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask - ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask - ('ask', 4, 5, None, 1, 4), # last not available - uses ask - ('ask', 4, 5, None, 0, 4), # last not available - uses ask - ('same', 21, 20, 10, 0.0, 20), # Full bid side - ('bid', 21, 20, 10, 0.0, 20), # Full bid side - ('bid', 21, 20, 10, 1.0, 10), # Full last side - ('bid', 21, 20, 10, 0.5, 15), # Between bid and last - ('bid', 21, 20, 10, 0.7, 13), # Between bid and last - ('bid', 21, 20, 10, 0.3, 17), # Between bid and last - ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid - ('bid', 21, 20, 10, None, 20), # price_last_balance missing - ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid - ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid - ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid - ('bid', 6, 5, None, 1, 5), # last not available - uses bid - ('bid', 6, 5, None, 0, 5), # last not available - uses bid -]) +@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data) def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) @@ -2411,27 +2435,7 @@ def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, assert not log_has("Using cached entry rate for ETH/BTC.", caplog) -@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [ - ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side - ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side - ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat - ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid - ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid - ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid - ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), - ('bid', 0.003, 0.002, 0.005, None, 0.002), - ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side - ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side - ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat - ('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask - ('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask - ('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask - ('ask', 10.0, 11.0, 11.0, 0.0, 10.0), - ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), - ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), - ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), - ('ask', 0.006, 1.0, 11.0, None, 0.006), -]) +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data) def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask, last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) @@ -2481,14 +2485,14 @@ def test_get_ticker_rate_error(mocker, entry, default_conf, caplog, side, is_sho @pytest.mark.parametrize('is_short,side,expected', [ - (False, 'bid', 0.043936), # Value from order_book_l2 fitxure - bids side - (False, 'ask', 0.043949), # Value from order_book_l2 fitxure - asks side - (False, 'other', 0.043936), # Value from order_book_l2 fitxure - bids side - (False, 'same', 0.043949), # Value from order_book_l2 fitxure - asks side - (True, 'bid', 0.043936), # Value from order_book_l2 fitxure - bids side - (True, 'ask', 0.043949), # Value from order_book_l2 fitxure - asks side - (True, 'other', 0.043949), # Value from order_book_l2 fitxure - asks side - (True, 'same', 0.043936), # Value from order_book_l2 fitxure - bids side + (False, 'bid', 0.043936), # Value from order_book_l2 fixture - bids side + (False, 'ask', 0.043949), # Value from order_book_l2 fixture - asks side + (False, 'other', 0.043936), # Value from order_book_l2 fixture - bids side + (False, 'same', 0.043949), # Value from order_book_l2 fixture - asks side + (True, 'bid', 0.043936), # Value from order_book_l2 fixture - bids side + (True, 'ask', 0.043949), # Value from order_book_l2 fixture - asks side + (True, 'other', 0.043949), # Value from order_book_l2 fixture - asks side + (True, 'same', 0.043936), # Value from order_book_l2 fixture - bids side ]) def test_get_exit_rate_orderbook( default_conf, mocker, caplog, is_short, side, expected, order_book_l2): @@ -2521,7 +2525,8 @@ def test_get_exit_rate_orderbook_exception(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError): exchange.get_rate(pair, refresh=True, side="exit", is_short=False) - assert log_has_re(r"Exit Price at location 1 from orderbook could not be determined\..*", + assert log_has_re(rf"{pair} - Exit Price at location 1 from orderbook " + rf"could not be determined\..*", caplog) @@ -2548,6 +2553,84 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short): assert exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) == 0.13 +@pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", get_entry_rate_data) +@pytest.mark.parametrize("side2", ['bid', 'ask']) +@pytest.mark.parametrize("use_order_book", [True, False]) +def test_get_rates_testing_buy(mocker, default_conf, caplog, side, ask, bid, + last, last_ab, expected, + side2, use_order_book, order_book_l2) -> None: + caplog.set_level(logging.DEBUG) + if last_ab is None: + del default_conf['entry_pricing']['price_last_balance'] + else: + default_conf['entry_pricing']['price_last_balance'] = last_ab + default_conf['entry_pricing']['price_side'] = side + default_conf['exit_pricing']['price_side'] = side2 + default_conf['exit_pricing']['use_order_book'] = use_order_book + api_mock = MagicMock() + api_mock.fetch_l2_order_book = order_book_l2 + api_mock.fetch_ticker = MagicMock( + return_value={'ask': ask, 'last': last, 'bid': bid}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + assert exchange.get_rates('ETH/BTC', refresh=True, is_short=False)[0] == expected + assert not log_has("Using cached buy rate for ETH/BTC.", caplog) + + api_mock.fetch_l2_order_book.reset_mock() + api_mock.fetch_ticker.reset_mock() + assert exchange.get_rates('ETH/BTC', refresh=False, is_short=False)[0] == expected + assert log_has("Using cached buy rate for ETH/BTC.", caplog) + assert api_mock.fetch_l2_order_book.call_count == 0 + assert api_mock.fetch_ticker.call_count == 0 + # Running a 2nd time with Refresh on! + caplog.clear() + + assert exchange.get_rates('ETH/BTC', refresh=True, is_short=False)[0] == expected + assert not log_has("Using cached buy rate for ETH/BTC.", caplog) + + assert api_mock.fetch_l2_order_book.call_count == int(use_order_book) + assert api_mock.fetch_ticker.call_count == 1 + + +@pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', get_sell_rate_data) +@pytest.mark.parametrize("side2", ['bid', 'ask']) +@pytest.mark.parametrize("use_order_book", [True, False]) +def test_get_rates_testing_sell(default_conf, mocker, caplog, side, bid, ask, + last, last_ab, expected, + side2, use_order_book, order_book_l2) -> None: + caplog.set_level(logging.DEBUG) + + default_conf['exit_pricing']['price_side'] = side + if last_ab is not None: + default_conf['exit_pricing']['price_last_balance'] = last_ab + + default_conf['entry_pricing']['price_side'] = side2 + default_conf['entry_pricing']['use_order_book'] = use_order_book + api_mock = MagicMock() + api_mock.fetch_l2_order_book = order_book_l2 + api_mock.fetch_ticker = MagicMock( + return_value={'ask': ask, 'last': last, 'bid': bid}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + pair = "ETH/BTC" + + # Test regular mode + rate = exchange.get_rates(pair, refresh=True, is_short=False)[1] + assert not log_has("Using cached sell rate for ETH/BTC.", caplog) + assert isinstance(rate, float) + assert rate == expected + # Use caching + api_mock.fetch_l2_order_book.reset_mock() + api_mock.fetch_ticker.reset_mock() + + rate = exchange.get_rates(pair, refresh=False, is_short=False)[1] + assert rate == expected + assert log_has("Using cached sell rate for ETH/BTC.", caplog) + + assert api_mock.fetch_l2_order_book.call_count == 0 + assert api_mock.fetch_ticker.call_count == 0 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.asyncio async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name): diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index fca9c01b2..2bb7de574 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -1,8 +1,10 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument from copy import deepcopy +from unittest.mock import MagicMock import pandas as pd +import pytest from arrow import Arrow from freqtrade.configuration import TimeRange @@ -87,3 +89,87 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or round(ln.iloc[0]["low"], 6) < round( t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) + + +def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> None: + default_conf['use_exit_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10) + mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + patch_exchange(mocker) + default_conf.update({ + "stake_amount": 100.0, + "dry_run_wallet": 1000.0, + "strategy": "StrategyTestV3" + }) + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + pair = 'XRP/USDT' + row = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 2.1, # Open + 2.2, # High + 1.9, # Low + 2.1, # Close + 1, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_short + '', # enter_tag + '', # exit_tag + ] + trade = backtesting._enter_trade(pair, row=row, direction='long') + trade.orders[0].close_bt_order(row[0], trade) + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 1 + backtesting.strategy.adjust_trade_position = MagicMock(return_value=None) + + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 1 + # Increase position by 100 + backtesting.strategy.adjust_trade_position = MagicMock(return_value=100) + + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 200.0 + assert pytest.approx(trade.amount) == 95.23809524 + assert len(trade.orders) == 2 + + # Reduce by more than amount - no change to trade. + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-500) + + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 200.0 + assert pytest.approx(trade.amount) == 95.23809524 + assert len(trade.orders) == 2 + assert trade.nr_of_successful_entries == 2 + + # Reduce position by 50 + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-100) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 3 + assert trade.nr_of_successful_entries == 2 + assert trade.nr_of_successful_exits == 1 + + # Adjust below minimum + backtesting.strategy.adjust_trade_position = MagicMock(return_value=-99) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) + + assert trade + assert pytest.approx(trade.stake_amount) == 100.0 + assert pytest.approx(trade.amount) == 47.61904762 + assert len(trade.orders) == 3 + assert trade.nr_of_successful_entries == 2 + assert trade.nr_of_successful_exits == 1 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6e19fcaf3..02c62e337 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -111,6 +111,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, + 'realized_profit': 0.0, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -196,6 +197,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + 'realized_profit': 0.0, 'leverage': 1.0, 'interest_rate': 0.0, 'liquidation_price': None, @@ -841,7 +843,8 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: 'side': 'sell', 'amount': amount, 'remaining': amount, - 'filled': 0.0 + 'filled': 0.0, + 'id': trade.orders[0].order_id, } ) msg = rpc._rpc_force_exit('3') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8d244f3fd..98c06c8e9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -272,7 +272,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) assert re.search(r'Average Entry Price', msg) - assert re.search(r'Order filled at', msg) + assert re.search(r'Order filled', msg) assert re.search(r'Close Date:', msg) is None assert re.search(r'Close Profit:', msg) is None @@ -959,6 +959,9 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False, + 'cumulative_profit': 0.0, } == last_msg @@ -1028,6 +1031,9 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False, + 'cumulative_profit': 0.0, } == last_msg @@ -1087,6 +1093,9 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'stake_amount': 0.0009999999999054, + 'sub_trade': False, + 'cumulative_profit': 0.0, } == msg @@ -1437,7 +1446,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) default_conf['pairlists'] = [{'method': 'VolumePairList', - 'number_assets': 4 + 'number_assets': 4 }] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1789,7 +1798,6 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en 'leverage': leverage, 'stake_amount': 0.01465333, 'direction': entered, - # 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': 'USD', 'open_rate': 1.099e-05, @@ -1806,6 +1814,33 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en '*Total:* `(0.01465333 BTC, 180.895 USD)`' ) + msg_mock.reset_mock() + telegram.send_msg({ + 'type': message_type, + 'trade_id': 1, + 'enter_tag': enter_signal, + 'exchange': 'Binance', + 'pair': 'ETH/BTC', + 'leverage': leverage, + 'stake_amount': 0.01465333, + 'sub_trade': True, + 'direction': entered, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + 'open_rate': 1.099e-05, + 'amount': 1333.3333333333335, + 'open_date': arrow.utcnow().shift(hours=-1) + }) + + assert msg_mock.call_args[0][0] == ( + f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Amount:* `1333.33333333`\n' + f"{leverage_text}" + '*Open Rate:* `0.00001099`\n' + '*Total:* `(0.01465333 BTC, 180.895 USD)`' + ) + def test_send_msg_sell_notification(default_conf, mocker) -> None: @@ -1840,14 +1875,53 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Enter Tag:* `buy_signal1`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60.0 min)`\n' '*Direction:* `Long`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1:00:00 (60.0 min)`' ) + msg_mock.reset_mock() + telegram.send_msg({ + 'type': RPCMessageType.EXIT, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'direction': 'Long', + 'gain': 'loss', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'order_type': 'market', + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'cumulative_profit': -0.15746268, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD', + 'enter_tag': 'buy_signal1', + 'exit_reason': ExitType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), + 'close_date': arrow.utcnow(), + 'stake_amount': 0.01, + 'sub_trade': True, + }) + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' + '*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' + '*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n' + '*Enter Tag:* `buy_signal1`\n' + '*Exit Reason:* `stop_loss`\n' + '*Direction:* `Long`\n' + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Current Rate:* `0.00003201`\n' + '*Exit Rate:* `0.00003201`\n' + '*Remaining:* `(0.01 ETH, -24.812 USD)`' + ) + msg_mock.reset_mock() telegram.send_msg({ 'type': RPCMessageType.EXIT, @@ -1871,15 +1945,15 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: }) assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41%`\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' '*Enter Tag:* `buy_signal1`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Direction:* `Long`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount @@ -1954,15 +2028,15 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction, leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n' - '*Profit:* `-57.41%`\n' + '*Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' f'*Enter Tag:* `{enter_signal}`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' f"*Direction:* `{direction}`\n" f"{leverage_text}" '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) @@ -2090,16 +2164,16 @@ def test_send_msg_sell_notification_no_fiat( leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41%`\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\n' f'*Enter Tag:* `{enter_signal}`\n' '*Exit Reason:* `stop_loss`\n' - '*Duration:* `2:35:03 (155.1 min)`\n' f'*Direction:* `{direction}`\n' f'{leverage_text}' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `2:35:03 (155.1 min)`' ) diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 2c7ccbdf2..088ab21d4 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -185,9 +185,12 @@ class StrategyTestV3(IStrategy): return 3.0 - def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, - min_stake: Optional[float], max_stake: float, **kwargs): + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: if current_profit < -0.0075: orders = trade.select_filled_orders(trade.entry_side) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 438a2704c..0b073a062 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -843,8 +843,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, # In case of closed order order['status'] = 'closed' - order['price'] = 10 - order['cost'] = 100 + order['average'] = 10 + order['cost'] = 300 order['id'] = '444' mocker.patch('freqtrade.exchange.Exchange.create_order', @@ -855,7 +855,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, assert trade assert trade.open_order_id is None assert trade.open_rate == 10 - assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) + assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8) assert pytest.approx(trade.liquidation_price) == liq_price # In case of rejected or expired order and partially filled @@ -863,8 +863,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['amount'] = 30.0 order['filled'] = 20.0 order['remaining'] = 10.00 - order['price'] = 0.5 - order['cost'] = 15.0 + order['average'] = 0.5 + order['cost'] = 10.0 order['id'] = '555' mocker.patch('freqtrade.exchange.Exchange.create_order', MagicMock(return_value=order)) @@ -872,9 +872,9 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade = Trade.query.all()[3] trade.is_short = is_short assert trade - assert trade.open_order_id == '555' + assert trade.open_order_id is None assert trade.open_rate == 0.5 - assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8) + assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8) # Test with custom stake order['status'] = 'open' @@ -901,7 +901,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['amount'] = 30.0 * leverage order['filled'] = 0.0 order['remaining'] = 30.0 - order['price'] = 0.5 + order['average'] = 0.5 order['cost'] = 0.0 order['id'] = '66' mocker.patch('freqtrade.exchange.Exchange.create_order', @@ -1083,7 +1083,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': enter_order['id']}, + enter_order, exit_order, ]), get_fee=fee, @@ -1109,20 +1109,20 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # should do nothing and return false trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id == 100 + assert trade.stoploss_order_id == "100" # Third case: when stoploss was set but it was canceled for some reason # should set a stoploss immediately and return False caplog.clear() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) @@ -2039,6 +2039,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit trade = MagicMock() trade.open_order_id = '123' + trade.amount = 123 # Test raise of OperationalException exception mocker.patch( @@ -2352,9 +2353,9 @@ def test_close_trade( trade.is_short = is_short assert trade - oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], 'buy') + oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], trade.enter_side) trade.update_trade(oobj) - oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], 'sell') + oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], trade.exit_side) trade.update_trade(oobj) assert trade.is_open is False @@ -2397,8 +2398,8 @@ def test_manage_open_orders_entry_usercustom( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), - cancel_order_with_result=cancel_order_wr_mock, cancel_order=cancel_order_mock, + cancel_order_with_result=cancel_order_wr_mock, get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) @@ -2446,7 +2447,9 @@ def test_manage_open_orders_entry( ) -> None: old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) - old_order['id'] = open_trade.open_order_id + open_trade.open_order_id = old_order['id'] + order = Order.parse_from_ccxt_object(old_order, 'mocked', 'buy') + open_trade.orders[0] = order limit_buy_cancel = deepcopy(old_order) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) @@ -2637,7 +2640,9 @@ def test_manage_open_orders_exit_usercustom( is_short, open_trade_usdt, caplog ) -> None: default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} - limit_sell_order_old['id'] = open_trade_usdt.open_order_id + open_trade_usdt.open_order_id = limit_sell_order_old['id'] + order = Order.parse_from_ccxt_object(limit_sell_order_old, 'mocked', 'sell') + open_trade_usdt.orders[0] = order if is_short: limit_sell_order_old['side'] = 'buy' open_trade_usdt.is_short = is_short @@ -3250,6 +3255,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3310,6 +3318,9 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3391,6 +3402,9 @@ def test_execute_trade_exit_custom_exit_price( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3459,6 +3473,9 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3690,7 +3707,7 @@ def test_execute_trade_exit_market_order( ) assert not trade.is_open - assert trade.close_profit == profit_ratio + assert pytest.approx(trade.close_profit) == profit_ratio assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-2][0][0] @@ -3718,6 +3735,9 @@ def test_execute_trade_exit_market_order( 'open_date': ANY, 'close_date': ANY, 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), } == last_msg @@ -3789,7 +3809,7 @@ def test_exit_profit_only( 'last': bid }), create_order=MagicMock(side_effect=[ - limit_order_open[eside], + limit_order[eside], {'id': 1234553382}, ]), get_fee=fee, @@ -4081,7 +4101,7 @@ def test_trailing_stop_loss_positive( 'last': enter_price - (-0.01 if is_short else 0.01), }), create_order=MagicMock(side_effect=[ - limit_order_open[eside], + limit_order[eside], {'id': 1234553382}, ]), get_fee=fee, @@ -4632,7 +4652,7 @@ def test_order_book_entry_pricing1(mocker, default_conf_usdt, order_book_l2, exc with pytest.raises(PricingError): freqtrade.exchange.get_rate('ETH/USDT', side="entry", is_short=False, refresh=True) assert log_has_re( - r'Entry Price at location 1 from orderbook could not be determined.', caplog) + r'ETH/USDT - Entry Price at location 1 from orderbook could not be determined.', caplog) else: assert freqtrade.exchange.get_rate( 'ETH/USDT', side="entry", is_short=False, refresh=True) == 0.043935 @@ -4711,8 +4731,9 @@ def test_order_book_exit_pricing( return_value={'bids': [[]], 'asks': [[]]}) with pytest.raises(PricingError): freqtrade.handle_trade(trade) - assert log_has_re(r'Exit Price at location 1 from orderbook could not be determined\..*', - caplog) + assert log_has_re( + r"ETH/USDT - Exit Price at location 1 from orderbook could not be determined\..*", + caplog) def test_startup_state(default_conf_usdt, mocker): @@ -5385,7 +5406,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'status': None, 'price': 9, 'amount': 12, - 'cost': 100, + 'cost': 108, 'ft_is_open': True, 'id': '651', 'order_id': '651' @@ -5480,7 +5501,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert trade.open_order_id is None assert pytest.approx(trade.open_rate) == 9.90909090909 assert trade.amount == 22 - assert trade.stake_amount == 218 + assert pytest.approx(trade.stake_amount) == 218 orders = Order.query.all() assert orders @@ -5533,6 +5554,329 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Make sure the closed order is found as the second order. order = trade.select_order('buy', False) assert order.order_id == '652' + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': 8, + 'average': 8, + 'amount': 15, + 'filled': 15, + 'cost': 120, + 'ft_is_open': False, + 'id': '653', + 'order_id': '653' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=8, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=15) + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.is_open + assert trade.amount == 22 + assert trade.stake_amount == 192.05405405405406 + assert pytest.approx(trade.open_rate) == 8.729729729729 + + orders = Order.query.all() + assert orders + assert len(orders) == 4 + + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '653' + + +def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: + """ + TODO: Should be adjusted to test both long and short + buy 100 @ 11 + sell 50 @ 8 + sell 50 @ 16 + """ + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_wallet(mocker, free=10000) + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 200.0, + "dry_run_wallet": 1000.0, + }) + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + bid = 11 + amount = 100 + buy_rate_mock = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + # Initial buy + closed_successful_buy_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': bid, + 'average': bid, + 'cost': bid * amount, + 'amount': amount, + 'filled': amount, + 'ft_is_open': False, + 'id': '600', + 'order_id': '600' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_successful_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_successful_buy_order)) + assert freqtrade.execute_entry(pair, amount) + # Should create an closed trade with an no open order id + # Order is filled and trade is open + orders = Order.query.all() + assert orders + assert len(orders) == 1 + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == bid + assert trade.stake_amount == bid * amount + + # Assume it does nothing since order is closed and trade is open + freqtrade.update_closed_trades_without_assigned_fees() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == bid + assert trade.stake_amount == bid * amount + assert not trade.fee_updated(trade.entry_side) + + freqtrade.manage_open_orders() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == bid + assert trade.stake_amount == bid * amount + assert not trade.fee_updated(trade.entry_side) + + amount = 50 + ask = 8 + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': ask, + 'average': ask, + 'amount': amount, + 'filled': amount, + 'cost': amount * ask, + 'ft_is_open': False, + 'id': '601', + 'order_id': '601' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=ask, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=amount) + trades: List[Trade] = trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + # Assert trade is as expected (averaged dca) + + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.amount == 50 + assert trade.open_rate == 11 + assert trade.stake_amount == 550 + assert pytest.approx(trade.realized_profit) == -152.375 + assert pytest.approx(trade.close_profit_abs) == -152.375 + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '601' + + amount = 50 + ask = 16 + closed_sell_dca_order_2 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': ask, + 'average': ask, + 'amount': amount, + 'filled': amount, + 'cost': amount * ask, + 'ft_is_open': False, + 'id': '602', + 'order_id': '602' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_2)) + assert freqtrade.execute_trade_exit(trade=trade, limit=ask, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=amount) + # Assert trade is as expected (averaged dca) + + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.amount == 50 + assert trade.open_rate == 11 + assert trade.stake_amount == 550 + # Trade fully realized + assert pytest.approx(trade.realized_profit) == 94.25 + assert pytest.approx(trade.close_profit_abs) == 94.25 + orders = Order.query.all() + assert orders + assert len(orders) == 3 + + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '602' + assert trade.is_open is False + + +@pytest.mark.parametrize('data', [ + ( + # tuple 1 - side amount, price + # tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit + (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), + (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, 336.625, 0.1343142)), # final profit (sum) + ), + ( + (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), + (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 3175.75, 0.9747170)), # final profit + ) +]) +def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 200.0, + "dry_run_wallet": 1000.0, + }) + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_wallet(mocker, free=10000) + freqtrade = FreqtradeBot(default_conf_usdt) + trade = None + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + for idx, (order, result) in enumerate(data): + amount = order[1] + price = order[2] + price_mock = MagicMock(return_value=price) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=price_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + closed_successful_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': order[0], + 'side': order[0], + 'type': 'limit', + 'status': 'closed', + 'price': price, + 'average': price, + 'cost': price * amount, + 'amount': amount, + 'filled': amount, + 'ft_is_open': False, + 'id': f'60{idx}', + 'order_id': f'60{idx}' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_successful_order)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_successful_order)) + if order[0] == 'buy': + assert freqtrade.execute_entry(pair, amount, trade=trade) + else: + assert freqtrade.execute_trade_exit( + trade=trade, limit=price, + exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), + sub_trade_amt=amount) + + orders1 = Order.query.all() + assert orders1 + assert len(orders1) == idx + 1 + + trade = Trade.query.first() + assert trade + if idx < len(data) - 1: + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.amount == result[0] + assert trade.open_rate == result[1] + assert trade.stake_amount == result[2] + assert pytest.approx(trade.realized_profit) == result[3] + assert pytest.approx(trade.close_profit_abs) == result[4] + assert pytest.approx(trade.close_profit) == result[5] + + order_obj = trade.select_order(order[0], False) + assert order_obj.order_id == f'60{idx}' + + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.is_open is False def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None: @@ -5556,9 +5900,25 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca "max_entry_position_adjustment": 0, }) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - + buy_rate_mock = MagicMock(return_value=10) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) create_mock_trades(fee) caplog.set_level(logging.DEBUG) - + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=10) freqtrade.process_open_trade_positions() assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog) + + caplog.clear() + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-10) + freqtrade.process_open_trade_positions() + assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index 83f54becb..40fdb4277 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC -from tests.conftest import get_patched_freqtradebot, patch_get_signal +from tests.conftest import get_patched_freqtradebot, log_has_re, patch_get_signal def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, @@ -455,3 +455,60 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: # Check the 2 filled orders equal the above amount assert pytest.approx(trade.orders[1].amount) == 30.150753768 assert pytest.approx(trade.orders[-1].amount) == 61.538461232 + + +def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> None: + default_conf_usdt['position_adjustment_enable'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + get_min_pair_stake_amount=MagicMock(return_value=10), + ) + + patch_get_signal(freqtrade) + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + assert pytest.approx(trade.amount) == 30.0 + assert trade.open_rate == 2.0 + + # Too small size + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59) + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + assert pytest.approx(trade.amount) == 30.0 + assert log_has_re("Remaining amount of 1.6.* would be too small.", caplog) + + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) + + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_side == 'sell' + assert pytest.approx(trade.stake_amount) == 40.198 + assert pytest.approx(trade.amount) == 20.099 + assert trade.open_rate == 2.0 + assert trade.is_open + caplog.clear() + + # Sell more than what we got (we got ~20 coins left) + # First adjusts the amount to 20 - then rejects. + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) + freqtrade.process() + assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) + assert log_has_re("Remaining amount of 0.0 would be too small.", caplog) + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_side == 'sell' + assert pytest.approx(trade.stake_amount) == 40.198 + assert trade.is_open diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0c1fc01a5..42fcc7413 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -500,7 +500,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ assert trade.close_profit is None assert trade.close_date is None - trade.open_order_id = 'something' + trade.open_order_id = enter_order['id'] oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side) trade.orders.append(oobj) trade.update_trade(oobj) @@ -515,7 +515,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ caplog) caplog.clear() - trade.open_order_id = 'something' + trade.open_order_id = enter_order['id'] time_machine.move_to("2022-03-31 21:45:05 +00:00") oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side) trade.orders.append(oobj) @@ -550,7 +550,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, leverage=1.0, ) - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_market_buy' oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') trade.orders.append(oobj) trade.update_trade(oobj) @@ -565,7 +565,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog.clear() trade.is_open = True - trade.open_order_id = 'something' + trade.open_order_id = 'mocked_market_sell' oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') trade.orders.append(oobj) trade.update_trade(oobj) @@ -630,14 +630,14 @@ def test_calc_open_close_trade_price( trade.open_rate = 2.0 trade.close_rate = 2.2 trade.recalc_open_trade_value() - assert isclose(trade._calc_open_trade_value(), open_value) + assert isclose(trade._calc_open_trade_value(trade.amount, trade.open_rate), open_value) assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value) assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8)) assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio @pytest.mark.usefixtures("init_persistence") -def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): +def test_trade_close(fee): trade = Trade( pair='ADA/USDT', stake_amount=60.0, @@ -815,7 +815,7 @@ def test_calc_open_trade_value( trade.update_trade(oobj) # Buy @ 2.0 # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == result + assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == result @pytest.mark.parametrize( @@ -905,7 +905,7 @@ def test_calc_close_trade_price( ('binance', False, 1, 1.9, 0.003, -3.3209999, -0.055211970, spot, 0), ('binance', False, 1, 2.2, 0.003, 5.6520000, 0.093965087, spot, 0), - # # FUTURES, funding_fee=1 + # FUTURES, funding_fee=1 ('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819, futures, 1), ('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458, futures, 1), ('binance', True, 1, 2.1, 0.0025, -2.3074999, -0.03855472, futures, 1), @@ -1191,6 +1191,11 @@ def test_calc_profit( assert pytest.approx(trade.calc_profit(rate=close_rate)) == round(profit, 8) assert pytest.approx(trade.calc_profit_ratio(rate=close_rate)) == round(profit_ratio, 8) + assert pytest.approx(trade.calc_profit(close_rate, trade.amount, + trade.open_rate)) == round(profit, 8) + assert pytest.approx(trade.calc_profit_ratio(close_rate, trade.amount, + trade.open_rate)) == round(profit_ratio, 8) + def test_migrate_new(mocker, default_conf, fee, caplog): """ @@ -1382,7 +1387,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", caplog) - assert trade.open_trade_value == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value(trade.amount, trade.open_rate) assert trade.close_profit_abs is None orders = trade.orders @@ -1744,6 +1749,7 @@ def test_to_json(fee): 'stake_amount': 0.001, 'trade_duration': None, 'trade_duration_s': None, + 'realized_profit': 0.0, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -1820,6 +1826,7 @@ def test_to_json(fee): 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, + 'realized_profit': 0.0, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -2262,7 +2269,7 @@ def test_update_order_from_ccxt(caplog): 'symbol': 'ADA/USDT', 'type': 'limit', 'price': 1234.5, - 'amount': 20.0, + 'amount': 20.0, 'filled': 9, 'remaining': 11, 'status': 'open', @@ -2421,7 +2428,7 @@ def test_recalc_trade_from_orders(fee): ) assert fee.return_value == 0.0025 - assert trade._calc_open_trade_value() == o1_trade_val + assert trade._calc_open_trade_value(trade.amount, trade.open_rate) == o1_trade_val assert trade.amount == o1_amount assert trade.stake_amount == o1_cost assert trade.open_rate == o1_rate @@ -2533,7 +2540,8 @@ def test_recalc_trade_from_orders(fee): assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val - # Just to make sure sell orders are ignored, let's calculate one more time. + # Just to make sure full sell orders are ignored, let's calculate one more time. + sell1 = Order( ft_order_side='sell', ft_pair=trade.pair, @@ -2695,7 +2703,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_entries == 2 - # Just to make sure exit orders are ignored, let's calculate one more time. + # Reduce position - this will reduce amount again. sell1 = Order( ft_order_side=exit_side, ft_pair=trade.pair, @@ -2706,7 +2714,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): side=exit_side, price=4, average=3, - filled=2, + filled=o1_amount, remaining=1, cost=5, order_date=trade.open_date, @@ -2715,11 +2723,11 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): trade.orders.append(sell1) trade.recalc_trade_from_orders() - assert trade.amount == 2 * o1_amount - assert trade.stake_amount == 2 * o1_amount + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount assert trade.open_rate == o1_rate - assert trade.fee_open_cost == 2 * o1_fee_cost - assert trade.open_trade_value == 2 * o1_trade_val + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val assert trade.nr_of_successful_entries == 2 # Check with 1 order @@ -2743,11 +2751,11 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): trade.recalc_trade_from_orders() # Calling recalc with single initial order should not change anything - assert trade.amount == 3 * o1_amount - assert trade.stake_amount == 3 * o1_amount + assert trade.amount == 2 * o1_amount + assert trade.stake_amount == 2 * o1_amount assert trade.open_rate == o1_rate - assert trade.fee_open_cost == 3 * o1_fee_cost - assert trade.open_trade_value == 3 * o1_trade_val + assert trade.fee_open_cost == 2 * o1_fee_cost + assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_entries == 3 @@ -2815,3 +2823,144 @@ def test_order_to_ccxt(limit_buy_order_open): del raw_order['stopPrice'] del limit_buy_order_open['datetime'] assert raw_order == limit_buy_order_open + + +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('data', [ + { + # tuple 1 - side, amount, price + # tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit + 'orders': [ + (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), + (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -25.0, -25.0, -0.04)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 725.0, 750.0, 0.60)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 350.0, -375.0, -0.60)), + ], + 'end_profit': 350.0, + 'end_profit_ratio': 0.14, + 'fee': 0.0, + }, + { + 'orders': [ + (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), + (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, -377.1875, -0.60199501)), + ], + 'end_profit': 336.625, + 'end_profit_ratio': 0.1343142, + 'fee': 0.0025, + }, + { + 'orders': [ + (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), + (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 1787.25, 1.08048062)), + ], + 'end_profit': 3175.75, + 'end_profit_ratio': 0.9747170, + 'fee': 0.0025, + }, + { + # Test above without fees + 'orders': [ + (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), + (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 600.0, 600.0, 1.2)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 600.0, 600.0, 1.2)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1400.0, 800.0, 0.72727273)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3200.0, 1800.0, 1.09090909)), + ], + 'end_profit': 3200.0, + 'end_profit_ratio': 0.98461538, + 'fee': 0.0, + }, + { + 'orders': [ + (('buy', 100, 8), (100.0, 8.0, 800.0, 0.0, None, None)), + (('buy', 100, 9), (200.0, 8.5, 1700.0, 0.0, None, None)), + (('sell', 100, 10), (100.0, 8.5, 850.0, 150.0, 150.0, 0.17647059)), + (('buy', 150, 11), (250.0, 10, 2500.0, 150.0, 150.0, 0.17647059)), + (('sell', 100, 12), (150.0, 10.0, 1500.0, 350.0, 350.0, 0.2)), + (('sell', 150, 14), (150.0, 10.0, 1500.0, 950.0, 950.0, 0.40)), + ], + 'end_profit': 950.0, + 'end_profit_ratio': 0.283582, + 'fee': 0.0, + }, +]) +def test_recalc_trade_from_orders_dca(data) -> None: + + pair = 'ETH/USDT' + trade = Trade( + id=2, + pair=pair, + stake_amount=1000, + open_rate=data['orders'][0][0][2], + amount=data['orders'][0][0][1], + is_open=True, + open_date=arrow.utcnow().datetime, + fee_open=data['fee'], + fee_close=data['fee'], + exchange='binance', + is_short=False, + leverage=1.0, + trading_mode=TradingMode.SPOT + ) + Trade.query.session.add(trade) + + for idx, (order, result) in enumerate(data['orders']): + amount = order[1] + price = order[2] + + order_obj = Order( + ft_order_side=order[0], + ft_pair=trade.pair, + order_id=f"order_{order[0]}_{idx}", + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side=order[0], + price=price, + average=price, + filled=amount, + remaining=0, + cost=amount * price, + order_date=arrow.utcnow().shift(hours=-10 + idx).datetime, + order_filled_date=arrow.utcnow().shift(hours=-10 + idx).datetime, + ) + trade.orders.append(order_obj) + trade.recalc_trade_from_orders() + Trade.commit() + + orders1 = Order.query.all() + assert orders1 + assert len(orders1) == idx + 1 + + trade = Trade.query.first() + assert trade + assert len(trade.orders) == idx + 1 + if idx < len(data) - 1: + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.amount == result[0] + assert trade.open_rate == result[1] + assert trade.stake_amount == result[2] + # TODO: enable the below. + assert pytest.approx(trade.realized_profit) == result[3] + # assert pytest.approx(trade.close_profit_abs) == result[4] + assert pytest.approx(trade.close_profit) == result[5] + + trade.close(price) + assert pytest.approx(trade.close_profit_abs) == data['end_profit'] + assert pytest.approx(trade.close_profit) == data['end_profit_ratio'] + assert not trade.is_open + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None From cbb05354a8968c10bfcb5944db2c5fa13eb6fd5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jul 2022 15:10:01 +0200 Subject: [PATCH 029/132] Add install variant for freqai --- requirements-freqai.txt | 1 - setup.py | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index c3aa2e4db..060a5219d 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -3,7 +3,6 @@ # Required for freqai scikit-learn==1.1.1 -scikit-optimize==0.9.0 joblib==1.1.0 catboost==1.0.4 lightgbm==3.3.2 diff --git a/setup.py b/setup.py index 7aa56bf81..33e35c5ab 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,13 @@ hyperopt = [ 'progressbar2', ] +freqai = [ + 'scikit-learn', + 'joblib', + 'catboost', + 'lightgbm', +] + develop = [ 'coveralls', 'flake8', @@ -31,7 +38,7 @@ jupyter = [ 'nbconvert', ] -all_extra = plot + develop + jupyter + hyperopt +all_extra = plot + develop + jupyter + hyperopt + freqai setup( tests_require=[ @@ -79,6 +86,7 @@ setup( 'plot': plot, 'jupyter': jupyter, 'hyperopt': hyperopt, + 'freqai': freqai, 'all': all_extra, }, ) From 659870312df456036372a3afa7825220292a3229 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Jul 2022 15:23:27 +0200 Subject: [PATCH 030/132] Use JSON Schema validation for freaAI schema validation --- freqtrade/configuration/config_validation.py | 17 ------------ freqtrade/constants.py | 29 +++++++++----------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 5d9667196..ee846e7e6 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -85,7 +85,6 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) _validate_unlimited_amount(conf) _validate_ask_orderbook(conf) validate_migrated_strategy_settings(conf) - _validate_freqai(conf) # validate configuration before returning logger.info('Validating configuration ...') @@ -164,22 +163,6 @@ def _validate_edge(conf: Dict[str, Any]) -> None: ) -def _validate_freqai(conf: Dict[str, Any]) -> None: - """ - Freqai param validator - """ - - if not conf.get('freqai', {}): - return - - for param in constants.SCHEMA_FREQAI_REQUIRED: - if param not in conf.get('freqai', {}): - if param not in conf.get('freqai', {}).get('feature_parameters', {}): - raise OperationalException( - f'{param} not found in Freqai config' - ) - - def _validate_whitelist(conf: Dict[str, Any]) -> None: """ Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0134dafc0..af2e4748a 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -241,6 +241,7 @@ CONF_SCHEMA = { }, 'exchange': {'$ref': '#/definitions/exchange'}, 'edge': {'$ref': '#/definitions/edge'}, + 'freqai': {'$ref': '#/definitions/freqai'}, 'experimental': { 'type': 'object', 'properties': { @@ -484,20 +485,21 @@ CONF_SCHEMA = { "keras": {"type": "boolean", "default": False}, "conv_width": {"type": "integer", "default": 2}, "train_period_days": {"type": "integer", "default": 0}, - "backtest_period_days": {"type": "float", "default": 7}, - "identifier": {"type": "str", "default": "example"}, + "backtest_period_days": {"type": "number", "default": 7}, + "identifier": {"type": "string", "default": "example"}, "feature_parameters": { "type": "object", "properties": { - "include_corr_pairlist": {"type": "list"}, - "include_timeframes": {"type": "list"}, + "include_corr_pairlist": {"type": "array"}, + "include_timeframes": {"type": "array"}, "label_period_candles": {"type": "integer"}, "include_shifted_candles": {"type": "integer", "default": 0}, - "DI_threshold": {"type": "float", "default": 0}, + "DI_threshold": {"type": "number", "default": 0}, "weight_factor": {"type": "number", "default": 0}, "principal_component_analysis": {"type": "boolean", "default": False}, "use_SVM_to_remove_outliers": {"type": "boolean", "default": False}, }, + "required": ["include_timeframes", "include_corr_pairlist", ] }, "data_split_parameters": { "type": "object", @@ -516,6 +518,12 @@ CONF_SCHEMA = { }, }, }, + "required": ["train_period_days", + "backtest_period_days", + "identifier", + "feature_parameters", + "data_split_parameters", + "model_training_parameters"] }, }, } @@ -560,17 +568,6 @@ SCHEMA_MINIMAL_REQUIRED = [ 'dataformat_trades', ] -SCHEMA_FREQAI_REQUIRED = [ - 'include_timeframes', - 'train_period_days', - 'backtest_period_days', - 'identifier', - 'include_corr_pairlist', - 'feature_parameters', - 'data_split_parameters', - 'model_training_parameters' -] - CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", From 153336d4241b79c8c9195ca19124fe6be56cfb29 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 31 Jul 2022 15:45:15 +0200 Subject: [PATCH 031/132] move corr_pairlist expansion to after expand_pairlist() --- freqtrade/plugins/pairlist/pairlist_helpers.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index ee17c1be9..a07a0f783 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -43,12 +43,10 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], def dynamic_expand_pairlist(config: dict, markets: list) -> List[str]: + expanded_pairs = expand_pairlist(config['pairs'], markets) if config.get('freqai', {}): corr_pairlist = config['freqai']['feature_parameters']['include_corr_pairlist'] - full_pairs = config['pairs'] + [pair for pair in corr_pairlist - if pair not in config['pairs']] - expanded_pairs = expand_pairlist(full_pairs, markets) - else: - expanded_pairs = expand_pairlist(config['pairs'], markets) + expanded_pairs += [pair for pair in corr_pairlist + if pair not in config['pairs']] return expanded_pairs From d830105605863c2c88839d7df864c35b6db7de4f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 31 Jul 2022 17:05:29 +0200 Subject: [PATCH 032/132] *BREAKING CHANGE* remove unnecessary arguments from populate_any_indicators(), accommodate tests --- freqtrade/freqai/data_kitchen.py | 13 +++----- freqtrade/strategy/interface.py | 5 ++- freqtrade/templates/FreqaiExampleStrategy.py | 31 +++++++++++++------ .../strats/freqai_test_multimodel_strat.py | 4 ++- tests/strategy/strats/freqai_test_strat.py | 4 ++- 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 4a936475a..aedf01d11 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -892,29 +892,26 @@ class FreqaiDataKitchen: else: dataframe = base_dataframes[self.config["timeframe"]].copy() - sgi = True + sgi = False for tf in tfs: + if tf == tfs[-1]: + sgi = True # doing this last allows user to use all tf raw prices in labels dataframe = strategy.populate_any_indicators( - pair, pair, dataframe.copy(), tf, informative=base_dataframes[tf], - coin=pair.split("/")[0] + "-", - set_generalized_indicators=sgi, + set_generalized_indicators=sgi ) - sgi = False if pairs: for i in pairs: if pair in i: continue # dont repeat anything from whitelist dataframe = strategy.populate_any_indicators( - pair, i, dataframe.copy(), tf, - informative=corr_dataframes[i][tf], - coin=i.split("/")[0] + "-", + informative=corr_dataframes[i][tf] ) return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 431e67a98..be6447811 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -557,8 +557,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return None - def populate_any_indicators(self, basepair: str, pair: str, df: DataFrame, tf: str, - informative: DataFrame = None, coin: str = "", + def populate_any_indicators(self, pair: str, df: DataFrame, tf: str, + informative: DataFrame = None, set_generalized_indicators: bool = False) -> DataFrame: """ Function designed to automatically generate, name and merge features @@ -570,7 +570,6 @@ class IStrategy(ABC, HyperStrategyMixin): :param df: strategy dataframe which will receive merges from informatives :param tf: timeframe of the dataframe which will modify the feature names :param informative: the dataframe associated with the informative pair - :param coin: the name of the coin which will modify the feature names. """ return df diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 1196405ab..90343cc80 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -65,7 +65,7 @@ class FreqaiExampleStrategy(IStrategy): return informative_pairs def populate_any_indicators( - self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False + self, pair, df, tf, informative=None, set_generalized_indicators=False ): """ Function designed to automatically generate, name and merge features @@ -78,9 +78,10 @@ class FreqaiExampleStrategy(IStrategy): :param df: strategy dataframe which will receive merges from informatives :param tf: timeframe of the dataframe which will modify the feature names :param informative: the dataframe associated with the informative pair - :param coin: the name of the coin which will modify the feature names. """ + coin = pair.split('/')[0] + with self.freqai.lock: if informative is None: informative = self.dp.get_pair_dataframe(pair, tf) @@ -92,11 +93,8 @@ class FreqaiExampleStrategy(IStrategy): informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) - informative[f"{coin}20sma-period_{t}"] = ta.SMA(informative, timeperiod=t) - informative[f"{coin}21ema-period_{t}"] = ta.EMA(informative, timeperiod=t) - informative[f"%-{coin}close_over_20sma-period_{t}"] = ( - informative["close"] / informative[f"{coin}20sma-period_{t}"] - ) + informative[f"{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) + informative[f"{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) @@ -148,8 +146,6 @@ class FreqaiExampleStrategy(IStrategy): df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25 # user adds targets here by prepending them with &- (see convention below) - # If user wishes to use multiple targets, a multioutput prediction model - # needs to be used such as templates/CatboostPredictionMultiModel.py df["&-s_close"] = ( df["close"] .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) @@ -159,6 +155,23 @@ class FreqaiExampleStrategy(IStrategy): - 1 ) + # If user wishes to use multiple targets, they can add more by + # appending more columns with '&'. User should keep in mind that multi targets + # requires a multioutput prediction model such as + # templates/CatboostPredictionMultiModel.py, + + # df["&-s_range"] = ( + # df["close"] + # .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) + # .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) + # .max() + # - + # df["close"] + # .shift(-self.freqai_info["feature_parameters"]["label_period_candles"]) + # .rolling(self.freqai_info["feature_parameters"]["label_period_candles"]) + # .min() + # ) + return df def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/tests/strategy/strats/freqai_test_multimodel_strat.py b/tests/strategy/strats/freqai_test_multimodel_strat.py index 9652e816b..e58086757 100644 --- a/tests/strategy/strats/freqai_test_multimodel_strat.py +++ b/tests/strategy/strats/freqai_test_multimodel_strat.py @@ -62,7 +62,7 @@ class freqai_test_multimodel_strat(IStrategy): return informative_pairs def populate_any_indicators( - self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False + self, pair, df, tf, informative=None, set_generalized_indicators=False ): """ Function designed to automatically generate, name and merge features @@ -79,6 +79,8 @@ class freqai_test_multimodel_strat(IStrategy): :coin: the name of the coin which will modify the feature names. """ + coin = pair.split('/')[0] + with self.freqai.lock: if informative is None: informative = self.dp.get_pair_dataframe(pair, tf) diff --git a/tests/strategy/strats/freqai_test_strat.py b/tests/strategy/strats/freqai_test_strat.py index 8679d4d74..8288228d1 100644 --- a/tests/strategy/strats/freqai_test_strat.py +++ b/tests/strategy/strats/freqai_test_strat.py @@ -62,7 +62,7 @@ class freqai_test_strat(IStrategy): return informative_pairs def populate_any_indicators( - self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False + self, pair, df, tf, informative=None, set_generalized_indicators=False ): """ Function designed to automatically generate, name and merge features @@ -79,6 +79,8 @@ class freqai_test_strat(IStrategy): :coin: the name of the coin which will modify the feature names. """ + coin = pair.split('/')[0] + with self.freqai.lock: if informative is None: informative = self.dp.get_pair_dataframe(pair, tf) From 4e68626bcbced4ac2d7e6a41b6e330802b6bf097 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 31 Jul 2022 17:51:19 +0200 Subject: [PATCH 033/132] ensure convolutional window is prepended for frequi consistency --- freqtrade/freqai/data_drawer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 97cf7607a..6844af0ec 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -238,6 +238,15 @@ class FreqaiDataDrawer: mrv_df["do_predict"] = do_preds + # for keras type models, the conv_window needs to be prepended so + # viewing is correct in frequi + if self.freqai_info.get('keras', False): + n_lost_points = self.freqai_info.get('conv_width', 2) + zeros_df = DataFrame(np.zeros((n_lost_points, len(mrv_df.columns))), + columns=mrv_df.columns) + self.model_return_values[pair] = pd.concat( + [zeros_df, mrv_df], axis=0, ignore_index=True) + def append_model_predictions(self, pair: str, predictions: DataFrame, do_preds: NDArray[np.int_], dk: FreqaiDataKitchen, len_df: int) -> None: From 946d4c7cfcc645f3061b714d6b3579bdb68c820d Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 31 Jul 2022 18:39:46 +0200 Subject: [PATCH 034/132] fix trailing whitespace for flake8 --- freqtrade/freqai/data_drawer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 6844af0ec..f14277bed 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -238,7 +238,7 @@ class FreqaiDataDrawer: mrv_df["do_predict"] = do_preds - # for keras type models, the conv_window needs to be prepended so + # for keras type models, the conv_window needs to be prepended so # viewing is correct in frequi if self.freqai_info.get('keras', False): n_lost_points = self.freqai_info.get('conv_width', 2) From 7a696f58f95839c917d3a95225e86c166fb21afd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:30 +0000 Subject: [PATCH 035/132] Bump ccxt from 1.91.29 to 1.91.52 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.91.29 to 1.91.52. - [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/1.91.29...1.91.52) --- 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 b9e87749d..9aec6af63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.91.29 +ccxt==1.91.52 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From a75fa26caf915d3235b9e0e7bda794a43441a8b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:38 +0000 Subject: [PATCH 036/132] Bump scipy from 1.8.1 to 1.9.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.8.1 to 1.9.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.8.1...v1.9.0) --- updated-dependencies: - dependency-name: scipy 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 94e59ec15..cc659fc50 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.8.1 +scipy==1.9.0 scikit-learn==1.1.1 scikit-optimize==0.9.0 filelock==3.7.1 From b4ded59c63f6c2fa92e6e1722f0bb6cf758589e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:43 +0000 Subject: [PATCH 037/132] Bump flake8 from 4.0.1 to 5.0.1 Bumps [flake8](https://github.com/pycqa/flake8) from 4.0.1 to 5.0.1. - [Release notes](https://github.com/pycqa/flake8/releases) - [Commits](https://github.com/pycqa/flake8/compare/4.0.1...5.0.1) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:development update-type: version-update:semver-major ... 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 3b98e20db..ee7899eeb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -flake8==4.0.1 +flake8==5.0.1 flake8-tidy-imports==4.8.0 mypy==0.971 pre-commit==2.20.0 From 372be542528457a22ce16f5d408551abe170377e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:46 +0000 Subject: [PATCH 038/132] Bump types-requests from 2.28.3 to 2.28.6 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.3 to 2.28.6. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests 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 3b98e20db..d34e551d2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,6 +24,6 @@ nbconvert==6.5.0 # mypy types types-cachetools==5.2.1 types-filelock==3.2.7 -types-requests==2.28.3 +types-requests==2.28.6 types-tabulate==0.8.11 types-python-dateutil==2.8.19 From ed230dd750e48ecc2148f91ea72bb39cf95dac09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:01:52 +0000 Subject: [PATCH 039/132] Bump orjson from 3.7.8 to 3.7.11 Bumps [orjson](https://github.com/ijl/orjson) from 3.7.8 to 3.7.11. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.7.8...3.7.11) --- updated-dependencies: - dependency-name: orjson 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 b9e87749d..7aee44d85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.8 # Properly format api responses -orjson==3.7.8 +orjson==3.7.11 # Notify systemd sdnotify==0.3.2 From 79b650258ee719b5438b37036ef255379aa0fafe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:02:02 +0000 Subject: [PATCH 040/132] Bump urllib3 from 1.26.10 to 1.26.11 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.10 to 1.26.11. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/1.26.11/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.10...1.26.11) --- updated-dependencies: - dependency-name: urllib3 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 b9e87749d..36989119c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ python-telegram-bot==13.13 arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 -urllib3==1.26.10 +urllib3==1.26.11 jsonschema==4.7.2 TA-Lib==0.4.24 technical==1.3.0 From 97064a9ce3bbc25350966a47d1e549f985b076ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 03:13:38 +0000 Subject: [PATCH 041/132] Bump pypa/gh-action-pypi-publish from 1.5.0 to 1.5.1 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.5.0...v1.5.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b077be04..bb5bc209e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -351,7 +351,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.5.0 + uses: pypa/gh-action-pypi-publish@v1.5.1 if: (github.event_name == 'release') with: user: __token__ @@ -359,7 +359,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.5.0 + uses: pypa/gh-action-pypi-publish@v1.5.1 if: (github.event_name == 'release') with: user: __token__ From 3013282dbf83ee06985478cfcf8866b633c5af53 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 1 Aug 2022 05:39:38 +0200 Subject: [PATCH 042/132] remove non-catboost stuff from schema --- freqtrade/constants.py | 11 +++++++---- freqtrade/templates/FreqaiExampleStrategy.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index af2e4748a..d1a009ece 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -498,6 +498,12 @@ CONF_SCHEMA = { "weight_factor": {"type": "number", "default": 0}, "principal_component_analysis": {"type": "boolean", "default": False}, "use_SVM_to_remove_outliers": {"type": "boolean", "default": False}, + "svm_params": {"type": "object", + "properties": { + "shuffle": {"type": "boolean", "default": False}, + "nu": {"type": "number", "default": 0.1} + }, + } }, "required": ["include_timeframes", "include_corr_pairlist", ] }, @@ -511,10 +517,7 @@ CONF_SCHEMA = { "model_training_parameters": { "type": "object", "properties": { - "n_estimators": {"type": "integer", "default": 2000}, - "random_state": {"type": "integer", "default": 1}, - "learning_rate": {"type": "number", "default": 0.02}, - "task_type": {"type": "string", "default": "CPU"}, + "n_estimators": {"type": "integer", "default": 1000} }, }, }, diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 90343cc80..4f632f907 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -93,8 +93,8 @@ class FreqaiExampleStrategy(IStrategy): informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) - informative[f"{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) - informative[f"{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) + informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) + informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) From f3154a4313cb03ea09f3517272b554ccb9b6664a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 04:35:30 +0000 Subject: [PATCH 043/132] Bump jsonschema from 4.7.2 to 4.9.0 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.7.2 to 4.9.0. - [Release notes](https://github.com/python-jsonschema/jsonschema/releases) - [Changelog](https://github.com/python-jsonschema/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/python-jsonschema/jsonschema/compare/v4.7.2...v4.9.0) --- updated-dependencies: - dependency-name: jsonschema 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 69f6d1d24..f6ba2a444 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.2 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.11 -jsonschema==4.7.2 +jsonschema==4.9.0 TA-Lib==0.4.24 technical==1.3.0 tabulate==0.8.10 From 707a4e7c9eee26ec29749465dda9aa12de69c9bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 06:41:08 +0200 Subject: [PATCH 044/132] types-requests bump 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 759ac0a6a..398d09875 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.2.1 - types-filelock==3.2.7 - - types-requests==2.28.3 + - types-requests==2.28.6 - types-tabulate==0.8.11 - types-python-dateutil==2.8.19 # stages: [push] From d75e0a982091bbe99e5f7795ca83fb56c210617d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 06:43:59 +0200 Subject: [PATCH 045/132] Fix Flake8 errors after flake update --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/persistence/trade_model.py | 8 ++++---- tests/edge/test_edge.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 46774e8a5..029946cfb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -595,7 +595,7 @@ class Backtesting: if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): # Checks and adds an exit tag, after checking that the length of the # row has the length for an exit tag column - if( + if ( len(row) > EXIT_TAG_IDX and row[EXIT_TAG_IDX] is not None and len(row[EXIT_TAG_IDX]) > 0 diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 44ac4a5b3..519022db2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -639,7 +639,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - if(tag_type == "enter_tag"): + if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: headers = _get_line_header("TAG", stake_currency, 'Sells') diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index fcb84a59a..19d9361b6 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1300,7 +1300,7 @@ class Trade(_DECL_BASE, LocalTrade): """ filters = [Trade.is_open.is_(False)] - if(pair is not None): + if (pair is not None): filters.append(Trade.pair == pair) enter_tag_perf = Trade.query.with_entities( @@ -1333,7 +1333,7 @@ class Trade(_DECL_BASE, LocalTrade): """ filters = [Trade.is_open.is_(False)] - if(pair is not None): + if (pair is not None): filters.append(Trade.pair == pair) sell_tag_perf = Trade.query.with_entities( @@ -1366,7 +1366,7 @@ class Trade(_DECL_BASE, LocalTrade): """ filters = [Trade.is_open.is_(False)] - if(pair is not None): + if (pair is not None): filters.append(Trade.pair == pair) mix_tag_perf = Trade.query.with_entities( @@ -1386,7 +1386,7 @@ class Trade(_DECL_BASE, LocalTrade): enter_tag = enter_tag if enter_tag is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other" - if(exit_reason is not None and enter_tag is not None): + if (exit_reason is not None and enter_tag is not None): mix_tag = enter_tag + " " + exit_reason i = 0 if not any(item["mix_tag"] == mix_tag for item in return_list): diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index b30d6f998..1b0191fda 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -136,7 +136,7 @@ def test_adjust(mocker, edge_conf): )) pairs = ['A/B', 'C/D', 'E/F', 'G/H'] - assert(edge.adjust(pairs) == ['E/F', 'C/D']) + assert (edge.adjust(pairs) == ['E/F', 'C/D']) def test_stoploss(mocker, edge_conf): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d73e26683..bbe424430 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3810,8 +3810,8 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): since=unix_time ) - assert(isclose(expected_fees, fees_from_datetime)) - assert(isclose(expected_fees, fees_from_unix_time)) + assert (isclose(expected_fees, fees_from_datetime)) + assert (isclose(expected_fees, fees_from_unix_time)) ccxt_exceptionhandlers( mocker, From 895ebbfd1836b3d9964b70422ba221aa084df2f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 07:32:25 +0000 Subject: [PATCH 046/132] Exclude aarch64 from catboost requirements --- docs/freqai.md | 3 +++ requirements-freqai.txt | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index de321b787..adf64dd07 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -56,6 +56,9 @@ Use `pip` to install the prerequisites with: pip install -r requirements-freqai.txt ``` +!!! Note + Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since Catboost does not provide wheels for this platform. + ## Running from the example files An example strategy, an example prediction model, and example config can all be found in diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 060a5219d..e4f4a7c17 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -4,5 +4,5 @@ # Required for freqai scikit-learn==1.1.1 joblib==1.1.0 -catboost==1.0.4 +catboost==1.0.4; platform_machine != 'aarch64' lightgbm==3.3.2 diff --git a/setup.py b/setup.py index 33e35c5ab..dfadbdefa 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ hyperopt = [ freqai = [ 'scikit-learn', 'joblib', - 'catboost', + 'catboost; platform_machine != "aarch64"', 'lightgbm', ] From ae0d6f63fa458cc2435dcfd8704a2258b7a82358 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Aug 2022 19:43:13 +0200 Subject: [PATCH 047/132] Version bump ccxt to 1.91.55 closes #7151 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f6ba2a444..4cb6519b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.1 pandas==1.4.3 pandas-ta==0.3.14b -ccxt==1.91.52 +ccxt==1.91.55 # Pin cryptography for now due to rust build errors with piwheels cryptography==37.0.4 aiohttp==3.8.1 From 95d3009a9531c99802cfa5ce3c9c46ff8010d633 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 2 Aug 2022 20:14:02 +0200 Subject: [PATCH 048/132] give user ability to analyze live trade dataframe inside custom prediction model. Add documentation to explain new functionality --- docs/freqai.md | 40 ++++++++++++++++ freqtrade/freqai/data_drawer.py | 22 +++++++-- freqtrade/freqai/data_kitchen.py | 30 ++++++++++-- freqtrade/freqai/freqai_interface.py | 68 ++++++++++++++++++++++++++-- 4 files changed, 147 insertions(+), 13 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index adf64dd07..0a07f45ef 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -619,6 +619,46 @@ If the user sets this value, FreqAI will initially use the predictions from the and then subsequently begin introducing real prediction data as it is generated. FreqAI will save this historical data to be reloaded if the user stops and restarts with the same `identifier`. +## Extra returns per train + +Users may find that there are some important metrics that they'd like to return to the strategy at the end of each retrain. +Users can include these metrics by assigining them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside their custom prediction +model class. FreqAI takes the `my_new_value` assigned in this dictionary and expands it to fit the return dataframe to the strategy. +The user can then use the value in the strategy with `dataframe['my_new_value']`. An example of how this is already used in FreqAI is +the `&*_mean` and `&*_std` values, which indicate the mean and standard deviation of that particular label during the most recent training. +Another example is shown below if the user wants to use live metrics from the trade databse. + +The user needs to set the standard dictionary in the config so FreqAI can return proper dataframe shapes: + +```json + "freqai": { + "extra_returns_per_train": {"total_profit": 4} + } +``` + +These values will likely be overridden by the user prediction model, but in the case where the user model has yet to set them, or needs +a default initial value - this is the value that will be returned. + +## Analyzing the trade live database + +Users can analyze the live trade database by calling `analyze_trade_database()` in their custom prediction model. FreqAI already has the +database setup in a pandas dataframe and ready to be analyzed. Here is an example usecase: + +```python + def analyze_trade_database(self, dk: FreqaiDataKitchen, pair: str) -> None: + """ + User analyzes the trade database here and returns summary stats which will be passed back + to the strategy for reinforcement learning or for additional adaptive metrics for use + in entry/exit signals. Store these metrics in dk.data['extra_returns_per_train'] and + they will format themselves into the dataframe as an additional column in the user + strategy. User has access to the current trade database in dk.trade_database_df. + """ + total_profit = dk.trade_database_df['close_profit_abs'].sum() + dk.data['extra_returns_per_train']['total_profit'] = total_profit + + return +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FreqAI + + + + + + + + + + + + + + + diff --git a/docs/assets/freqai_logo_no_md.svg b/docs/assets/freqai_logo_no_md.svg deleted file mode 100644 index 62c32217f..000000000 --- a/docs/assets/freqai_logo_no_md.svg +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - FreqAI - - - - - - - - - - - - - diff --git a/docs/freqai.md b/docs/freqai.md index b3682127c..9b2377557 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -1,21 +1,37 @@ -![freqai-logo](assets/freqai_logo_no_md.svg) +![freqai-logo](assets/freqai_doc_logo.svg) # FreqAI -FreqAI is a module designed to automate a variety of tasks associated with -training a predictive model to provide signals based on input features. +FreqAI is a module designed to automate a variety of tasks associated with training a predictive model to generate market forecasts given a set of input features. Among the the features included: -* Create large rich feature sets (10k+ features) based on simple user created strategies. -* Sweep model training and backtesting to simulate consistent model retraining through time. -* Remove outliers automatically from training and prediction sets using a Dissimilarity Index and Support Vector Machines. -* Reduce the dimensionality of the data with Principal Component Analysis. -* Store models to disk to make reloading from a crash fast and easy (and purge obsolete files automatically for sustained dry/live runs). -* Normalize the data automatically in a smart and statistically safe way. -* Automated data download and data handling. -* Clean the incoming data of NaNs in a safe way before training and prediction. -* Retrain live automatically so that the model self-adapts to the market in an unsupervised manner. +* **Self-adaptive retraining**: automatically retrain models during live deployments to self-adapt to the market in an unsupervised manner. +* **Rapid feature engineering**: create large rich feature sets (10k+ features) based on simple user created strategies. +* **High performance**: adaptive retraining occurs on separate thread (or on GPU if available) from inferencing and bot trade operations. Keep newest models and data in memory for rapid inferencing. +* **Realistic backtesting**: emulate self-adaptive retraining with backtesting module that automates past retraining. +* **Modifiable**: use the generalized and robust architecture for incorporating any machine learning library/method available in Python. Seven examples available. +* **Smart outlier removal**: remove outliers automatically from training and prediction sets using a variety of outlier detection techniques. +* **Crash resilience**: automatic model storage to disk to make reloading from a crash fast and easy (and purge obsolete files automatically for sustained dry/live runs). +* **Automated data normalization**: automatically normalize the data automatically in a smart and statistically safe way. +* **Automatic data download**: automatically compute the data download timerange and downloads data accordingly (in live deployments). +* **Clean the incoming data of NaNs in a safe way before training and prediction. +* **Dimensionality reduction**: reduce the size of the training data via Principal Component Analysis. +* **Deploy bot fleets**: set one bot to train models while a fleet of other bots inference into the models and handle trades. + +## Quick start + +The easiest way to quickly test FreqAI is to run it in dry run with the following command + +```bash +freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates +``` + +where the user will see the boot-up process of auto-data downloading, followed by simultaneous training and trading. + +The example strategy, example prediction model, and example config can all be found in +`freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`, +`config_examples/config_freqai.example.json`, respectively. ## General approach @@ -30,7 +46,7 @@ An overview of the algorithm is shown here to help users understand the data pro ## Background and vocabulary **Features** are the quantities with which a model is trained. $X_i$ represents the -vector of all features for a single candle. In Freqai, the user +vector of all features for a single candle. In FreqAI, the user builds the features from anything they can construct in the strategy. **Labels** are the target values with which the weights inside a model are trained @@ -50,7 +66,7 @@ directly influence nodal weights within the model. ## Install prerequisites -Use `pip` to install the prerequisites with: +The normal Freqtrade install process will ask the user if they wish to install `FreqAI` dependencies. The user should reply "yes" to this question if they wish to use FreqAI. If the user did not reply yes, they can manually install these dependencies after the install with: ``` bash pip install -r requirements-freqai.txt @@ -59,18 +75,6 @@ pip install -r requirements-freqai.txt !!! Note Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since Catboost does not provide wheels for this platform. -## Running from the example files - -An example strategy, an example prediction model, and example config can all be found in -`freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`, -`config_examples/config_freqai.example.json`, respectively. - -Assuming the user has downloaded the necessary data, Freqai can be executed from these templates with: - -```bash -freqtrade backtesting --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates --timerange 20220101-20220201 -``` - ## Configuring the bot ### Parameter table @@ -92,13 +96,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `purge_old_models` | Tell FreqAI to delete obsolete models. Otherwise, all historic models will remain on disk. Defaults to `False`.
**Datatype:** boolean. | `expiration_hours` | Ask FreqAI to avoid making predictions if a model is more than `expiration_hours` old. Defaults to 0 which means models never expire.
**Datatype:** positive integer. | | **Feature Parameters** -| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#building-the-feature-set)
**Datatype:** dictionary. +| `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples shown [here](#feature-engineering)
**Datatype:** dictionary. | `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `populate_any_indicators` will be created for each coin in this list, and that set of features is added to the base asset feature set.
**Datatype:** list of assets (strings). | `include_timeframes` | A list of timeframes that all indicators in `populate_any_indicators` will be created for and added as features to the base asset feature set.
**Datatype:** list of timeframes (strings). | `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `populate_any_indicators`, refer to `templates/FreqaiExampleStrategy.py` for detailed usage. The user can create custom labels, making use of this parameter not.
**Datatype:** positive integer. | `include_shifted_candles` | Parameter used to add a sense of temporal recency to flattened regression type input data. `include_shifted_candles` takes all features, duplicates and shifts them by the number indicated by user.
**Datatype:** positive integer. | `DI_threshold` | Activates the Dissimilarity Index for outlier detection when above 0, explained more [here](#removing-outliers-with-the-dissimilarity-index).
**Datatype:** positive float (typically below 1). -| `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](##controlling-the-model-learning-process).
**Datatype:** positive float (typically below 1). +| `weight_factor` | Used to set weights for training data points according to their recency, see details and a figure of how it works [here](#controlling-the-model-learning-process).
**Datatype:** positive float (typically below 1). | `principal_component_analysis` | Ask FreqAI to automatically reduce the dimensionality of the data set using PCA.
**Datatype:** boolean. | `use_SVM_to_remove_outliers` | Ask FreqAI to train a support vector machine to detect and remove outliers from the training data set as well as from incoming data points.
**Datatype:** boolean. | `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. E.g. `nu` *Very* broadly, is the percentage of data points that should be considered outliers. `shuffle` is by default false to maintain reprodicibility. But these and all others can be added/changed in this dictionary.
**Datatype:** dictionary. @@ -133,8 +137,8 @@ Here are the values the user can expect to include/use inside the typical strate ### Example config file -The user interface is isolated to the typical config file. A typical Freqai -config setup includes: +The user interface is isolated to the typical config file. A typical FreqAI +config setup could include: ```json "freqai": { @@ -169,7 +173,7 @@ config setup includes: } ``` -### Building the feature set +### Feature engineering Features are added by the user inside the `populate_any_indicators()` method of the strategy by prepending indicators with `%` and labels are added by prepending `&`. @@ -182,7 +186,7 @@ various configuration parameters which multiply the feature set such as `include ```python def populate_any_indicators( - self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False + self, pair, df, tf, informative=None, set_generalized_indicators=False ): """ Function designed to automatically generate, name and merge features @@ -198,6 +202,8 @@ various configuration parameters which multiply the feature set such as `include :param coin: the name of the coin which will modify the feature names. """ + coint = pair.split('/')[0] + with self.freqai.lock: if informative is None: informative = self.dp.get_pair_dataframe(pair, tf) @@ -265,7 +271,7 @@ various configuration parameters which multiply the feature set such as `include return df ``` -The user of the present example does not want to pass the `bb_lowerband` as a feature to the model, +The user of the present example does not wish to pass the `bb_lowerband` as a feature to the model, and has therefore not prepended it with `%`. The user does, however, wish to pass `bb_width` to the model for training/prediction and has therefore prepended it with `%`. @@ -313,7 +319,7 @@ set will include all the features from `populate_any_indicators` on all the `inc `include_shifted_candles` is another user controlled parameter which indicates the number of previous candles to include in the present feature set. In other words, `include_shifted_candles: 2`, tells -Freqai to include the the past 2 candles for each of the features included in the dataset. +FreqAI to include the the past 2 candles for each of the features included in the dataset. In total, the number of features the present user has created is: @@ -326,12 +332,12 @@ Users define the backtesting timerange with the typical `--timerange` parameter configuration file. `train_period_days` is the duration of the sliding training window, while `backtest_period_days` is the sliding backtesting window, both in number of days (`backtest_period_days` can be a float to indicate sub daily retraining in live/dry mode). In the present example, -the user is asking Freqai to use a training period of 30 days and backtest the subsequent 7 days. +the user is asking FreqAI to use a training period of 30 days and backtest the subsequent 7 days. This means that if the user sets `--timerange 20210501-20210701`, -Freqai will train 8 separate models (because the full range comprises 8 weeks), +FreqAI will train 8 separate models (because the full range comprises 8 weeks), and then backtest the subsequent week associated with each of the 8 training data set timerange months. Users can think of this as a "sliding window" which -emulates Freqai retraining itself once per week in live using the previous +emulates FreqAI retraining itself once per week in live using the previous month of data. In live, the required training data is automatically computed and downloaded. However, in backtesting @@ -349,16 +355,18 @@ and adding this to the `train_period_days`. The units need to be in the base can !!! Note Although fractional `backtest_period_days` is allowed, the user should be ware that the `--timerange` is divided by this value to determine the number of models that FreqAI will need to train in order to backtest the full range. For example, if the user wants to set a `--timerange` of 10 days, and asks for a `backtest_period_days` of 0.1, FreqAI will need to train 100 models per pair to complete the full backtest. This is why it is physically impossible to truly backtest FreqAI adaptive training. The best way to fully test a model is to run it dry and let it constantly train. In this case, backtesting would take the exact same amount of time as a dry run. -## Running Freqai +## Running FreqAI -### Training and backtesting +### Backtesting -The freqai training/backtesting module can be executed with the following command: +The FreqAI backtesting module can be executed with the following command: ```bash freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor --timerange 20210501-20210701 ``` +Backtesting mode requires the user to have the data pre-downloaded (unlike dry/live, where FreqAI automatically downloads the necessary data). The user should be careful to consider that the range of the downloaded data is more than the backtesting range. This is because FreqAI needs data prior to the desired backtesting range in order to train a model to be ready to make predictions on the first candle of the user set backtesting range. More details on how to calculate the data download timerange can be found [here](#deciding-the-sliding-training-window-and-backtesting-duration). + If this command has never been executed with the existing config file, then it will train a new model for each pair, for each backtesting window within the bigger `--timerange`. @@ -374,7 +382,7 @@ for each pair, for each backtesting window within the bigger `--timerange`. ### Building a freqai strategy -The Freqai strategy requires the user to include the following lines of code in the strategy: +The FreqAI strategy requires the user to include the following lines of code in the strategy: ```python @@ -419,21 +427,16 @@ FreqAI includes a the `CatboostClassifier` via the flag `--freqaimodel CatboostC df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') ``` -### Building an IFreqaiModel - -FreqAI has multiple example prediction model based libraries such as `Catboost` regression (`freqai/prediction_models/CatboostRegressor.py`) and `LightGBM` regression. -However, users can customize and create their own prediction models using the `IFreqaiModel` class. -Users are encouraged to inherit `train()` and `predict()` to let them customize various aspects of their training procedures. ### Running the model live -Freqai can be run dry/live using the following command +FreqAI can be run dry/live using the following command ```bash -freqtrade trade --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel ExamplePredictionModel +freqtrade trade --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel LightGBMRegressor ``` -By default, Freqai will not find find any existing models and will start by training a new one +By default, FreqAI will not find find any existing models and will start by training a new one given the user configuration settings. Following training, it will use that model to make predictions on incoming candles until a new model is available. New models are typically generated as often as possible, with FreqAI managing an internal queue of the pairs to try and keep all models equally "young." FreqAI will always use the newest trained model to make predictions on incoming live data. If users do not want FreqAI to retrain new models as often as possible, they can set `live_retrain_hours` to tell FreqAI to wait at least that number of hours before retraining a new model. Additionally, users can set `expired_hours` to tell FreqAI to avoid making predictions on models aged over this number of hours. If the user wishes to start dry/live from a backtested saved model, the user only needs to reuse @@ -446,7 +449,7 @@ the same `identifier` parameter } ``` -In this case, although Freqai will initiate with a +In this case, although FreqAI will initiate with a pre-trained model, it will still check to see how much time has elapsed since the model was trained, and if a full `live_retrain_hours` has elapsed since the end of the loaded model, FreqAI will self retrain. @@ -473,7 +476,7 @@ the user is asking for `labels` that are 24 candles in the future. ### Removing outliers with the Dissimilarity Index The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each -prediction by the model. To do so, Freqai measures the distance between each training +prediction by the model. To do so, FreqAI measures the distance between each training data point and all other training data points: $$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$ @@ -679,6 +682,11 @@ database setup in a pandas dataframe and ready to be analyzed. Here is an exampl return ``` +## Building an IFreqaiModel + +FreqAI has multiple example prediction model based libraries such as `Catboost` regression (`freqai/prediction_models/CatboostRegressor.py`) and `LightGBM` regression. +However, users can customize and create their own prediction models using the `IFreqaiModel` class. +Users are encouraged to inherit `train()` and `predict()` to let them customize various aspects of their training procedures. - ## Additional information ### Common pitfalls diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index adfd68c84..5e64d165d 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -2,9 +2,8 @@ import copy import datetime import logging import shutil -import sqlite3 from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Tuple import numpy as np import numpy.typing as npt @@ -88,20 +87,6 @@ class FreqaiDataKitchen: config["freqai"]["backtest_period_days"], ) - self.database_path: Optional[Path] = None - - if self.live: - db_url = self.config.get('db_url', None) - self.database_path = Path(db_url) - if 'sqlite' not in self.database_path.parts[0]: - self.database_path = None - logger.warning('FreqAI database analyzer only available for sqlite dbs. ' - ' FreqAI will still run, but user cannot use database analyzer.') - else: - self.database_name = Path(*self.database_path.parts[1:]) - - self.trade_database_df: DataFrame = pd.DataFrame() - self.data['extra_returns_per_train'] = self.freqai_config.get('extra_returns_per_train', {}) self.thread_count = self.freqai_config.get("data_kitchen_thread_count", -1) self.train_dates: DataFrame = pd.DataFrame() @@ -1007,13 +992,6 @@ class FreqaiDataKitchen: f = spy.stats.norm.fit(self.data_dictionary["train_labels"][label]) self.data["labels_mean"][label], self.data["labels_std"][label] = f[0], f[1] - # KEEPME incase we want to let user start to grab quantiles. - # upper_q = spy.stats.norm.ppf(self.freqai_config['feature_parameters'][ - # 'target_quantile'], *f) - # lower_q = spy.stats.norm.ppf(1 - self.freqai_config['feature_parameters'][ - # 'target_quantile'], *f) - # self.data["upper_quantile"] = upper_q - # self.data["lower_quantile"] = lower_q return def remove_features_from_df(self, dataframe: DataFrame) -> DataFrame: @@ -1025,181 +1003,3 @@ class FreqaiDataKitchen: col for col in dataframe.columns if not col.startswith("%") or col.startswith("%%") ] return dataframe[to_keep] - - def get_current_trade_database(self) -> None: - - if self.database_path is None: - logger.warning('No trade database found. Skipping analysis.') - return - - data = sqlite3.connect(self.database_name) - query = data.execute("SELECT * From trades") - cols = [column[0] for column in query.description] - df = pd.DataFrame.from_records(data=query.fetchall(), columns=cols) - self.trade_database_df = df.dropna(subset='close_date') - data.close() - - def np_encoder(self, object): - if isinstance(object, np.generic): - return object.item() - - # Functions containing useful data manipulation examples. but not actively in use. - - # Possibly phasing these outlier removal methods below out in favor of - # use_SVM_to_remove_outliers (computationally more efficient and apparently higher performance). - # But these have good data manipulation examples, so keep them commented here for now. - - # def determine_statistical_distributions(self) -> None: - # from fitter import Fitter - - # logger.info('Determining best model for all features, may take some time') - - # def compute_quantiles(ft): - # f = Fitter(self.data_dictionary["train_features"][ft], - # distributions=['gamma', 'cauchy', 'laplace', - # 'beta', 'uniform', 'lognorm']) - # f.fit() - # # f.summary() - # dist = list(f.get_best().items())[0][0] - # params = f.get_best()[dist] - # upper_q = getattr(spy.stats, list(f.get_best().items())[0][0]).ppf(0.999, **params) - # lower_q = getattr(spy.stats, list(f.get_best().items())[0][0]).ppf(0.001, **params) - - # return ft, upper_q, lower_q, dist - - # quantiles_tuple = Parallel(n_jobs=-1)( - # delayed(compute_quantiles)(ft) for ft in self.data_dictionary[ - # 'train_features'].columns) - - # df = pd.DataFrame(quantiles_tuple, columns=['features', 'upper_quantiles', - # 'lower_quantiles', 'dist']) - # self.data_dictionary['upper_quantiles'] = df['upper_quantiles'] - # self.data_dictionary['lower_quantiles'] = df['lower_quantiles'] - - # return - - # def remove_outliers(self, predict: bool) -> None: - # """ - # Remove data that looks like an outlier based on the distribution of each - # variable. - # :params: - # :predict: boolean which tells the function if this is prediction data or - # training data coming in. - # """ - - # lower_quantile = self.data_dictionary["lower_quantiles"].to_numpy() - # upper_quantile = self.data_dictionary["upper_quantiles"].to_numpy() - - # if predict: - - # df = self.data_dictionary["prediction_features"][ - # (self.data_dictionary["prediction_features"] < upper_quantile) - # & (self.data_dictionary["prediction_features"] > lower_quantile) - # ] - # drop_index = pd.isnull(df).any(1) - # self.data_dictionary["prediction_features"].fillna(0, inplace=True) - # drop_index = ~drop_index - # do_predict = np.array(drop_index.replace(True, 1).replace(False, 0)) - - # logger.info( - # "remove_outliers() tossed %s predictions", - # len(do_predict) - do_predict.sum(), - # ) - # self.do_predict += do_predict - # self.do_predict -= 1 - - # else: - - # filter_train_df = self.data_dictionary["train_features"][ - # (self.data_dictionary["train_features"] < upper_quantile) - # & (self.data_dictionary["train_features"] > lower_quantile) - # ] - # drop_index = pd.isnull(filter_train_df).any(1) - # drop_index = drop_index.replace(True, 1).replace(False, 0) - # self.data_dictionary["train_features"] = self.data_dictionary["train_features"][ - # (drop_index == 0) - # ] - # self.data_dictionary["train_labels"] = self.data_dictionary["train_labels"][ - # (drop_index == 0) - # ] - # self.data_dictionary["train_weights"] = self.data_dictionary["train_weights"][ - # (drop_index == 0) - # ] - - # logger.info( - # f'remove_outliers() tossed {drop_index.sum()}' - # f' training points from {len(filter_train_df)}' - # ) - - # # do the same for the test data - # filter_test_df = self.data_dictionary["test_features"][ - # (self.data_dictionary["test_features"] < upper_quantile) - # & (self.data_dictionary["test_features"] > lower_quantile) - # ] - # drop_index = pd.isnull(filter_test_df).any(1) - # drop_index = drop_index.replace(True, 1).replace(False, 0) - # self.data_dictionary["test_labels"] = self.data_dictionary["test_labels"][ - # (drop_index == 0) - # ] - # self.data_dictionary["test_features"] = self.data_dictionary["test_features"][ - # (drop_index == 0) - # ] - # self.data_dictionary["test_weights"] = self.data_dictionary["test_weights"][ - # (drop_index == 0) - # ] - - # logger.info( - # f'remove_outliers() tossed {drop_index.sum()}' - # f' test points from {len(filter_test_df)}' - # ) - - # return - - # def standardize_data(self, data_dictionary: Dict) -> Dict[Any, Any]: - # """ - # standardize all data in the data_dictionary according to the training dataset - # :params: - # :data_dictionary: dictionary containing the cleaned and split training/test data/labels - # :returns: - # :data_dictionary: updated dictionary with standardized values. - # """ - # # standardize the data by training stats - # train_mean = data_dictionary["train_features"].mean() - # train_std = data_dictionary["train_features"].std() - # data_dictionary["train_features"] = ( - # data_dictionary["train_features"] - train_mean - # ) / train_std - # data_dictionary["test_features"] = ( - # data_dictionary["test_features"] - train_mean - # ) / train_std - - # train_labels_std = data_dictionary["train_labels"].std() - # train_labels_mean = data_dictionary["train_labels"].mean() - # data_dictionary["train_labels"] = ( - # data_dictionary["train_labels"] - train_labels_mean - # ) / train_labels_std - # data_dictionary["test_labels"] = ( - # data_dictionary["test_labels"] - train_labels_mean - # ) / train_labels_std - - # for item in train_std.keys(): - # self.data[item + "_std"] = train_std[item] - # self.data[item + "_mean"] = train_mean[item] - - # self.data["labels_std"] = train_labels_std - # self.data["labels_mean"] = train_labels_mean - - # return data_dictionary - - # def standardize_data_from_metadata(self, df: DataFrame) -> DataFrame: - # """ - # Normalizes a set of data using the mean and standard deviation from - # the associated training data. - # :params: - # :df: Dataframe to be standardized - # """ - - # for item in df.keys(): - # df[item] = (df[item] - self.data[item + "_mean"]) / self.data[item + "_std"] - - # return df diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 833fb50d6..0c32a625d 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -491,9 +491,6 @@ class IFreqaiModel(ABC): model = self.train(unfiltered_dataframe, pair, dk) - dk.get_current_trade_database() - self.analyze_trade_database(dk, pair) - self.dd.pair_dict[pair]["trained_timestamp"] = new_trained_timerange.stopts dk.set_new_model_names(pair, new_trained_timerange) self.dd.pair_dict[pair]["first"] = False @@ -612,20 +609,3 @@ class IFreqaiModel(ABC): :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove data (NaNs) or felt uncertain about data (i.e. SVM and/or DI index) """ - - def analyze_trade_database(self, dk: FreqaiDataKitchen, pair: str) -> None: - """ - User analyzes the trade database here and returns summary stats which will be passed back - to the strategy for reinforcement learning or for additional adaptive metrics for use - in entry/exit signals. Store these metrics in dk.data['extra_returns_per_train'] and - they will format themselves into the dataframe as an additional column in the user - strategy. User has access to the current trade database in dk.trade_database_df. - """ - # if dk.trade_database_df.empty: - # logger.warning(f'No trades found for {pair} to analyze DB') - # return - - # total_profit = dk.trade_database_df['close_profit_abs'].sum() - # dk.data['extra_returns_per_train']['total_profit'] = total_profit - - return From f7502bcc923044a60d2bad31d5c3cd25ef12f56d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Aug 2022 11:35:24 +0000 Subject: [PATCH 118/132] slightly update dca_short test --- tests/test_integration.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 40fdb4277..6a11b13f4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -291,7 +291,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, get_fee=fee, - amount_to_precision=lambda s, x, y: y, + amount_to_precision=lambda s, x, y: round(y, 4), price_to_precision=lambda s, x, y: y, ) @@ -303,6 +303,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 assert trade.open_rate == 2.02 + assert trade.orders[0].amount == trade.amount # No adjustment freqtrade.process() trade = Trade.get_trades().first() @@ -331,8 +332,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: trade = Trade.get_trades().first() assert len(trade.orders) == 2 assert pytest.approx(trade.stake_amount) == 120 - # assert trade.orders[0].amount == 30 - assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4) assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.nr_of_successful_entries == 2 @@ -344,7 +344,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.is_open is False # assert trade.orders[0].amount == 30 assert trade.orders[0].side == 'sell' - assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + assert trade.orders[1].amount == round(60 / ticker_usdt_modif['ask'], 4) # Sold everything assert trade.orders[-1].side == 'buy' assert trade.orders[2].amount == trade.amount From dd4e44931e9a909ed5a6d04ab6e51ff26beb37c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Aug 2022 15:02:52 +0000 Subject: [PATCH 119/132] Improve NAN handling in RPC module --- freqtrade/rpc/rpc.py | 25 +++++++++++++++---------- tests/rpc/test_rpc.py | 40 ---------------------------------------- 2 files changed, 15 insertions(+), 50 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9f2c8cf37..ede8a9d3c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -239,12 +239,15 @@ class RPC: trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (PricingError, ExchangeError): current_rate = NAN - if len(trade.select_filled_orders(trade.entry_side)) > 0: - trade_profit = trade.calc_profit(current_rate) - profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' + trade_profit = NAN + profit_str = f'{NAN:.2%}' else: - trade_profit = 0.0 - profit_str = f'{0.0:.2f}' + if trade.nr_of_successful_entries > 0: + trade_profit = trade.calc_profit(current_rate) + profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' + else: + trade_profit = 0.0 + profit_str = f'{0.0:.2f}' direction_str = ('S' if trade.is_short else 'L') if nonspot else '' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( @@ -424,8 +427,6 @@ class RPC: for trade in trades: current_rate: float = 0.0 - if not trade.open_rate: - continue if trade.close_date: durations.append((trade.close_date - trade.open_date).total_seconds()) @@ -447,9 +448,13 @@ class RPC: trade.pair, side='exit', is_short=trade.is_short, refresh=False) except (PricingError, ExchangeError): current_rate = NAN - profit_ratio = trade.calc_profit_ratio(rate=current_rate) - profit_abs = trade.calc_profit( - rate=trade.close_rate or current_rate) + trade.realized_profit + if isnan(current_rate): + profit_ratio = NAN + profit_abs = NAN + else: + profit_ratio = trade.calc_profit_ratio(rate=current_rate) + profit_abs = trade.calc_profit( + rate=trade.close_rate or current_rate) + trade.realized_profit profit_all_coin.append(profit_abs) profit_all_ratio.append(profit_ratio) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4c580c3c2..1a2428fe7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -461,46 +461,6 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert isnan(stats['profit_all_coin']) -# Test that rpc_trade_statistics can handle trades that lacks -# trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee): - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', - return_value=1.1) - mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - get_fee=fee, - ) - - freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) - patch_get_signal(freqtradebot) - stake_currency = default_conf_usdt['stake_currency'] - fiat_display_currency = default_conf_usdt['fiat_display_currency'] - - rpc = RPC(freqtradebot) - - # Create some test data - create_mock_trades_usdt(fee) - - for trade in Trade.query.order_by(Trade.id).all(): - trade.open_rate = None - - stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) - assert stats['profit_closed_coin'] == 0 - assert stats['profit_closed_percent_mean'] == 0 - assert stats['profit_closed_fiat'] == 0 - assert stats['profit_all_coin'] == 0 - assert stats['profit_all_percent_mean'] == 0 - assert stats['profit_all_fiat'] == 0 - assert stats['trade_count'] == 7 - assert stats['first_trade_date'] == '2 days ago' - assert stats['latest_trade_date'] == '17 minutes ago' - assert stats['avg_duration'] == '0:00:00' - assert stats['best_pair'] == 'XRP/USDT' - assert stats['best_rate'] == 10.0 - - def test_rpc_balance_handle_error(default_conf, mocker): mock_balance = { 'BTC': { From de690b0a698f6c65ce1e4cebb6b2fc673df9b3af Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Aug 2022 20:08:40 +0200 Subject: [PATCH 120/132] Use PEP440 compatible versioning --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f8be8f66f..2572c03f1 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2022.8.dev' if 'dev' in __version__: try: From cc885e25ac048cba4df69b8a2bd8787d728dd69d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Aug 2022 20:16:07 +0200 Subject: [PATCH 121/132] Improve NAN Handling in RPC --- freqtrade/rpc/rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ede8a9d3c..ed7f13a96 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -179,8 +179,10 @@ class RPC: else: current_rate = trade.close_rate if len(trade.select_filled_orders(trade.entry_side)) > 0: - current_profit = trade.calc_profit_ratio(current_rate) - current_profit_abs = trade.calc_profit(current_rate) + current_profit = trade.calc_profit_ratio( + current_rate) if not isnan(current_rate) else NAN + current_profit_abs = trade.calc_profit( + current_rate) if not isnan(current_rate) else NAN current_profit_fiat: Optional[float] = None # Calculate fiat profit if self._fiat_converter: From aa1bf2adbd0b0f1796b7ad734123cea173bd43aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Aug 2022 06:43:34 +0200 Subject: [PATCH 122/132] Try fix windows testfailure --- tests/plugins/test_protections.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 4cebb6492..07541735d 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -335,6 +335,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, min_ago_open=110, min_ago_close=20, profit_rate=0.8, )) + Trade.commit() # Locks due to 2nd trade assert freqtrade.protections.global_stop() != only_per_side @@ -342,6 +343,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): assert PairLocks.is_pair_locked('XRP/BTC', side='long') assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side assert not PairLocks.is_global_lock() + Trade.commit() @pytest.mark.usefixtures("init_persistence") From d93bb82193fed27d255bb5386e2c6b31c7702bca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Aug 2022 08:19:40 +0200 Subject: [PATCH 123/132] Add more Commits to failing test --- tests/plugins/test_protections.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 07541735d..7a21b2ed6 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -305,6 +305,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) + Trade.commit() # Not locked with 1 trade assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') @@ -316,6 +317,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) + Trade.commit() # Not locked with 1 trade (first trade is outside of lookback_period) assert not freqtrade.protections.global_stop() assert not freqtrade.protections.stop_per_pair('XRP/BTC') @@ -327,6 +329,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True )) + Trade.commit() assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side assert not PairLocks.is_pair_locked('XRP/BTC', side='*') assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side From b427c7ff136fb017d75eb5f59b8adf98dfb306ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Aug 2022 07:28:19 +0000 Subject: [PATCH 124/132] Use diff. close time to avoid buggy behavior --- tests/plugins/test_protections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 7a21b2ed6..8a5356b3e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -336,7 +336,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): Trade.query.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=110, min_ago_close=20, profit_rate=0.8, + min_ago_open=110, min_ago_close=21, profit_rate=0.8, )) Trade.commit() From fb4b73ce8961dd9db34ad5b94d7484ca80ffc06b Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 12 Aug 2022 12:03:44 +0200 Subject: [PATCH 125/132] ensure dates are saved --- freqtrade/freqai/data_kitchen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 5e64d165d..0cff9c90e 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -202,7 +202,7 @@ class FreqaiDataKitchen: labels = unfiltered_dataframe.filter(label_list, axis=1) drop_index_labels = pd.isnull(labels).any(1) drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0) - dates = unfiltered_dataframe.filter('date', axis=1) + dates = unfiltered_dataframe['date'] filtered_dataframe = filtered_dataframe[ (drop_index == 0) & (drop_index_labels == 0) ] # dropping values From f6545ebdb8a82cedce065779873d395631aff07f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:10:03 +0200 Subject: [PATCH 126/132] Disallow backtesting with --strategy-list for now. --- freqtrade/optimize/backtesting.py | 3 +++ tests/freqai/test_freqai_backtsting.py | 33 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/freqai/test_freqai_backtsting.py diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 86c1822f7..c00f30686 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,6 +89,9 @@ class Backtesting: self.dataprovider = DataProvider(self.config, self.exchange) if self.config.get('strategy_list'): + if self.config.get('freqai'): + raise OperationalException( + "You can't use strategy_list and freqai at the same time.") for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat diff --git a/tests/freqai/test_freqai_backtsting.py b/tests/freqai/test_freqai_backtsting.py new file mode 100644 index 000000000..bb0a71e2c --- /dev/null +++ b/tests/freqai/test_freqai_backtsting.py @@ -0,0 +1,33 @@ +from pathlib import Path +from unittest.mock import PropertyMock + +import pytest + +from freqtrade.commands.optimize_commands import start_backtesting +from freqtrade.exceptions import OperationalException +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, patch_exchange, + patched_configuration_load_config_file) + + +def test_backtest_start_backtest_list_freqai(freqai_conf, mocker, testdatadir): + # Tests detail-data loading + patch_exchange(mocker) + + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT'])) + # mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) + + patched_configuration_load_config_file(mocker, freqai_conf) + + args = [ + 'backtesting', + '--config', 'config.json', + '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), + '--timeframe', '1h', + '--strategy-list', CURRENT_TEST_STRATEGY + ] + args = get_args(args) + with pytest.raises(OperationalException, + match=r"You can't use strategy_list and freqai at the same time\."): + start_backtesting(args) From 49989012abae22740a6a65ccc5ba2685c036aa91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:20:58 +0200 Subject: [PATCH 127/132] Bump catboost requirement to latest --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 5e67a5539..26e4617af 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -4,5 +4,5 @@ # Required for freqai scikit-learn==1.1.2 joblib==1.1.0 -catboost==1.0.4; platform_machine != 'aarch64' +catboost==1.0.6; platform_machine != 'aarch64' lightgbm==3.3.2 From 3b827ee60a6f9194c825a7ee472bfbc4815e4fe6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:24:04 +0200 Subject: [PATCH 128/132] Add "freqai.enabled" flag to disable freqAI via config flag aligns with how other optional modules work in freqtrade. --- config_examples/config_freqai.example.json | 1 + freqtrade/constants.py | 16 ++++++++++------ freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/plugins/pairlist/pairlist_helpers.py | 2 +- freqtrade/strategy/interface.py | 2 +- tests/freqai/conftest.py | 1 + 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index e9fc50a4a..aeb1cb13d 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -52,6 +52,7 @@ } ], "freqai": { + "enabled": true, "startup_candles": 10000, "purge_old_models": true, "train_period_days": 15, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b5d6b4b8e..ddbc84fa9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -486,6 +486,7 @@ CONF_SCHEMA = { "freqai": { "type": "object", "properties": { + "enabled": {"type": "boolean", "default": False}, "keras": {"type": "boolean", "default": False}, "conv_width": {"type": "integer", "default": 2}, "train_period_days": {"type": "integer", "default": 0}, @@ -525,12 +526,15 @@ CONF_SCHEMA = { }, }, }, - "required": ["train_period_days", - "backtest_period_days", - "identifier", - "feature_parameters", - "data_split_parameters", - "model_training_parameters"] + "required": [ + "enabled", + "train_period_days", + "backtest_period_days", + "identifier", + "feature_parameters", + "data_split_parameters", + "model_training_parameters" + ] }, }, } diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c00f30686..7691b96f1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,7 +89,7 @@ class Backtesting: self.dataprovider = DataProvider(self.config, self.exchange) if self.config.get('strategy_list'): - if self.config.get('freqai'): + if self.config.get('freqai', {}).get('enabled', False): raise OperationalException( "You can't use strategy_list and freqai at the same time.") for strat in list(self.config['strategy_list']): @@ -210,7 +210,7 @@ class Backtesting: """ self.progress.init_step(BacktestState.DATALOAD, 1) - if self.config.get('freqai') is not None: + if self.config.get('freqai', {}).get('enabled', False): startup_candles = int(self.config.get('freqai', {}).get('startup_candles', 0)) if not startup_candles: raise OperationalException('FreqAI backtesting module requires user set ' diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index a07a0f783..f1eca6df0 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -44,7 +44,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], def dynamic_expand_pairlist(config: dict, markets: list) -> List[str]: expanded_pairs = expand_pairlist(config['pairs'], markets) - if config.get('freqai', {}): + if config.get('freqai', {}).get('enabled', False): corr_pairlist = config['freqai']['feature_parameters']['include_corr_pairlist'] expanded_pairs += [pair for pair in corr_pairlist if pair not in config['pairs']] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index cda8a1cbc..d10699e3b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -146,7 +146,7 @@ class IStrategy(ABC, HyperStrategyMixin): self._ft_informative.append((informative_data, cls_method)) def load_freqAI_model(self) -> None: - if self.config.get('freqai', None): + if self.config.get('freqai', {}).get('enabled', False): # Import here to avoid importing this if freqAI is disabled from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index ee02cc097..63f3ef2c7 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -17,6 +17,7 @@ def freqai_conf(default_conf, tmpdir): freqaiconf = deepcopy(default_conf) freqaiconf.update( { + "enabled": True, "datadir": Path(default_conf["datadir"]), "strategy": "freqai_test_strat", "user_data_dir": Path(tmpdir), From 3918f4abbd7ed975e113477f09bd231ceb8f2fab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:27:56 +0200 Subject: [PATCH 129/132] Simplify strategy interface by removing explicit self.freqai_info assignment --- docs/freqai.md | 2 -- freqtrade/strategy/interface.py | 1 + freqtrade/templates/FreqaiExampleStrategy.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/freqai.md b/docs/freqai.md index c495ba24b..eb76ab1a4 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -401,8 +401,6 @@ The FreqAI strategy requires the user to include the following lines of code in def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - self.freqai_info = self.config["freqai"] - # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d10699e3b..760d852c4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -151,6 +151,7 @@ class IStrategy(ABC, HyperStrategyMixin): from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver self.freqai = FreqaiModelResolver.load_freqaimodel(self.config) + self.freqai_info = self.config["freqai"] def ft_bot_start(self, **kwargs) -> None: """ diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 01f947a6a..d8584d5f9 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -180,8 +180,6 @@ class FreqaiExampleStrategy(IStrategy): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - self.freqai_info = self.config["freqai"] - # All indicators must be populated by populate_any_indicators() for live functionality # to work correctly. From c190d57f1a67ffd3a6735365bbe3ee592c445a6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:48:59 +0200 Subject: [PATCH 130/132] Test populate_any_indicator interface --- tests/strategy/test_interface.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4257b2cf9..8b4ba5f03 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -290,6 +290,18 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed +def test_populate_any_indicators(default_conf, testdatadir) -> None: + strategy = StrategyResolver.load_strategy(default_conf) + + timerange = TimeRange.parse_timerange('1510694220-1510700340') + data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, + fill_up_missing=True) + processed = strategy.populate_any_indicators('UNITTEST/BTC', data, '5m') + assert processed == data + assert id(processed) == id(data) + assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed + + def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: strategy = StrategyResolver.load_strategy(default_conf) aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') From b682fc446e10c3df0aee91776df7a6e0ccebf851 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:53:18 +0200 Subject: [PATCH 131/132] Graciously fail if strategy has freqAI code, but freqAI is not enabled. --- freqtrade/strategy/interface.py | 7 +++++++ tests/strategy/test_interface.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 760d852c4..45609beaa 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -152,6 +152,13 @@ class IStrategy(ABC, HyperStrategyMixin): self.freqai = FreqaiModelResolver.load_freqaimodel(self.config) self.freqai_info = self.config["freqai"] + else: + # Gracious failures if freqAI is disabled but "start" is called. + class DummyClass(): + def start(self, *args, **kwargs): + raise OperationalException( + 'freqAI is not enabled. Please enable it in your config to use this strategy.') + self.freqai = DummyClass() # type: ignore def ft_bot_start(self, **kwargs) -> None: """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 8b4ba5f03..83f7d19b7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -302,6 +302,13 @@ def test_populate_any_indicators(default_conf, testdatadir) -> None: assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed +def test_freqai_not_initialized(default_conf) -> None: + strategy = StrategyResolver.load_strategy(default_conf) + strategy.ft_bot_start() + with pytest.raises(OperationalException, match=r'freqAI is not enabled\.'): + strategy.freqai.start() + + def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: strategy = StrategyResolver.load_strategy(default_conf) aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') From 1ac6ec1446a02a30d0a840e2181fcd2dcf6de932 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Aug 2022 09:56:21 +0200 Subject: [PATCH 132/132] Fix failing test... --- tests/freqai/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 63f3ef2c7..6ace13677 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -17,7 +17,6 @@ def freqai_conf(default_conf, tmpdir): freqaiconf = deepcopy(default_conf) freqaiconf.update( { - "enabled": True, "datadir": Path(default_conf["datadir"]), "strategy": "freqai_test_strat", "user_data_dir": Path(tmpdir), @@ -26,6 +25,7 @@ def freqai_conf(default_conf, tmpdir): "freqaimodel_path": "freqai/prediction_models", "timerange": "20180110-20180115", "freqai": { + "enabled": True, "startup_candles": 10000, "purge_old_models": True, "train_period_days": 5,