From d4272269007b3f1d7027ca7b4f5e1c99980666b1 Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:15:20 +0200 Subject: [PATCH 01/39] Update docker_quickstart.md --- docs/docker_quickstart.md | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 84c1d596a..6b48a7877 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -10,14 +10,14 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -To simplify running freqtrade, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). +To simplify running freqtrade, [`docker compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). -## Freqtrade with docker-compose +## Freqtrade with docker -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. !!! Note - - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. + - The following section assumes that `docker` is installed and available to the logged in user. - All below commands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. ### Docker quick start @@ -31,13 +31,13 @@ cd ft_userdata/ curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image -docker-compose pull +docker compose pull # Create user directory structure -docker-compose run --rm freqtrade create-userdir --userdir user_data +docker compose run --rm freqtrade create-userdir --userdir user_data # Create configuration - Requires answering interactive questions -docker-compose run --rm freqtrade new-config --config user_data/config.json +docker compose run --rm freqtrade new-config --config user_data/config.json ``` The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. @@ -64,7 +64,7 @@ The `SampleStrategy` is run by default. Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). ``` bash -docker-compose up -d +docker compose up -d ``` !!! Warning "Default configuration" @@ -84,27 +84,27 @@ You can now access the UI by typing localhost:8080 in your browser. #### Monitoring the bot -You can check for running instances with `docker-compose ps`. +You can check for running instances with `docker compose ps`. This should list the service `freqtrade` as `running`. If that's not the case, best check the logs (see next point). -#### Docker-compose logs +#### Docker compose logs Logs will be written to: `user_data/logs/freqtrade.log`. -You can also check the latest log with the command `docker-compose logs -f`. +You can also check the latest log with the command `docker compose logs -f`. #### Database The database will be located at: `user_data/tradesv3.sqlite` -#### Updating freqtrade with docker-compose +#### Updating freqtrade with docker -Updating freqtrade when using `docker-compose` is as simple as running the following 2 commands: +Updating freqtrade when using `docker` is as simple as running the following 2 commands: ``` bash # Download the latest image -docker-compose pull +docker compose pull # Restart the image -docker-compose up -d +docker compose up -d ``` This will first pull the latest image, and will then restart the container with the just pulled version. @@ -116,43 +116,43 @@ This will first pull the latest image, and will then restart the container with Advanced users may edit the docker-compose file further to include all possible options or arguments. -All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. +All freqtrade arguments will be available by running `docker compose run --rm freqtrade `. -!!! Warning "`docker-compose` for trade commands" - Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead. +!!! Warning "`docker compose` for trade commands" + Trade commands (`freqtrade trade <...>`) should not be ran via `docker compose run` - but should use `docker compose up -d` instead. This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot. If you intend to use freqUI, please also ensure to adjust the [configuration accordingly](rest-api.md#configuration-with-docker), otherwise the UI will not be available. -!!! Note "`docker-compose run --rm`" +!!! Note "`docker compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). -??? Note "Using docker without docker-compose" - "`docker-compose run --rm`" will require a compose file to be provided. +??? Note "Using docker without docker" + "`docker compose run --rm`" will require a compose file to be provided. Some freqtrade commands that don't require authentication such as `list-pairs` can be run with "`docker run --rm`" instead. For example `docker run --rm freqtradeorg/freqtrade:stable list-pairs --exchange binance --quote BTC --print-json`. This can be useful for fetching exchange information to add to your `config.json` without affecting your running containers. -#### Example: Download data with docker-compose +#### Example: Download data with docker Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. ``` bash -docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h +docker compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h ``` Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. -#### Example: Backtest with docker-compose +#### Example: Backtest with docker Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: ``` bash -docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m +docker compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m ``` Head over to the [Backtesting Documentation](backtesting.md) to learn more. -### Additional dependencies with docker-compose +### Additional dependencies with docker If your strategy requires dependencies not included in the default image - it will be necessary to build the image on your host. For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.custom](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.custom) for an example). @@ -166,15 +166,15 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the dockerfile: "./Dockerfile." ``` -You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above. +You can then run `docker compose build --pull` to build the docker image, and run it using the commands described above. -### Plotting with docker-compose +### Plotting with docker Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. You can then use these commands as follows: ``` bash -docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805 +docker compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805 ``` The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. @@ -185,7 +185,7 @@ Freqtrade provides a docker-compose file which starts up a jupyter lab server. You can run this server using the following command: ``` bash -docker-compose -f docker/docker-compose-jupyter.yml up +docker compose -f docker/docker-compose-jupyter.yml up ``` This will create a docker-container running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. @@ -194,7 +194,7 @@ Please use the link that's printed in the console after startup for simplified l Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) up-to-date. ``` bash -docker-compose -f docker/docker-compose-jupyter.yml build --no-cache +docker compose -f docker/docker-compose-jupyter.yml build --no-cache ``` ## Troubleshooting From abcbe7a42153740208f7ac4186fdecb0bc45f3f4 Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:15:59 +0200 Subject: [PATCH 02/39] Update updating.md --- docs/updating.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/updating.md b/docs/updating.md index 893bc846e..1e5dc8ffe 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -6,14 +6,14 @@ To update your freqtrade installation, please use one of the below methods, corr Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release. For the develop branch, please follow PR's to avoid being surprised by changes. -## docker-compose +## docker !!! Note "Legacy installations using the `master` image" We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` ``` bash -docker-compose pull -docker-compose up -d +docker compose pull +docker compose up -d ``` ## Installation via setup script From 11d6d0be9e25b1fac1be5cde8addbedeaedf129f Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:22:07 +0200 Subject: [PATCH 03/39] Update sql_cheatsheet.md --- docs/sql_cheatsheet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index c42cb5575..67c081d4c 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -13,12 +13,12 @@ Feel free to use a visual Database editor like SqliteBrowser if you feel more co sudo apt-get install sqlite3 ``` -### Using sqlite3 via docker-compose +### Using sqlite3 via docker The freqtrade docker image does contain sqlite3, so you can edit the database without having to install anything on the host system. ``` bash -docker-compose exec freqtrade /bin/bash +docker compose exec freqtrade /bin/bash sqlite3 .sqlite ``` From fe3d99b5685ad681347a448681f045c05e7f541e Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:22:49 +0200 Subject: [PATCH 04/39] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a18915462..db335bf09 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -18,7 +18,7 @@ Have you search for this feature before requesting it? It's highly likely that a * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) - * Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker) + * Freqtrade Version: ____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker) ## Describe the enhancement From 67850d92af1c81dfc139b7045ac33f4e2056e9fb Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:24:46 +0200 Subject: [PATCH 05/39] Update question.md --- .github/ISSUE_TEMPLATE/question.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 4b02e5f19..9283f0e4f 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -18,7 +18,7 @@ Please do not use the question template to report bugs or to request new feature * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) - * Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker) + * Freqtrade Version: ____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker) ## Your question From 35cc6aa966cc509cc9dc528c813928be4e6f3757 Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:25:37 +0200 Subject: [PATCH 06/39] Update data-analysis.md --- docs/data-analysis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 926ed3eae..5f01ae38f 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -5,7 +5,7 @@ You can analyze the results of backtests and trading history easily using Jupyte ## Quick start with docker Freqtrade provides a docker-compose file which starts up a jupyter lab server. -You can run this server using the following command: `docker-compose -f docker/docker-compose-jupyter.yml up` +You can run this server using the following command: `docker compose -f docker/docker-compose-jupyter.yml up` This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. Please use the link that's printed in the console after startup for simplified login. From 8c39b37223ec3ea449bf277957794f19a5bd9c58 Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:26:09 +0200 Subject: [PATCH 07/39] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 54c9eab50..8637c0d68 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,7 +20,7 @@ Please do not use bug reports to request new features. * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) - * Freqtrade Version: ____ (`freqtrade -V` or `docker-compose run --rm freqtrade -V` for Freqtrade running in docker) + * Freqtrade Version: ____ (`freqtrade -V` or `docker compose run --rm freqtrade -V` for Freqtrade running in docker) Note: All issues other than enhancement requests will be closed without further comment if the above template is deleted or not filled out. From 51b410ac1a333e5ae744e68be13b5dca8b3a1748 Mon Sep 17 00:00:00 2001 From: Matteo Manzi <33622899+matteoettam09@users.noreply.github.com> Date: Tue, 18 Oct 2022 19:28:29 +0200 Subject: [PATCH 08/39] Update utils.md --- docs/utils.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index ee8793159..2f4604323 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -654,7 +654,7 @@ Common arguments: You can also use webserver mode via docker. Starting a one-off container requires the configuration of the port explicitly, as ports are not exposed by default. -You can use `docker-compose run --rm -p 127.0.0.1:8080:8080 freqtrade webserver` to start a one-off container that'll be removed once you stop it. This assumes that port 8080 is still available and no other bot is running on that port. +You can use `docker compose run --rm -p 127.0.0.1:8080:8080 freqtrade webserver` to start a one-off container that'll be removed once you stop it. This assumes that port 8080 is still available and no other bot is running on that port. Alternatively, you can reconfigure the docker-compose file to have the command updated: @@ -664,7 +664,7 @@ Alternatively, you can reconfigure the docker-compose file to have the command u --config /freqtrade/user_data/config.json ``` -You can now use `docker-compose up` to start the webserver. +You can now use `docker compose up` to start the webserver. This assumes that the configuration has a webserver enabled and configured for docker (listening port = `0.0.0.0`). !!! Tip From eb81cccedebb79bd363b4d8fb48b49b6700e9749 Mon Sep 17 00:00:00 2001 From: k <> Date: Thu, 1 Dec 2022 16:37:24 +0800 Subject: [PATCH 09/39] add download-data command change directory fix relative config path --- .../templates/strategy_analysis_example.ipynb | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 5fb14ab2f..f7d68b41c 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -7,7 +7,7 @@ "# Strategy analysis example\n", "\n", "Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n", - "The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location." + "The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location, using command like `freqtrade download-data --exchange binance --trading-mod spot --pairs BTC/USDT --days 7 -t 5m`." ] }, { @@ -23,7 +23,21 @@ "metadata": {}, "outputs": [], "source": [ + "import os\n", "from pathlib import Path\n", + "\n", + "# Change current working directory from `somedir/freqtrade/user_data/notebooks` to project root `somedir/freqtrade`, so relative paths remain consistent.\n", + "if not Path(\"LICENSE\").is_file():\n", + " os.chdir(\"../../\")\n", + "print(Path.cwd())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "from freqtrade.configuration import Configuration\n", "\n", "# Customize these according to your needs.\n", @@ -31,14 +45,14 @@ "# Initialize empty configuration object\n", "config = Configuration.from_files([])\n", "# Optionally (recommended), use existing configuration file\n", - "# config = Configuration.from_files([\"config.json\"])\n", + "# config = Configuration.from_files([\"user_data/config.json\"])\n", "\n", "# Define some constants\n", "config[\"timeframe\"] = \"5m\"\n", "# Name of the strategy class\n", "config[\"strategy\"] = \"SampleStrategy\"\n", "# Location of the data\n", - "data_location = config['datadir']\n", + "data_location = config[\"datadir\"]\n", "# Pair to analyze - Only use one pair here\n", "pair = \"BTC/USDT\"" ] @@ -56,7 +70,7 @@ "candles = load_pair_history(datadir=data_location,\n", " timeframe=config[\"timeframe\"],\n", " pair=pair,\n", - " data_format = \"hdf5\",\n", + " data_format = \"json\",\n", " candle_type=CandleType.SPOT,\n", " )\n", "\n", @@ -365,7 +379,7 @@ "metadata": { "file_extension": ".py", "kernelspec": { - "display_name": "Python 3.9.7 64-bit ('trade_397')", + "display_name": "Python 3.11.0 64-bit", "language": "python", "name": "python3" }, @@ -379,7 +393,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.7" + "version": "3.11.0" }, "mimetype": "text/x-python", "name": "python", @@ -430,7 +444,7 @@ "version": 3, "vscode": { "interpreter": { - "hash": "675f32a300d6d26767470181ad0b11dd4676bcce7ed1dd2ffe2fbc370c95fc7c" + "hash": "945ba00099661281427cc644a7000ee9eeea5ce6ad3bf937939d3d384b8f3881" } } }, From 77dc2c92a7147239fc4853b361f54010ecc7b38e Mon Sep 17 00:00:00 2001 From: Wagner Costa Date: Thu, 1 Dec 2022 12:53:19 -0300 Subject: [PATCH 10/39] performance improvevemnts - backtest freqai from saved predictions --- freqtrade/freqai/data_kitchen.py | 6 +++--- freqtrade/freqai/freqai_interface.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index c6f22e468..9c8158c8a 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -462,10 +462,10 @@ class FreqaiDataKitchen: :param df: Dataframe containing all candles to run the entire backtest. Here it is sliced down to just the present training period. """ - - df = df.loc[df["date"] >= timerange.startdt, :] if not self.live: - df = df.loc[df["date"] < timerange.stopdt, :] + df = df.loc[(df["date"] >= timerange.startdt) & (df["date"] < timerange.stopdt), :] + else: + df = df.loc[df["date"] >= timerange.startdt, :] return df diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 3386d2881..34780f930 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -282,10 +282,10 @@ class IFreqaiModel(ABC): train_it += 1 total_trains = len(dk.backtesting_timeranges) self.training_timerange = tr_train - dataframe_train = dk.slice_dataframe(tr_train, dataframe) - dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) + len_backtest_df = len(dataframe.loc[(dataframe["date"] >= tr_backtest.startdt) & ( + dataframe["date"] < tr_backtest.stopdt), :]) - if not self.ensure_data_exists(dataframe_backtest, tr_backtest, pair): + if not self.ensure_data_exists(len_backtest_df, tr_backtest, pair): continue self.log_backtesting_progress(tr_train, pair, train_it, total_trains) @@ -298,13 +298,15 @@ class IFreqaiModel(ABC): dk.set_new_model_names(pair, timestamp_model_id) - if dk.check_if_backtest_prediction_is_valid(len(dataframe_backtest)): + if dk.check_if_backtest_prediction_is_valid(len_backtest_df): self.dd.load_metadata(dk) - dk.find_features(dataframe_train) + dk.find_features(dataframe) self.check_if_feature_list_matches_strategy(dk) append_df = dk.get_backtesting_prediction() dk.append_predictions(append_df) else: + dataframe_train = dk.slice_dataframe(tr_train, dataframe) + dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) if not self.model_exists(dk): dk.find_features(dataframe_train) dk.find_labels(dataframe_train) @@ -804,16 +806,16 @@ class IFreqaiModel(ABC): self.pair_it = 1 self.current_candle = self.dd.current_candle - def ensure_data_exists(self, dataframe_backtest: DataFrame, + def ensure_data_exists(self, len_dataframe_backtest: int, tr_backtest: TimeRange, pair: str) -> bool: """ Check if the dataframe is empty, if not, report useful information to user. - :param dataframe_backtest: the backtesting dataframe, maybe empty. + :param len_dataframe_backtest: the len of backtesting dataframe :param tr_backtest: current backtesting timerange. :param pair: current pair :return: if the data exists or not """ - if self.config.get("freqai_backtest_live_models", False) and len(dataframe_backtest) == 0: + if self.config.get("freqai_backtest_live_models", False) and len_dataframe_backtest == 0: logger.info(f"No data found for pair {pair} from " f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. " "Probably more than one training within the same candle period.") From 075c8c23c8bf50294e4a49b60466291dd63c2522 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Sat, 3 Dec 2022 21:16:04 +1100 Subject: [PATCH 11/39] add state/action info to callbacks --- .../prediction_models/ReinforcementLearner.py | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index 61b01e21b..ff39a66e0 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -71,7 +71,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): model.learn( total_timesteps=int(total_timesteps), - callback=self.eval_callback + callback=[self.eval_callback, self.tensorboard_callback] ) if Path(dk.data_path / "best_model.zip").is_file(): @@ -88,6 +88,33 @@ class ReinforcementLearner(BaseReinforcementLearningModel): User can override any function in BaseRLEnv and gym.Env. Here the user sets a custom reward based on profit and trade duration. """ + def reset(self): + + # Reset custom info + self.custom_info = {} + self.custom_info["Invalid"] = 0 + self.custom_info["Hold"] = 0 + self.custom_info["Unknown"] = 0 + self.custom_info["pnl_factor"] = 0 + self.custom_info["duration_factor"] = 0 + self.custom_info["reward_exit"] = 0 + self.custom_info["reward_hold"] = 0 + for action in Actions: + self.custom_info[f"{action.name}"] = 0 + return super().reset() + + def step(self, action: int): + observation, step_reward, done, info = super().step(action) + info = dict( + tick=self._current_tick, + action=action, + total_reward=self.total_reward, + total_profit=self._total_profit, + position=self._position.value, + trade_duration=self.get_trade_duration(), + current_profit_pct=self.get_unrealized_profit() + ) + return observation, step_reward, done, info def calculate_reward(self, action: int) -> float: """ @@ -100,17 +127,24 @@ class ReinforcementLearner(BaseReinforcementLearningModel): """ # first, penalize if the action is not valid if not self._is_valid(action): + self.custom_info["Invalid"] += 1 return -2 pnl = self.get_unrealized_profit() factor = 100. # reward agent for entering trades - if (action in (Actions.Long_enter.value, Actions.Short_enter.value) + if (action ==Actions.Long_enter.value and self._position == Positions.Neutral): + self.custom_info[f"{Actions.Long_enter.name}"] += 1 + return 25 + if (action == Actions.Short_enter.value + and self._position == Positions.Neutral): + self.custom_info[f"{Actions.Short_enter.name}"] += 1 return 25 # discourage agent from not entering trades if action == Actions.Neutral.value and self._position == Positions.Neutral: + self.custom_info[f"{Actions.Neutral.name}"] += 1 return -1 max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300) @@ -124,18 +158,22 @@ class ReinforcementLearner(BaseReinforcementLearningModel): # discourage sitting in position if (self._position in (Positions.Short, Positions.Long) and action == Actions.Neutral.value): + self.custom_info["Hold"] += 1 return -1 * trade_duration / max_trade_duration # close long if action == Actions.Long_exit.value and self._position == Positions.Long: if pnl > self.profit_aim * self.rr: factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) + self.custom_info[f"{Actions.Long_exit.name}"] += 1 return float(pnl * factor) # close short if action == Actions.Short_exit.value and self._position == Positions.Short: if pnl > self.profit_aim * self.rr: factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) + self.custom_info[f"{Actions.Short_exit.name}"] += 1 return float(pnl * factor) - + + self.custom_info["Unknown"] += 1 return 0. From 469aa0d43fcc7e2176690ab834a3f2be98709e32 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Sat, 3 Dec 2022 21:16:46 +1100 Subject: [PATCH 12/39] add state/action info to callbacks --- .../RL/BaseReinforcementLearningModel.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index 81f8edfc4..15acde6fb 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -13,9 +13,11 @@ import torch as th import torch.multiprocessing from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback +from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.utils import set_random_seed from stable_baselines3.common.vec_env import SubprocVecEnv +from stable_baselines3.common.logger import HParam from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen @@ -155,6 +157,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): self.eval_callback = EvalCallback(self.eval_env, deterministic=True, render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) + + self.tensorboard_callback = TensorboardCallback() @abstractmethod def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs): @@ -398,3 +402,48 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, return env set_random_seed(seed) return _init + +class TensorboardCallback(BaseCallback): + """ + Custom callback for plotting additional values in tensorboard. + """ + def __init__(self, verbose=1): + super(TensorboardCallback, self).__init__(verbose) + + def _on_training_start(self) -> None: + hparam_dict = { + "algorithm": self.model.__class__.__name__, + "learning_rate": self.model.learning_rate, + "gamma": self.model.gamma, + "gae_lambda": self.model.gae_lambda, + "batch_size": self.model.batch_size, + "n_steps": self.model.n_steps, + } + metric_dict = { + "eval/mean_reward": 0, + "rollout/ep_rew_mean": 0, + "rollout/ep_len_mean":0 , + "train/value_loss": 0, + "train/explained_variance": 0, + } + self.logger.record( + "hparams", + HParam(hparam_dict, metric_dict), + exclude=("stdout", "log", "json", "csv"), + ) + + def _on_step(self) -> bool: + custom_info = self.training_env.get_attr("custom_info")[0] + self.logger.record(f"_state/position", self.locals["infos"][0]["position"]) + self.logger.record(f"_state/trade_duration", self.locals["infos"][0]["trade_duration"]) + self.logger.record(f"_state/current_profit_pct", self.locals["infos"][0]["current_profit_pct"]) + self.logger.record(f"_reward/total_profit", self.locals["infos"][0]["total_profit"]) + self.logger.record(f"_reward/total_reward", self.locals["infos"][0]["total_reward"]) + self.logger.record_mean(f"_reward/mean_trade_duration", self.locals["infos"][0]["trade_duration"]) + self.logger.record(f"_actions/action", self.locals["infos"][0]["action"]) + self.logger.record(f"_actions/_Invalid", custom_info["Invalid"]) + self.logger.record(f"_actions/_Unknown", custom_info["Unknown"]) + self.logger.record(f"_actions/Hold", custom_info["Hold"]) + for action in Actions: + self.logger.record(f"_actions/{action.name}", custom_info[action.name]) + return True \ No newline at end of file From d6f45a12ae0778c6de86bd8020a69299ee474d31 Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Sat, 3 Dec 2022 22:30:04 +1100 Subject: [PATCH 13/39] add multiproc fix flake8 --- freqtrade/freqai/prediction_models/ReinforcementLearner.py | 6 +++--- .../prediction_models/ReinforcementLearner_multiproc.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index ff39a66e0..fa1087497 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -102,7 +102,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): for action in Actions: self.custom_info[f"{action.name}"] = 0 return super().reset() - + def step(self, action: int): observation, step_reward, done, info = super().step(action) info = dict( @@ -134,7 +134,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): factor = 100. # reward agent for entering trades - if (action ==Actions.Long_enter.value + if (action == Actions.Long_enter.value and self._position == Positions.Neutral): self.custom_info[f"{Actions.Long_enter.name}"] += 1 return 25 @@ -174,6 +174,6 @@ class ReinforcementLearner(BaseReinforcementLearningModel): factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) self.custom_info[f"{Actions.Short_exit.name}"] += 1 return float(pnl * factor) - + self.custom_info["Unknown"] += 1 return 0. diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index 56636c1f6..dd5430aa7 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -8,7 +8,7 @@ from stable_baselines3.common.vec_env import SubprocVecEnv from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner -from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env +from freqtrade.freqai.RL.BaseReinforcementLearningModel import TensorboardCallback, make_env logger = logging.getLogger(__name__) @@ -49,3 +49,5 @@ class ReinforcementLearner_multiproc(ReinforcementLearner): self.eval_callback = EvalCallback(self.eval_env, deterministic=True, render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) + + self.tensorboard_callback = TensorboardCallback() From b2edc58089a98994861409a106a8804b9f92270c Mon Sep 17 00:00:00 2001 From: smarmau <42020297+smarmau@users.noreply.github.com> Date: Sat, 3 Dec 2022 22:31:02 +1100 Subject: [PATCH 14/39] fix flake8 --- .../RL/BaseReinforcementLearningModel.py | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index 15acde6fb..b9b6cdd96 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -12,12 +12,11 @@ import pandas as pd import torch as th import torch.multiprocessing from pandas import DataFrame -from stable_baselines3.common.callbacks import EvalCallback -from stable_baselines3.common.callbacks import BaseCallback +from stable_baselines3.common.callbacks import BaseCallback, EvalCallback +from stable_baselines3.common.logger import HParam from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.utils import set_random_seed from stable_baselines3.common.vec_env import SubprocVecEnv -from stable_baselines3.common.logger import HParam from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen @@ -157,7 +156,7 @@ class BaseReinforcementLearningModel(IFreqaiModel): self.eval_callback = EvalCallback(self.eval_env, deterministic=True, render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) - + self.tensorboard_callback = TensorboardCallback() @abstractmethod @@ -403,6 +402,7 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, set_random_seed(seed) return _init + class TensorboardCallback(BaseCallback): """ Custom callback for plotting additional values in tensorboard. @@ -422,7 +422,7 @@ class TensorboardCallback(BaseCallback): metric_dict = { "eval/mean_reward": 0, "rollout/ep_rew_mean": 0, - "rollout/ep_len_mean":0 , + "rollout/ep_len_mean": 0, "train/value_loss": 0, "train/explained_variance": 0, } @@ -431,19 +431,21 @@ class TensorboardCallback(BaseCallback): HParam(hparam_dict, metric_dict), exclude=("stdout", "log", "json", "csv"), ) - + def _on_step(self) -> bool: custom_info = self.training_env.get_attr("custom_info")[0] - self.logger.record(f"_state/position", self.locals["infos"][0]["position"]) - self.logger.record(f"_state/trade_duration", self.locals["infos"][0]["trade_duration"]) - self.logger.record(f"_state/current_profit_pct", self.locals["infos"][0]["current_profit_pct"]) - self.logger.record(f"_reward/total_profit", self.locals["infos"][0]["total_profit"]) - self.logger.record(f"_reward/total_reward", self.locals["infos"][0]["total_reward"]) - self.logger.record_mean(f"_reward/mean_trade_duration", self.locals["infos"][0]["trade_duration"]) - self.logger.record(f"_actions/action", self.locals["infos"][0]["action"]) - self.logger.record(f"_actions/_Invalid", custom_info["Invalid"]) - self.logger.record(f"_actions/_Unknown", custom_info["Unknown"]) - self.logger.record(f"_actions/Hold", custom_info["Hold"]) + self.logger.record("_state/position", self.locals["infos"][0]["position"]) + self.logger.record("_state/trade_duration", self.locals["infos"][0]["trade_duration"]) + self.logger.record("_state/current_profit_pct", self.locals["infos"] + [0]["current_profit_pct"]) + self.logger.record("_reward/total_profit", self.locals["infos"][0]["total_profit"]) + self.logger.record("_reward/total_reward", self.locals["infos"][0]["total_reward"]) + self.logger.record_mean("_reward/mean_trade_duration", self.locals["infos"] + [0]["trade_duration"]) + self.logger.record("_actions/action", self.locals["infos"][0]["action"]) + self.logger.record("_actions/_Invalid", custom_info["Invalid"]) + self.logger.record("_actions/_Unknown", custom_info["Unknown"]) + self.logger.record("_actions/Hold", custom_info["Hold"]) for action in Actions: self.logger.record(f"_actions/{action.name}", custom_info[action.name]) - return True \ No newline at end of file + return True From 24766928baddfed919be1138a64d51cdbb0d3764 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 4 Dec 2022 13:54:30 +0100 Subject: [PATCH 15/39] reorganize/generalize tensorboard callback --- freqtrade/freqai/RL/Base4ActionRLEnv.py | 8 ++- freqtrade/freqai/RL/Base5ActionRLEnv.py | 8 ++- freqtrade/freqai/RL/BaseEnvironment.py | 37 ++++++++++- .../RL/BaseReinforcementLearningModel.py | 63 +++---------------- freqtrade/freqai/RL/TensorboardCallback.py | 61 ++++++++++++++++++ .../prediction_models/ReinforcementLearner.py | 27 -------- .../ReinforcementLearner_multiproc.py | 9 +-- 7 files changed, 125 insertions(+), 88 deletions(-) create mode 100644 freqtrade/freqai/RL/TensorboardCallback.py diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index df4e79bea..7818ac51e 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -20,6 +20,9 @@ class Base4ActionRLEnv(BaseEnvironment): """ Base class for a 4 action environment """ + def __init__(self, *args): + super().__init__(*args) + self.actions = Actions def set_action_space(self): self.action_space = spaces.Discrete(len(Actions)) @@ -92,9 +95,12 @@ class Base4ActionRLEnv(BaseEnvironment): info = dict( tick=self._current_tick, + action=action, total_reward=self.total_reward, total_profit=self._total_profit, - position=self._position.value + position=self._position.value, + trade_duration=self.get_trade_duration(), + current_profit_pct=self.get_unrealized_profit() ) observation = self._get_observation() diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py index 68b2e011b..1c09f9386 100644 --- a/freqtrade/freqai/RL/Base5ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py @@ -21,6 +21,9 @@ class Base5ActionRLEnv(BaseEnvironment): """ Base class for a 5 action environment """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.actions = Actions def set_action_space(self): self.action_space = spaces.Discrete(len(Actions)) @@ -98,9 +101,12 @@ class Base5ActionRLEnv(BaseEnvironment): info = dict( tick=self._current_tick, + action=action, total_reward=self.total_reward, total_profit=self._total_profit, - position=self._position.value + position=self._position.value, + trade_duration=self.get_trade_duration(), + current_profit_pct=self.get_unrealized_profit() ) observation = self._get_observation() diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index e7bd26a92..3fca6a25d 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -2,7 +2,7 @@ import logging import random from abc import abstractmethod from enum import Enum -from typing import Optional +from typing import Optional, Type import gym import numpy as np @@ -17,6 +17,17 @@ from freqtrade.data.dataprovider import DataProvider logger = logging.getLogger(__name__) +class BaseActions(Enum): + """ + Default action space, mostly used for type handling. + """ + Neutral = 0 + Long_enter = 1 + Long_exit = 2 + Short_enter = 3 + Short_exit = 4 + + class Positions(Enum): Short = 0 Long = 1 @@ -64,6 +75,9 @@ class BaseEnvironment(gym.Env): else: self.fee = 0.0015 + # set here to default 5Ac, but all children envs can overwrite this + self.actions: Type[Enum] = BaseActions + def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int, reward_kwargs: dict, starting_point=True): """ @@ -106,6 +120,7 @@ class BaseEnvironment(gym.Env): self._total_unrealized_profit: float = 1 self.history: dict = {} self.trade_history: list = [] + self.custom_info: dict = {} @abstractmethod def set_action_space(self): @@ -118,6 +133,19 @@ class BaseEnvironment(gym.Env): return [seed] def reset(self): + """ + Reset is called at the beginning of every episode + """ + # custom_info is used for episodic reports and tensorboard logging + self.custom_info["Invalid"] = 0 + self.custom_info["Hold"] = 0 + self.custom_info["Unknown"] = 0 + self.custom_info["pnl_factor"] = 0 + self.custom_info["duration_factor"] = 0 + self.custom_info["reward_exit"] = 0 + self.custom_info["reward_hold"] = 0 + for action in self.actions: + self.custom_info[f"{action.name}"] = 0 self._done = False @@ -271,6 +299,13 @@ class BaseEnvironment(gym.Env): def current_price(self) -> float: return self.prices.iloc[self._current_tick].open + def get_actions(self) -> Type[Enum]: + """ + Used by SubprocVecEnv to get actions from + initialized env for tensorboard callback + """ + return self.actions + # Keeping around incase we want to start building more complex environment # templates in the future. # def most_recent_return(self): diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index b9b6cdd96..5e9b81108 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -12,8 +12,7 @@ import pandas as pd import torch as th import torch.multiprocessing from pandas import DataFrame -from stable_baselines3.common.callbacks import BaseCallback, EvalCallback -from stable_baselines3.common.logger import HParam +from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.utils import set_random_seed from stable_baselines3.common.vec_env import SubprocVecEnv @@ -22,7 +21,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv -from freqtrade.freqai.RL.BaseEnvironment import Positions +from freqtrade.freqai.RL.BaseEnvironment import BaseActions, Positions +from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback from freqtrade.persistence import Trade @@ -45,8 +45,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): 'cpu_count', 1), max(int(self.max_system_threads / 2), 1)) th.set_num_threads(self.max_threads) self.reward_params = self.freqai_info['rl_config']['model_reward_parameters'] - self.train_env: Union[SubprocVecEnv, gym.Env] = None - self.eval_env: Union[SubprocVecEnv, gym.Env] = None + self.train_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env() + self.eval_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env() self.eval_callback: Optional[EvalCallback] = None self.model_type = self.freqai_info['rl_config']['model_type'] self.rl_config = self.freqai_info['rl_config'] @@ -66,6 +66,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): self.unset_outlier_removal() self.net_arch = self.rl_config.get('net_arch', [128, 128]) self.dd.model_type = import_str + self.tensorboard_callback: TensorboardCallback = \ + TensorboardCallback(verbose=1, actions=BaseActions) def unset_outlier_removal(self): """ @@ -157,7 +159,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) - self.tensorboard_callback = TensorboardCallback() + actions = self.train_env.get_actions() + self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) @abstractmethod def fit(self, data_dictionary: Dict[str, Any], dk: FreqaiDataKitchen, **kwargs): @@ -401,51 +404,3 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, return env set_random_seed(seed) return _init - - -class TensorboardCallback(BaseCallback): - """ - Custom callback for plotting additional values in tensorboard. - """ - def __init__(self, verbose=1): - super(TensorboardCallback, self).__init__(verbose) - - def _on_training_start(self) -> None: - hparam_dict = { - "algorithm": self.model.__class__.__name__, - "learning_rate": self.model.learning_rate, - "gamma": self.model.gamma, - "gae_lambda": self.model.gae_lambda, - "batch_size": self.model.batch_size, - "n_steps": self.model.n_steps, - } - metric_dict = { - "eval/mean_reward": 0, - "rollout/ep_rew_mean": 0, - "rollout/ep_len_mean": 0, - "train/value_loss": 0, - "train/explained_variance": 0, - } - self.logger.record( - "hparams", - HParam(hparam_dict, metric_dict), - exclude=("stdout", "log", "json", "csv"), - ) - - def _on_step(self) -> bool: - custom_info = self.training_env.get_attr("custom_info")[0] - self.logger.record("_state/position", self.locals["infos"][0]["position"]) - self.logger.record("_state/trade_duration", self.locals["infos"][0]["trade_duration"]) - self.logger.record("_state/current_profit_pct", self.locals["infos"] - [0]["current_profit_pct"]) - self.logger.record("_reward/total_profit", self.locals["infos"][0]["total_profit"]) - self.logger.record("_reward/total_reward", self.locals["infos"][0]["total_reward"]) - self.logger.record_mean("_reward/mean_trade_duration", self.locals["infos"] - [0]["trade_duration"]) - self.logger.record("_actions/action", self.locals["infos"][0]["action"]) - self.logger.record("_actions/_Invalid", custom_info["Invalid"]) - self.logger.record("_actions/_Unknown", custom_info["Unknown"]) - self.logger.record("_actions/Hold", custom_info["Hold"]) - for action in Actions: - self.logger.record(f"_actions/{action.name}", custom_info[action.name]) - return True diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py new file mode 100644 index 000000000..4aea9bdf5 --- /dev/null +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -0,0 +1,61 @@ +from enum import Enum +from typing import Any, Dict, Type, Union + +from stable_baselines3.common.callbacks import BaseCallback +from stable_baselines3.common.logger import HParam + +from freqtrade.freqai.RL.BaseEnvironment import BaseActions + + +class TensorboardCallback(BaseCallback): + """ + Custom callback for plotting additional values in tensorboard and + episodic summary reports. + """ + def __init__(self, verbose=1, actions: Type[Enum] = BaseActions): + super(TensorboardCallback, self).__init__(verbose) + self.model: Any = None + # An alias for self.model.get_env(), the environment used for training + self.logger = None # type: Any + # self.training_env = None # type: Union[gym.Env, VecEnv] + self.actions: Type[Enum] = actions + + def _on_training_start(self) -> None: + hparam_dict = { + "algorithm": self.model.__class__.__name__, + "learning_rate": self.model.learning_rate, + # "gamma": self.model.gamma, + # "gae_lambda": self.model.gae_lambda, + # "batch_size": self.model.batch_size, + # "n_steps": self.model.n_steps, + } + metric_dict: Dict[str, Union[float, int]] = { + "eval/mean_reward": 0, + "rollout/ep_rew_mean": 0, + "rollout/ep_len_mean": 0, + "train/value_loss": 0, + "train/explained_variance": 0, + } + self.logger.record( + "hparams", + HParam(hparam_dict, metric_dict), + exclude=("stdout", "log", "json", "csv"), + ) + + def _on_step(self) -> bool: + custom_info = self.training_env.get_attr("custom_info")[0] # type: ignore + self.logger.record("_state/position", self.locals["infos"][0]["position"]) + self.logger.record("_state/trade_duration", self.locals["infos"][0]["trade_duration"]) + self.logger.record("_state/current_profit_pct", self.locals["infos"] + [0]["current_profit_pct"]) + self.logger.record("_reward/total_profit", self.locals["infos"][0]["total_profit"]) + self.logger.record("_reward/total_reward", self.locals["infos"][0]["total_reward"]) + self.logger.record_mean("_reward/mean_trade_duration", self.locals["infos"] + [0]["trade_duration"]) + self.logger.record("_actions/action", self.locals["infos"][0]["action"]) + self.logger.record("_actions/_Invalid", custom_info["Invalid"]) + self.logger.record("_actions/_Unknown", custom_info["Unknown"]) + self.logger.record("_actions/Hold", custom_info["Hold"]) + for action in self.actions: + self.logger.record(f"_actions/{action.name}", custom_info[action.name]) + return True diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index fa1087497..47dbaf99e 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -88,33 +88,6 @@ class ReinforcementLearner(BaseReinforcementLearningModel): User can override any function in BaseRLEnv and gym.Env. Here the user sets a custom reward based on profit and trade duration. """ - def reset(self): - - # Reset custom info - self.custom_info = {} - self.custom_info["Invalid"] = 0 - self.custom_info["Hold"] = 0 - self.custom_info["Unknown"] = 0 - self.custom_info["pnl_factor"] = 0 - self.custom_info["duration_factor"] = 0 - self.custom_info["reward_exit"] = 0 - self.custom_info["reward_hold"] = 0 - for action in Actions: - self.custom_info[f"{action.name}"] = 0 - return super().reset() - - def step(self, action: int): - observation, step_reward, done, info = super().step(action) - info = dict( - tick=self._current_tick, - action=action, - total_reward=self.total_reward, - total_profit=self._total_profit, - position=self._position.value, - trade_duration=self.get_trade_duration(), - current_profit_pct=self.get_unrealized_profit() - ) - return observation, step_reward, done, info def calculate_reward(self, action: int) -> float: """ diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index dd5430aa7..32a2a2076 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -1,14 +1,14 @@ import logging -from typing import Any, Dict # , Tuple +from typing import Any, Dict -# import numpy.typing as npt from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.vec_env import SubprocVecEnv from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner -from freqtrade.freqai.RL.BaseReinforcementLearningModel import TensorboardCallback, make_env +from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env +from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback logger = logging.getLogger(__name__) @@ -50,4 +50,5 @@ class ReinforcementLearner_multiproc(ReinforcementLearner): render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) - self.tensorboard_callback = TensorboardCallback() + actions = self.train_env.env_method("get_actions")[0] + self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) From d8565261e1880f0458356fa2dc477ea487a56c0e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 4 Dec 2022 14:10:33 +0100 Subject: [PATCH 16/39] ignore initializer type --- freqtrade/freqai/RL/BaseEnvironment.py | 1 + freqtrade/freqai/RL/TensorboardCallback.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index 3fca6a25d..e43951142 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -77,6 +77,7 @@ class BaseEnvironment(gym.Env): # set here to default 5Ac, but all children envs can overwrite this self.actions: Type[Enum] = BaseActions + self.custom_info: dict = {} def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int, reward_kwargs: dict, starting_point=True): diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 4aea9bdf5..b5b8ba23d 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -4,7 +4,7 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam -from freqtrade.freqai.RL.BaseEnvironment import BaseActions +from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment class TensorboardCallback(BaseCallback): @@ -15,9 +15,8 @@ class TensorboardCallback(BaseCallback): def __init__(self, verbose=1, actions: Type[Enum] = BaseActions): super(TensorboardCallback, self).__init__(verbose) self.model: Any = None - # An alias for self.model.get_env(), the environment used for training self.logger = None # type: Any - # self.training_env = None # type: Union[gym.Env, VecEnv] + self.training_env: BaseEnvironment = None # type: ignore self.actions: Type[Enum] = actions def _on_training_start(self) -> None: @@ -43,7 +42,7 @@ class TensorboardCallback(BaseCallback): ) def _on_step(self) -> bool: - custom_info = self.training_env.get_attr("custom_info")[0] # type: ignore + custom_info = self.training_env.custom_info self.logger.record("_state/position", self.locals["infos"][0]["position"]) self.logger.record("_state/trade_duration", self.locals["infos"][0]["trade_duration"]) self.logger.record("_state/current_profit_pct", self.locals["infos"] From 133a081a394828f1b45e0b3c90223adca96388de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 03:00:39 +0000 Subject: [PATCH 17/39] Bump pytest-random-order from 1.0.4 to 1.1.0 Bumps [pytest-random-order](https://github.com/jbasko/pytest-random-order) from 1.0.4 to 1.1.0. - [Release notes](https://github.com/jbasko/pytest-random-order/releases) - [Commits](https://github.com/jbasko/pytest-random-order/compare/v1.0.4...v1.1.0) --- updated-dependencies: - dependency-name: pytest-random-order dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ffce3d696..463d2656a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,7 +15,7 @@ pytest==7.2.0 pytest-asyncio==0.20.2 pytest-cov==4.0.0 pytest-mock==3.10.0 -pytest-random-order==1.0.4 +pytest-random-order==1.1.0 isort==5.10.1 # For datetime mocking time-machine==2.8.2 From 16bad8dca6235a2d7272b753b4f3fa35698f61de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 03:00:56 +0000 Subject: [PATCH 18/39] Bump pymdown-extensions from 9.8 to 9.9 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.8 to 9.9. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.8...9.9) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 224e9b548..7a2c806ca 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,5 +2,5 @@ markdown==3.3.7 mkdocs==1.4.2 mkdocs-material==8.5.10 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.8 +pymdown-extensions==9.9 jinja2==3.1.2 From 441069f36390ad90227648fbb7a69b760b97e04e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 03:01:03 +0000 Subject: [PATCH 19/39] Bump pandas from 1.5.1 to 1.5.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.5.1 to 1.5.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/main/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.5.1...v1.5.2) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dab8ae414..755c96da2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.23.5 -pandas==1.5.1 +pandas==1.5.2 pandas-ta==0.3.14b ccxt==2.2.36 From caae4441e5a5b604cd5d8f5994dfd80ef3a71a7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 03:01:12 +0000 Subject: [PATCH 20/39] Bump jsonschema from 4.17.1 to 4.17.3 Bumps [jsonschema](https://github.com/python-jsonschema/jsonschema) from 4.17.1 to 4.17.3. - [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.17.1...v4.17.3) --- updated-dependencies: - dependency-name: jsonschema 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 dab8ae414..62887f995 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ arrow==1.2.3 cachetools==4.2.2 requests==2.28.1 urllib3==1.26.13 -jsonschema==4.17.1 +jsonschema==4.17.3 TA-Lib==0.4.25 technical==1.3.0 tabulate==0.9.0 From 66bb2c52532167b9f64aadc7ca9cb739b1980119 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 03:01:16 +0000 Subject: [PATCH 21/39] Bump fastapi from 0.87.0 to 0.88.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.87.0 to 0.88.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.87.0...0.88.0) --- updated-dependencies: - dependency-name: fastapi 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 dab8ae414..d9c479444 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ orjson==3.8.2 sdnotify==0.3.2 # API Server -fastapi==0.87.0 +fastapi==0.88.0 pydantic==1.10.2 uvicorn==0.20.0 pyjwt==2.6.0 From 2eb8f9f0282ca0f9f30731265f667c31521760bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 03:03:47 +0000 Subject: [PATCH 22/39] Bump pypa/gh-action-pypi-publish from 1.5.1 to 1.6.1 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.5.1 to 1.6.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.5.1...v1.6.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... 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 e730d1489..273fb7ea0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -410,7 +410,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.5.1 + uses: pypa/gh-action-pypi-publish@v1.6.1 if: (github.event_name == 'release') with: user: __token__ @@ -418,7 +418,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.5.1 + uses: pypa/gh-action-pypi-publish@v1.6.1 if: (github.event_name == 'release') with: user: __token__ From 82d4dca1832ac306ea95dfdda52cd21d9cf8a68a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 05:21:15 +0000 Subject: [PATCH 23/39] Bump mkdocs-material from 8.5.10 to 8.5.11 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.10 to 8.5.11. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.10...8.5.11) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 7a2c806ca..fd4f66d71 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==8.5.10 +mkdocs-material==8.5.11 mdx_truly_sane_lists==1.3 pymdown-extensions==9.9 jinja2==3.1.2 From 179adea0e221eb2c85607a6a2e7b2a34e9116640 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 05:22:42 +0000 Subject: [PATCH 24/39] Bump ccxt from 2.2.36 to 2.2.67 Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.2.36 to 2.2.67. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/2.2.36...2.2.67) --- 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 1e9c5030e..99cdca11e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.23.5 pandas==1.5.2 pandas-ta==0.3.14b -ccxt==2.2.36 +ccxt==2.2.67 # Pin cryptography for now due to rust build errors with piwheels cryptography==38.0.1; platform_machine == 'armv7l' cryptography==38.0.4; platform_machine != 'armv7l' From 102ab91fa44ee1873f844b9fb9fe36e914910aef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Dec 2022 05:23:02 +0000 Subject: [PATCH 25/39] Bump orjson from 3.8.2 to 3.8.3 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.2 to 3.8.3. - [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.8.2...3.8.3) --- 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 1e9c5030e..51d3f9dde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.9 # Properly format api responses -orjson==3.8.2 +orjson==3.8.3 # Notify systemd sdnotify==0.3.2 From 189fa64052b0261a272765ed799941e8e001ef4a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Dec 2022 18:14:16 +0100 Subject: [PATCH 26/39] Add more dynamic to directory change --- docs/strategy_analysis_example.md | 33 +++++++++++++-- .../templates/strategy_analysis_example.ipynb | 40 ++++++++++++++----- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index bae4a9108..e3d2870e2 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -2,12 +2,37 @@ Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data. The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location. +Please follow the [documentation](https://www.freqtrade.io/en/stable/data-download/) for more details. ## Setup +### Change Working directory to repository root + ```python +import os from pathlib import Path + +# Change directory +# Modify this cell to insure that the output shows the correct path. +# Define all paths relative to the project root shown in the cell output +project_root = "somedir/freqtrade" +i=0 +try: + os.chdirdir(project_root) + assert Path('LICENSE').is_file() +except: + while i<4 and (not Path('LICENSE').is_file()): + os.chdir(Path(Path.cwd(), '../')) + i+=1 + project_root = Path.cwd() +print(Path.cwd()) +``` + +### Configure Freqtrade environment + + +```python from freqtrade.configuration import Configuration # Customize these according to your needs. @@ -15,14 +40,14 @@ from freqtrade.configuration import Configuration # Initialize empty configuration object config = Configuration.from_files([]) # Optionally (recommended), use existing configuration file -# config = Configuration.from_files(["config.json"]) +# config = Configuration.from_files(["user_data/config.json"]) # Define some constants config["timeframe"] = "5m" # Name of the strategy class config["strategy"] = "SampleStrategy" # Location of the data -data_location = config['datadir'] +data_location = config["datadir"] # Pair to analyze - Only use one pair here pair = "BTC/USDT" ``` @@ -36,12 +61,12 @@ from freqtrade.enums import CandleType candles = load_pair_history(datadir=data_location, timeframe=config["timeframe"], pair=pair, - data_format = "hdf5", + data_format = "json", # Make sure to update this to your data candle_type=CandleType.SPOT, ) # Confirm success -print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") +print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}") candles.head() ``` diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index f7d68b41c..dfbcedb72 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -7,14 +7,17 @@ "# Strategy analysis example\n", "\n", "Debugging a strategy can be time-consuming. Freqtrade offers helper functions to visualize raw data.\n", - "The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location, using command like `freqtrade download-data --exchange binance --trading-mod spot --pairs BTC/USDT --days 7 -t 5m`." + "The following assumes you work with SampleStrategy, data for 5m timeframe from Binance and have downloaded them into the data directory in the default location.\n", + "Please follow the [documentation](https://www.freqtrade.io/en/stable/data-download/) for more details." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Setup" + "## Setup\n", + "\n", + "### Change Working directory to repository root" ] }, { @@ -26,12 +29,29 @@ "import os\n", "from pathlib import Path\n", "\n", - "# Change current working directory from `somedir/freqtrade/user_data/notebooks` to project root `somedir/freqtrade`, so relative paths remain consistent.\n", - "if not Path(\"LICENSE\").is_file():\n", - " os.chdir(\"../../\")\n", + "# Change directory\n", + "# Modify this cell to insure that the output shows the correct path.\n", + "# Define all paths relative to the project root shown in the cell output\n", + "project_root = \"somedir/freqtrade\"\n", + "i=0\n", + "try:\n", + " os.chdirdir(project_root)\n", + " assert Path('LICENSE').is_file()\n", + "except:\n", + " while i<4 and (not Path('LICENSE').is_file()):\n", + " os.chdir(Path(Path.cwd(), '../'))\n", + " i+=1\n", + " project_root = Path.cwd()\n", "print(Path.cwd())" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Configure Freqtrade environment" + ] + }, { "cell_type": "code", "execution_count": null, @@ -70,12 +90,12 @@ "candles = load_pair_history(datadir=data_location,\n", " timeframe=config[\"timeframe\"],\n", " pair=pair,\n", - " data_format = \"json\",\n", + " data_format = \"json\", # Make sure to update this to your data\n", " candle_type=CandleType.SPOT,\n", " )\n", "\n", "# Confirm success\n", - "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", + "print(f\"Loaded {len(candles)} rows of data for {pair} from {data_location}\")\n", "candles.head()" ] }, @@ -379,7 +399,7 @@ "metadata": { "file_extension": ".py", "kernelspec": { - "display_name": "Python 3.11.0 64-bit", + "display_name": "Python 3.9.7 64-bit", "language": "python", "name": "python3" }, @@ -393,7 +413,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0" + "version": "3.9.7" }, "mimetype": "text/x-python", "name": "python", @@ -444,7 +464,7 @@ "version": 3, "vscode": { "interpreter": { - "hash": "945ba00099661281427cc644a7000ee9eeea5ce6ad3bf937939d3d384b8f3881" + "hash": "675f32a300d6d26767470181ad0b11dd4676bcce7ed1dd2ffe2fbc370c95fc7c" } } }, From 5e533b550f777e6e898091cc86f354ab7ef91a48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Oct 2022 20:22:58 +0200 Subject: [PATCH 27/39] Emit a simple "new candle" message to allow UI's to refresh charts --- freqtrade/data/dataprovider.py | 18 ++++++++++++------ freqtrade/enums/rpcmessagetype.py | 1 + freqtrade/rpc/webhook.py | 1 + freqtrade/strategy/interface.py | 6 +++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 4d7296ee7..7657549a3 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -101,16 +101,13 @@ class DataProvider: """ return self.__producer_pairs.get(producer_name, []).copy() - def _emit_df( - self, - pair_key: PairWithTimeframe, - dataframe: DataFrame - ) -> None: + def _emit_df(self, pair_key: PairWithTimeframe, dataframe: DataFrame, new_candle: bool) -> None: """ Send this dataframe as an ANALYZED_DF message to RPC :param pair_key: PairWithTimeframe tuple - :param data: Tuple containing the DataFrame and the datetime it was cached + :param dataframe: Dataframe to emit + :param new_candle: This is a new candle """ if self.__rpc: self.__rpc.send_msg( @@ -123,6 +120,15 @@ class DataProvider: } } ) + if new_candle: + self.__rpc.send_msg( + { + 'type': RPCMessageType.NEW_CANDLE, + 'data': { + 'key': pair_key, + } + } + ) def _add_external_df( self, diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index fae121a09..8b3596465 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -21,6 +21,7 @@ class RPCMessageType(str, Enum): WHITELIST = 'whitelist' ANALYZED_DF = 'analyzed_df' + NEW_CANDLE = 'new_candle' def __repr__(self): return self.value diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 19c4166b3..d81d8d24f 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -68,6 +68,7 @@ class Webhook(RPCHandler): RPCMessageType.PROTECTION_TRIGGER_GLOBAL, RPCMessageType.WHITELIST, RPCMessageType.ANALYZED_DF, + RPCMessageType.NEW_CANDLE, RPCMessageType.STRATEGY_MSG): # Don't fail for non-implemented types return None diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 681c5fcbb..781ae6c5c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -739,10 +739,10 @@ class IStrategy(ABC, HyperStrategyMixin): """ pair = str(metadata.get('pair')) + new_candle = self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date'] # Test if seen this pair and last candle before. # always run if process_only_new_candles is set to false - if (not self.process_only_new_candles or - self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): + if not self.process_only_new_candles or new_candle: # Defs that only make change on new candle data. dataframe = self.analyze_ticker(dataframe, metadata) @@ -751,7 +751,7 @@ class IStrategy(ABC, HyperStrategyMixin): candle_type = self.config.get('candle_type_def', CandleType.SPOT) self.dp._set_cached_df(pair, self.timeframe, dataframe, candle_type=candle_type) - self.dp._emit_df((pair, self.timeframe, candle_type), dataframe) + self.dp._emit_df((pair, self.timeframe, candle_type), dataframe, new_candle) else: logger.debug("Skipping TA Analysis for already analyzed candle") From 687eefa06e1318b41e55a3aee5eb4e3cb3d33df4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Oct 2022 20:31:31 +0200 Subject: [PATCH 28/39] Improve emit_df testcase --- tests/data/test_dataprovider.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 8500fa06c..025e6d08a 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -207,12 +207,18 @@ def test_emit_df(mocker, default_conf, ohlcv_history): assert send_mock.call_count == 0 # Rpc is added, we call emit, should call send_msg - dataprovider._emit_df(pair, ohlcv_history) + dataprovider._emit_df(pair, ohlcv_history, False) assert send_mock.call_count == 1 + send_mock.reset_mock() + dataprovider._emit_df(pair, ohlcv_history, True) + assert send_mock.call_count == 2 + + send_mock.reset_mock() + # No rpc added, emit called, should not call send_msg - dataprovider_no_rpc._emit_df(pair, ohlcv_history) - assert send_mock.call_count == 1 + dataprovider_no_rpc._emit_df(pair, ohlcv_history, False) + assert send_mock.call_count == 0 def test_refresh(mocker, default_conf, ohlcv_history): From d30a872ed48c6a72acdfc23ff64d045c2d1f33d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Dec 2022 19:23:03 +0100 Subject: [PATCH 29/39] Move message-silencing list next to enum --- freqtrade/enums/__init__.py | 2 +- freqtrade/enums/rpcmessagetype.py | 3 +++ freqtrade/rpc/rpc_manager.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 146d65f2d..eb70a2894 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -6,7 +6,7 @@ from freqtrade.enums.exittype import ExitType from freqtrade.enums.hyperoptstate import HyperoptState from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.ordertypevalue import OrderTypeValues -from freqtrade.enums.rpcmessagetype import RPCMessageType, RPCRequestType +from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 8b3596465..2453d16d9 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -36,3 +36,6 @@ class RPCRequestType(str, Enum): WHITELIST = 'whitelist' ANALYZED_DF = 'analyzed_df' + + +NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 9c25723b0..c4d4fa2dd 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -6,7 +6,7 @@ from collections import deque from typing import Any, Dict, List from freqtrade.constants import Config -from freqtrade.enums import RPCMessageType +from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType from freqtrade.rpc import RPC, RPCHandler @@ -67,7 +67,7 @@ class RPCManager: 'status': 'stopping bot' } """ - if msg.get('type') not in (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST): + if msg.get('type') not in NO_ECHO_MESSAGES: logger.info('Sending rpc message: %s', msg) if 'pair' in msg: msg.update({ From 24edc276ea31d4e733667155aecc8e403a43f7f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Dec 2022 19:43:36 +0100 Subject: [PATCH 30/39] Simplify new_candle message --- freqtrade/data/dataprovider.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 7657549a3..6b220c8b4 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -101,7 +101,12 @@ class DataProvider: """ return self.__producer_pairs.get(producer_name, []).copy() - def _emit_df(self, pair_key: PairWithTimeframe, dataframe: DataFrame, new_candle: bool) -> None: + def _emit_df( + self, + pair_key: PairWithTimeframe, + dataframe: DataFrame, + new_candle: bool + ) -> None: """ Send this dataframe as an ANALYZED_DF message to RPC @@ -121,14 +126,10 @@ class DataProvider: } ) if new_candle: - self.__rpc.send_msg( - { + self.__rpc.send_msg({ 'type': RPCMessageType.NEW_CANDLE, - 'data': { - 'key': pair_key, - } - } - ) + 'data': pair_key, + }) def _add_external_df( self, From 7c27eedda54e36c06471559a1dcf92bb98248405 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Dec 2022 19:56:33 +0100 Subject: [PATCH 31/39] Bump API version --- freqtrade/rpc/api_server/api_v1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index c0c9b8f57..9e4b140e4 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -37,7 +37,8 @@ logger = logging.getLogger(__name__) # 2.16: Additional daily metrics # 2.17: Forceentry - leverage, partial force_exit # 2.20: Add websocket endpoints -API_VERSION = 2.20 +# 2.21: Add new_candle messagetype +API_VERSION = 2.21 # Public API, requires no auth. router_public = APIRouter() From 72472587ddb4251270502e19f2808493c53cb2ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Dec 2022 20:18:47 +0100 Subject: [PATCH 32/39] Increase test range for api version test --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 043666853..ee067f911 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -588,7 +588,7 @@ def test_api_show_config(botclient): assert 'unfilledtimeout' in response assert 'version' in response assert 'api_version' in response - assert 2.1 <= response['api_version'] <= 2.2 + assert 2.1 <= response['api_version'] < 3.0 def test_api_daily(botclient, mocker, ticker, fee, markets): From 62c69bf2b5285196ce80760160712c04b339bad1 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Mon, 5 Dec 2022 20:22:54 +0100 Subject: [PATCH 33/39] fix custom_info --- freqtrade/freqai/RL/Base4ActionRLEnv.py | 4 ++-- freqtrade/freqai/RL/BaseEnvironment.py | 3 +-- freqtrade/freqai/RL/TensorboardCallback.py | 2 +- tests/freqai/test_freqai_interface.py | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index 7818ac51e..79616d778 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -20,8 +20,8 @@ class Base4ActionRLEnv(BaseEnvironment): """ Base class for a 4 action environment """ - def __init__(self, *args): - super().__init__(*args) + def __init__(self, **kwargs): + super().__init__(**kwargs) self.actions = Actions def set_action_space(self): diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index e43951142..a31ded0c6 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -75,7 +75,7 @@ class BaseEnvironment(gym.Env): else: self.fee = 0.0015 - # set here to default 5Ac, but all children envs can overwrite this + # set here to default 5Ac, but all children envs can override this self.actions: Type[Enum] = BaseActions self.custom_info: dict = {} @@ -121,7 +121,6 @@ class BaseEnvironment(gym.Env): self._total_unrealized_profit: float = 1 self.history: dict = {} self.trade_history: list = [] - self.custom_info: dict = {} @abstractmethod def set_action_space(self): diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index b5b8ba23d..f590bdf84 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -42,7 +42,7 @@ class TensorboardCallback(BaseCallback): ) def _on_step(self) -> bool: - custom_info = self.training_env.custom_info + custom_info = self.training_env.get_attr("custom_info")[0] self.logger.record("_state/position", self.locals["infos"][0]["position"]) self.logger.record("_state/trade_duration", self.locals["infos"][0]["trade_duration"]) self.logger.record("_state/current_profit_pct", self.locals["infos"] diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index c53137093..f19acb018 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -237,7 +237,6 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) df = freqai.cache_corr_pairlist_dfs(df, freqai.dk) for i in range(5): df[f'%-constant_{i}'] = i - # df.loc[:, f'%-constant_{i}'] = i metadata = {"pair": "LTC/BTC"} freqai.start_backtesting(df, metadata, freqai.dk) From 9b4364ddc3e410ca445cf08d73c606aed4323e6d Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 7 Dec 2022 19:49:14 +0100 Subject: [PATCH 34/39] ensure that add_state_info is deactivated during backtesting --- freqtrade/freqai/RL/BaseEnvironment.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index a31ded0c6..c217b72dd 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -12,6 +12,7 @@ from gym.utils import seeding from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider +from freqtrade.enums import RunMode logger = logging.getLogger(__name__) @@ -78,6 +79,11 @@ class BaseEnvironment(gym.Env): # set here to default 5Ac, but all children envs can override this self.actions: Type[Enum] = BaseActions self.custom_info: dict = {} + self.live: bool = False + if dp: + self.live = dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE) + if not self.live and self.add_state_info: + logger.warning("add_state_info is not available in backtesting. Deactivating.") def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int, reward_kwargs: dict, starting_point=True): @@ -188,7 +194,7 @@ class BaseEnvironment(gym.Env): """ features_window = self.signal_features[( self._current_tick - self.window_size):self._current_tick] - if self.add_state_info: + if self.add_state_info and self.live: features_and_state = DataFrame(np.zeros((len(features_window), 3)), columns=['current_profit_pct', 'position', From 7b3406914c2a219b877867e08f93c26ab64d9e41 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 7 Dec 2022 19:49:39 +0100 Subject: [PATCH 35/39] flip add_state_info --- freqtrade/freqai/RL/BaseEnvironment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index c217b72dd..86c63c382 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -83,6 +83,7 @@ class BaseEnvironment(gym.Env): if dp: self.live = dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE) if not self.live and self.add_state_info: + self.add_state_info = False logger.warning("add_state_info is not available in backtesting. Deactivating.") def reset_env(self, df: DataFrame, prices: DataFrame, window_size: int, From 74e623fe5b4c5931362f149ce88d52ed3cb12cdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Dec 2022 08:33:07 +0100 Subject: [PATCH 36/39] Improve kraken test resiliance --- tests/exchange/test_ccxt_compat.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 280876ae8..7f23c2031 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -224,8 +224,13 @@ class TestCCXTExchange(): for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: - assert len(l2['asks']) == val - assert len(l2['bids']) == val + if val > 50: + # Orderbooks are not always this deep. + assert val - 5 < len(l2['asks']) <= val + assert val - 5 < len(l2['bids']) <= val + else: + assert len(l2['asks']) == val + assert len(l2['bids']) == val else: next_limit = exchange.get_next_limit_in_list( val, l2_limit_range, l2_limit_range_required) From 3d3a7033ed34f8c9bed86c729198e8a4b5e0414f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Dec 2022 08:46:16 +0100 Subject: [PATCH 37/39] Improve Docker documentation wording --- docs/docker_quickstart.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 6b48a7877..89f737d71 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -4,13 +4,15 @@ This page explains how to run the bot with Docker. It is not meant to work out o ## Install Docker -Start by downloading and installing Docker CE for your platform: +Start by downloading and installing Docker / Docker Desktop for your platform: * [Mac](https://docs.docker.com/docker-for-mac/install/) * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -To simplify running freqtrade, [`docker compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start). +!!! Info "Docker compose install" + Freqtrade documentation assumes the use of Docker desktop (or the docker compose plugin). + While the docker-compose standalone installation still works, it will require changing all `docker compose` commands from `docker compose` to `docker-compose` to work (e.g. `docker compose up -d` will become `docker-compose up -d`). ## Freqtrade with docker From bbedc4b63efd08a4e4e3b2371a8463e6f6e445b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Dec 2022 14:15:29 +0100 Subject: [PATCH 38/39] Stop clock to avoid random failures on slow CI runs --- tests/rpc/test_rpc_telegram.py | 70 +++++++++++++++++----------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3552d5fe7..1f4665867 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -12,6 +12,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest +import time_machine from pandas import DataFrame from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -2065,41 +2066,42 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction, default_conf['telegram']['notification_settings']['exit_fill'] = 'on' telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram.send_msg({ - 'type': RPCMessageType.EXIT_FILL, - 'trade_id': 1, - 'exchange': 'Binance', - 'pair': 'KEY/ETH', - 'leverage': leverage, - 'direction': direction, - 'gain': 'loss', - 'limit': 3.201e-05, - 'amount': 1333.3333333333335, - 'order_type': 'market', - 'open_rate': 7.5e-05, - 'close_rate': 3.201e-05, - 'profit_amount': -0.05746268, - 'profit_ratio': -0.57405275, - 'stake_currency': 'ETH', - 'enter_tag': enter_signal, - 'exit_reason': ExitType.STOP_LOSS.value, - 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), - 'close_date': arrow.utcnow(), - }) + with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False) as t: + telegram.send_msg({ + 'type': RPCMessageType.EXIT_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'leverage': leverage, + 'direction': direction, + 'gain': 'loss', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'order_type': 'market', + 'open_rate': 7.5e-05, + 'close_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + 'enter_tag': enter_signal, + 'exit_reason': ExitType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), + 'close_date': arrow.utcnow(), + }) - 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% (loss: -0.05746268 ETH)`\n' - f'*Enter Tag:* `{enter_signal}`\n' - '*Exit Reason:* `stop_loss`\n' - f"*Direction:* `{direction}`\n" - f"{leverage_text}" - '*Amount:* `1333.33333333`\n' - '*Open Rate:* `0.00007500`\n' - '*Exit Rate:* `0.00003201`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`' - ) + 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% (loss: -0.05746268 ETH)`\n' + f'*Enter Tag:* `{enter_signal}`\n' + '*Exit Reason:* `stop_loss`\n' + f"*Direction:* `{direction}`\n" + f"{leverage_text}" + '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' + '*Exit Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' + ) def test_send_msg_status_notification(default_conf, mocker) -> None: From 1da8ad69d9501838fee5792b39563d9925ed7ad5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Dec 2022 14:33:07 +0100 Subject: [PATCH 39/39] improve more tests by freezing time --- tests/rpc/test_rpc_telegram.py | 223 +++++++++++++++++---------------- 1 file changed, 112 insertions(+), 111 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1f4665867..58977a94a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1907,119 +1907,120 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en def test_send_msg_sell_notification(default_conf, mocker) -> None: - telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False): + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - old_convamount = telegram._rpc._fiat_converter.convert_amount - telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 - telegram.send_msg({ - 'type': RPCMessageType.EXIT, - 'trade_id': 1, - 'exchange': 'Binance', - 'pair': 'KEY/ETH', - 'leverage': 1.0, - 'direction': 'Long', - 'gain': 'loss', - 'order_rate': 3.201e-05, - 'amount': 1333.3333333333335, - 'order_type': 'market', - 'open_rate': 7.5e-05, - 'current_rate': 3.201e-05, - '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(hours=-1), - 'close_date': arrow.utcnow(), - }) - assert msg_mock.call_args[0][0] == ( - '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41% (loss: -0.05746268 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' - '*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', - 'order_rate': 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)`' + old_convamount = telegram._rpc._fiat_converter.convert_amount + telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 + telegram.send_msg({ + 'type': RPCMessageType.EXIT, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'leverage': 1.0, + 'direction': 'Long', + 'gain': 'loss', + 'order_rate': 3.201e-05, + 'amount': 1333.3333333333335, + 'order_type': 'market', + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + '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(hours=-1), + 'close_date': arrow.utcnow(), + }) + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 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' + '*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', - 'order_rate': 3.201e-05, - 'amount': 1333.3333333333335, - 'order_type': 'market', - 'open_rate': 7.5e-05, - 'current_rate': 3.201e-05, - 'profit_amount': -0.05746268, - 'profit_ratio': -0.57405275, - 'stake_currency': 'ETH', - '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(), - }) - assert msg_mock.call_args[0][0] == ( - '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' - '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\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' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`' - ) - # Reset singleton function to avoid random breaks - telegram._rpc._fiat_converter.convert_amount = old_convamount + msg_mock.reset_mock() + telegram.send_msg({ + 'type': RPCMessageType.EXIT, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'direction': 'Long', + 'gain': 'loss', + 'order_rate': 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, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'direction': 'Long', + 'gain': 'loss', + 'order_rate': 3.201e-05, + 'amount': 1333.3333333333335, + 'order_type': 'market', + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + '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(), + }) + assert msg_mock.call_args[0][0] == ( + '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' + '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH)`\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' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' + ) + # Reset singleton function to avoid random breaks + telegram._rpc._fiat_converter.convert_amount = old_convamount def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: @@ -2066,7 +2067,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction, default_conf['telegram']['notification_settings']['exit_fill'] = 'on' telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False) as t: + with time_machine.travel("2022-09-01 05:00:00 +00:00", tick=False): telegram.send_msg({ 'type': RPCMessageType.EXIT_FILL, 'trade_id': 1,