diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e0483c3d..5c80bc141 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,8 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
-
+permissions:
+ repository-projects: read
jobs:
build_linux:
@@ -321,7 +322,6 @@ jobs:
build_linux_online:
# Run pytest with "live" checks
runs-on: ubuntu-22.04
- # permissions:
steps:
- uses: actions/checkout@v3
@@ -425,7 +425,7 @@ jobs:
python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test)
- uses: pypa/gh-action-pypi-publish@v1.7.1
+ uses: pypa/gh-action-pypi-publish@v1.8.3
if: (github.event_name == 'release')
with:
user: __token__
@@ -433,7 +433,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
- uses: pypa/gh-action-pypi-publish@v1.7.1
+ uses: pypa/gh-action-pypi-publish@v1.8.3
if: (github.event_name == 'release')
with:
user: __token__
@@ -466,12 +466,13 @@ jobs:
- name: Build and test and push docker images
env:
- IMAGE_NAME: freqtradeorg/freqtrade
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
run: |
build_helpers/publish_docker_multi.sh
deploy_arm:
+ permissions:
+ packages: write
needs: [ deploy ]
# Only run on 64bit machines
runs-on: [self-hosted, linux, ARM64]
@@ -494,8 +495,9 @@ jobs:
- name: Build and test and push docker images
env:
- IMAGE_NAME: freqtradeorg/freqtrade
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
+ GHCR_USERNAME: ${{ github.actor }}
+ GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
build_helpers/publish_docker_arm64.sh
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bc2e0bc0d..4784055a9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -15,10 +15,10 @@ repos:
additional_dependencies:
- types-cachetools==5.3.0.4
- types-filelock==3.2.7
- - types-requests==2.28.11.15
+ - types-requests==2.28.11.16
- types-tabulate==0.9.0.1
- types-python-dateutil==2.8.19.10
- - SQLAlchemy==2.0.5.post1
+ - SQLAlchemy==2.0.7
# stages: [push]
- repo: https://github.com/pycqa/isort
diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh
index 079d578b4..005d9abca 100755
--- a/build_helpers/install_ta-lib.sh
+++ b/build_helpers/install_ta-lib.sh
@@ -8,8 +8,8 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib \
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
- && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
- && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
+ && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
+ && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
&& ./configure --prefix=${INSTALL_LOC}/ \
&& make
if [ $? -ne 0 ]; then
diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh
index f3cedff2e..a6ecdbee6 100755
--- a/build_helpers/publish_docker_arm64.sh
+++ b/build_helpers/publish_docker_arm64.sh
@@ -3,6 +3,10 @@
# Use BuildKit, otherwise building on ARM fails
export DOCKER_BUILDKIT=1
+IMAGE_NAME=freqtradeorg/freqtrade
+CACHE_IMAGE=freqtradeorg/freqtrade_cache
+GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade
+
# Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
TAG_PLOT=${TAG}_plot
@@ -14,7 +18,6 @@ TAG_ARM=${TAG}_arm
TAG_PLOT_ARM=${TAG_PLOT}_arm
TAG_FREQAI_ARM=${TAG_FREQAI}_arm
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
-CACHE_IMAGE=freqtradeorg/freqtrade_cache
echo "Running for ${TAG}"
@@ -38,13 +41,13 @@ if [ $? -ne 0 ]; then
echo "failed building multiarch images"
return 1
fi
-# Tag image for upload and next build step
-docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
+# Tag image for upload and next build step
+docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
@@ -59,7 +62,6 @@ fi
docker images
-# docker push ${IMAGE_NAME}
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
@@ -82,14 +84,30 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
+# copy images to ghcr.io
+
+alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane"
+mkdir .crane
+chmod a+rwx .crane
+
+echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
+
+crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL}
+crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI}
+crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT}
+crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG}
+
# Tag as latest for develop builds
if [ "${TAG}" = "develop" ]; then
echo 'Tagging image as latest'
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
docker manifest push -p ${IMAGE_NAME}:latest
+
+ crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest
fi
docker images
+rm -rf .crane
# Cleanup old images from arm64 node.
docker image prune -a --force --filter "until=24h"
diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh
index 3e5e61564..27fa06b95 100755
--- a/build_helpers/publish_docker_multi.sh
+++ b/build_helpers/publish_docker_multi.sh
@@ -2,6 +2,8 @@
# The below assumes a correctly setup docker buildx environment
+IMAGE_NAME=freqtradeorg/freqtrade
+CACHE_IMAGE=freqtradeorg/freqtrade_cache
# Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
TAG_PLOT=${TAG}_plot
@@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi"
PI_PLATFORM="linux/arm/v7"
echo "Running for ${TAG}"
-CACHE_IMAGE=freqtradeorg/freqtrade_cache
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
# Add commit and commit_message to docker container
diff --git a/docs/bot-basics.md b/docs/bot-basics.md
index 1aa8f3085..ef5e6900b 100644
--- a/docs/bot-basics.md
+++ b/docs/bot-basics.md
@@ -60,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped.
* Load historic data for configured pairlist.
* Calls `bot_start()` once.
-* Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
* Loops per candle simulating entry and exit points.
+ * Calls `bot_loop_start()` strategy callback.
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
* Calls `adjust_entry_price()` strategy callback for open entry orders.
* Check for trade entry signals (`enter_long` / `enter_short` columns).
diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md
index f67ea8541..9822a895a 100644
--- a/docs/freqai-parameter-table.md
+++ b/docs/freqai-parameter-table.md
@@ -46,7 +46,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`.
| `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it.
**Datatype:** Boolean.
Default: `False` (no reversal).
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually.
**Datatype:** Boolean.
Default: `False`.
-| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.
**Datatype:** Boolean.
Default: `False`.
+| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.
**Datatype:** Integer.
Default: `0`.
### Data split parameters
diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md
index 04ca42a5d..f5679a4ba 100644
--- a/docs/freqai-reinforcement-learning.md
+++ b/docs/freqai-reinforcement-learning.md
@@ -55,7 +55,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from
dataframe["&-action"] = 0
```
-Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
+Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment:
```python
def feature_engineering_standard(self, dataframe, **kwargs):
@@ -248,13 +248,13 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
"""
def calculate_reward(self, action: int) -> float:
if not self._is_valid(action):
- self.tensorboard_log("is_valid")
+ self.tensorboard_log("invalid")
return -2
```
!!! Note
- The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter.
+ The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented.
### Choosing a base environment
diff --git a/docs/freqai-running.md b/docs/freqai-running.md
index 1eaee1bf2..f3ccc546f 100644
--- a/docs/freqai-running.md
+++ b/docs/freqai-running.md
@@ -128,6 +128,9 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number
You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models.
+???+ danger "Continual learning enforces a constant parameter space"
+ Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis).
+
## Hyperopt
You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md):
diff --git a/docs/includes/protections.md b/docs/includes/protections.md
index e0ad8189f..12af081c0 100644
--- a/docs/includes/protections.md
+++ b/docs/includes/protections.md
@@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour:
* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled.
* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`).
* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`).
-* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
+* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
``` python
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index d384a7ec5..7f4215aef 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==9.1.2
+mkdocs-material==9.1.4
mdx_truly_sane_lists==1.3
pymdown-extensions==9.10
jinja2==3.1.2
diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md
index f1cdc9f3b..329908527 100644
--- a/docs/strategy-callbacks.md
+++ b/docs/strategy-callbacks.md
@@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup.
## Bot loop start
-A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
+A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5
+seconds, unless configured differently) or once per candle in backtest/hyperopt mode.
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
``` python
@@ -61,11 +62,12 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods
- def bot_loop_start(self, **kwargs) -> None:
+ def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
+ :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
if self.config['runmode'].value in ('live', 'dry_run'):
diff --git a/docs/utils.md b/docs/utils.md
index 87c7f6aa6..eb675442f 100644
--- a/docs/utils.md
+++ b/docs/utils.md
@@ -955,3 +955,47 @@ Print trades with id 2 and 3 as json
``` bash
freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json
```
+
+### Strategy-Updater
+
+Updates listed strategies or all strategies within the strategies folder to be v3 compliant.
+If the command runs without --strategy-list then all strategies inside the strategies folder will be converted.
+Your original strategy will remain available in the `user_data/strategies_orig_updater/` directory.
+
+!!! Warning "Conversion results"
+ Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion.
+ We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner.
+
+```
+usage: freqtrade strategy-updater [-h] [-v] [--logfile FILE] [-V] [-c PATH]
+ [-d PATH] [--userdir PATH]
+ [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
+
+options:
+ -h, --help show this help message and exit
+ --strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
+ Provide a space-separated list of strategies to
+ backtest. Please note that timeframe needs to be set
+ either in config or via command line. When using this
+ together with `--export trades`, the strategy-name is
+ injected into the filename (so `backtest-data.json`
+ becomes `backtest-data-SampleStrategy.json`
+
+Common arguments:
+ -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
+ --logfile FILE, --log-file FILE
+ Log to the file specified. Special values are:
+ 'syslog', 'journald'. See the documentation for more
+ details.
+ -V, --version show program's version number and exit
+ -c PATH, --config PATH
+ Specify configuration file (default:
+ `userdir/config.json` or `config.json` whichever
+ exists). Multiple --config options may be used. Can be
+ set to `-` to read config from stdin.
+ -d PATH, --datadir PATH, --data-dir PATH
+ Path to directory with historical backtesting data.
+ --userdir PATH, --user-data-dir PATH
+ Path to userdata directory.
+
+```
diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py
index 788657cc8..66a9c995b 100644
--- a/freqtrade/commands/__init__.py
+++ b/freqtrade/commands/__init__.py
@@ -22,5 +22,6 @@ from freqtrade.commands.optimize_commands import (start_backtesting, start_backt
start_edge, start_hyperopt)
from freqtrade.commands.pairlist_commands import start_test_pairlist
from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit
+from freqtrade.commands.strategy_utils_commands import start_strategy_update
from freqtrade.commands.trade_commands import start_trading
from freqtrade.commands.webserver_commands import start_webserver
diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py
index 20afa7ffd..e928ccad7 100644
--- a/freqtrade/commands/analyze_commands.py
+++ b/freqtrade/commands/analyze_commands.py
@@ -40,8 +40,8 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s
if (not Path(signals_file).exists()):
raise OperationalException(
- (f"Cannot find latest backtest signals file: {signals_file}."
- "Run backtesting with `--export signals`.")
+ f"Cannot find latest backtest signals file: {signals_file}."
+ "Run backtesting with `--export signals`."
)
return config
diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index b53a1022d..47aa37fdf 100644
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -111,10 +111,13 @@ ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-freqaimodels",
"list-data", "hyperopt-list", "hyperopt-show", "backtest-filter",
- "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
+ "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv",
+ "strategy-updater"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
+ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
+
class Arguments:
"""
@@ -198,8 +201,8 @@ class Arguments:
start_list_freqAI_models, start_list_markets,
start_list_strategies, start_list_timeframes,
start_new_config, start_new_strategy, start_plot_dataframe,
- start_plot_profit, start_show_trades, start_test_pairlist,
- start_trading, start_webserver)
+ start_plot_profit, start_show_trades, start_strategy_update,
+ start_test_pairlist, start_trading, start_webserver)
subparsers = self.parser.add_subparsers(dest='command',
# Use custom message when no subhandler is added
@@ -440,3 +443,11 @@ class Arguments:
parents=[_common_parser])
webserver_cmd.set_defaults(func=start_webserver)
self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd)
+
+ # Add strategy_updater subcommand
+ strategy_updater_cmd = subparsers.add_parser('strategy-updater',
+ help='updates outdated strategy'
+ 'files to the current version',
+ parents=[_common_parser])
+ strategy_updater_cmd.set_defaults(func=start_strategy_update)
+ self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd)
diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py
index 1e74e1036..bcef1c252 100644
--- a/freqtrade/commands/data_commands.py
+++ b/freqtrade/commands/data_commands.py
@@ -204,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None:
pair, timeframe, candle_type,
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
) for pair, timeframe, candle_type in paircombs]
+
print(tabulate([
(pair, timeframe, candle_type,
start.strftime(DATETIME_PRINT_FORMAT),
end.strftime(DATETIME_PRINT_FORMAT))
- for pair, timeframe, candle_type, start, end in paircombs1
+ for pair, timeframe, candle_type, start, end in sorted(
+ paircombs1,
+ key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]))
],
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
tablefmt='psql', stralign='right'))
diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py
index c424016b1..d83605c6f 100644
--- a/freqtrade/commands/db_commands.py
+++ b/freqtrade/commands/db_commands.py
@@ -1,7 +1,7 @@
import logging
from typing import Any, Dict
-from sqlalchemy import func
+from sqlalchemy import func, select
from freqtrade.configuration.config_setup import setup_utils_configuration
from freqtrade.enums import RunMode
@@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None:
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
init_db(config['db_url'])
- session_target = Trade._session
+ session_target = Trade.session
init_db(config['db_url_from'])
logger.info("Starting db migration.")
@@ -36,16 +36,16 @@ def start_convert_db(args: Dict[str, Any]) -> None:
session_target.commit()
- for pairlock in PairLock.query:
+ for pairlock in PairLock.get_all_locks():
pairlock_count += 1
make_transient(pairlock)
session_target.add(pairlock)
session_target.commit()
# Update sequences
- max_trade_id = session_target.query(func.max(Trade.id)).scalar()
- max_order_id = session_target.query(func.max(Order.id)).scalar()
- max_pairlock_id = session_target.query(func.max(PairLock.id)).scalar()
+ max_trade_id = session_target.scalar(select(func.max(Trade.id)))
+ max_order_id = session_target.scalar(select(func.max(Order.id)))
+ max_pairlock_id = session_target.scalar(select(func.max(PairLock.id)))
set_sequence_ids(session_target.get_bind(),
trade_id=max_trade_id,
diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py
new file mode 100644
index 000000000..e579ec475
--- /dev/null
+++ b/freqtrade/commands/strategy_utils_commands.py
@@ -0,0 +1,55 @@
+import logging
+import sys
+import time
+from pathlib import Path
+from typing import Any, Dict
+
+from freqtrade.configuration import setup_utils_configuration
+from freqtrade.enums import RunMode
+from freqtrade.resolvers import StrategyResolver
+from freqtrade.strategy.strategyupdater import StrategyUpdater
+
+
+logger = logging.getLogger(__name__)
+
+
+def start_strategy_update(args: Dict[str, Any]) -> None:
+ """
+ Start the strategy updating script
+ :param args: Cli args from Arguments()
+ :return: None
+ """
+
+ if sys.version_info == (3, 8): # pragma: no cover
+ sys.exit("Freqtrade strategy updater requires Python version >= 3.9")
+
+ config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
+
+ strategy_objs = StrategyResolver.search_all_objects(
+ config, enum_failed=False, recursive=config.get('recursive_strategy_search', False))
+
+ filtered_strategy_objs = []
+ if args['strategy_list']:
+ filtered_strategy_objs = [
+ strategy_obj for strategy_obj in strategy_objs
+ if strategy_obj['name'] in args['strategy_list']
+ ]
+
+ else:
+ # Use all available entries.
+ filtered_strategy_objs = strategy_objs
+
+ processed_locations = set()
+ for strategy_obj in filtered_strategy_objs:
+ if strategy_obj['location'] not in processed_locations:
+ processed_locations.add(strategy_obj['location'])
+ start_conversion(strategy_obj, config)
+
+
+def start_conversion(strategy_obj, config):
+ print(f"Conversion of {Path(strategy_obj['location']).name} started.")
+ instance_strategy_updater = StrategyUpdater()
+ start = time.perf_counter()
+ instance_strategy_updater.start(config, strategy_obj)
+ elapsed = time.perf_counter() - start
+ print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.")
diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py
index 606f081ef..0ee48cf91 100644
--- a/freqtrade/configuration/config_validation.py
+++ b/freqtrade/configuration/config_validation.py
@@ -27,10 +27,7 @@ def _extend_validator(validator_class):
if 'default' in subschema:
instance.setdefault(prop, subschema['default'])
- for error in validate_properties(
- validator, properties, instance, schema,
- ):
- yield error
+ yield from validate_properties(validator, properties, instance, schema)
return validators.extend(
validator_class, {'properties': set_defaults}
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 46e9b5cd4..ebb946221 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -36,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
-AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
-AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5']
-AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet']
+AVAILABLE_PROTECTIONS = ['CooldownPeriod',
+ 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
+AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
+AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
BACKTEST_CACHE_DEFAULT = 'day'
diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py
index 9772506a7..3567f4112 100644
--- a/freqtrade/data/btanalysis.py
+++ b/freqtrade/data/btanalysis.py
@@ -373,7 +373,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
filters = []
if strategy:
filters.append(Trade.strategy == strategy)
- trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
+ trades = trade_list_to_dataframe(list(Trade.get_trades(filters).all()))
return trades
diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py
index 3991432a4..d05ee5db7 100644
--- a/freqtrade/data/dataprovider.py
+++ b/freqtrade/data/dataprovider.py
@@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.exchange.types import OrderBook
from freqtrade.misc import append_candles_to_dataframe
from freqtrade.rpc import RPCManager
+from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg
from freqtrade.util import PeriodicCache
@@ -118,8 +119,7 @@ class DataProvider:
:param new_candle: This is a new candle
"""
if self.__rpc:
- self.__rpc.send_msg(
- {
+ msg: RPCAnalyzedDFMsg = {
'type': RPCMessageType.ANALYZED_DF,
'data': {
'key': pair_key,
@@ -127,7 +127,7 @@ class DataProvider:
'la': datetime.now(timezone.utc)
}
}
- )
+ self.__rpc.send_msg(msg)
if new_candle:
self.__rpc.send_msg({
'type': RPCMessageType.NEW_CANDLE,
diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py
index 22a6805e7..bb387fc84 100644
--- a/freqtrade/data/history/featherdatahandler.py
+++ b/freqtrade/data/history/featherdatahandler.py
@@ -4,7 +4,7 @@ from typing import Optional
from pandas import DataFrame, read_feather, to_datetime
from freqtrade.configuration import TimeRange
-from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
+from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
from freqtrade.enums import CandleType
from .idatahandler import IDataHandler
@@ -92,12 +92,11 @@ class FeatherDataHandler(IDataHandler):
:param data: List of Lists containing trade data,
column sequence as in DEFAULT_TRADES_COLUMNS
"""
- # filename = self._pair_trades_filename(self._datadir, pair)
+ filename = self._pair_trades_filename(self._datadir, pair)
+ self.create_dir_if_needed(filename)
- raise NotImplementedError()
- # array = pa.array(data)
- # array
- # feather.write_feather(data, filename)
+ tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
+ tradesdata.to_feather(filename, compression_level=9, compression='lz4')
def trades_append(self, pair: str, data: TradeList):
"""
@@ -116,14 +115,13 @@ class FeatherDataHandler(IDataHandler):
:param timerange: Timerange to load trades for - currently not implemented
:return: List of trades
"""
- raise NotImplementedError()
- # filename = self._pair_trades_filename(self._datadir, pair)
- # tradesdata = misc.file_load_json(filename)
+ filename = self._pair_trades_filename(self._datadir, pair)
+ if not filename.exists():
+ return []
- # if not tradesdata:
- # return []
+ tradesdata = read_feather(filename)
- # return tradesdata
+ return tradesdata.values.tolist()
@classmethod
def _get_file_extension(cls):
diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py
index 9580bc690..7ac496f62 100644
--- a/freqtrade/exchange/binance.py
+++ b/freqtrade/exchange/binance.py
@@ -23,7 +23,7 @@ class Binance(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop_loss_limit"},
- "order_time_in_force": ['GTC', 'FOK', 'IOC'],
+ "order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"ohlcv_candle_limit": 1000,
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
@@ -31,6 +31,7 @@ class Binance(Exchange):
}
_ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
+ "order_time_in_force": ["GTC", "FOK", "IOC"],
"tickers_have_price": False,
"floor_leverage": True,
"stop_price_type_field": "workingType",
diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py
index 6f841b608..a4b070741 100644
--- a/freqtrade/exchange/bybit.py
+++ b/freqtrade/exchange/bybit.py
@@ -114,7 +114,7 @@ class Bybit(Exchange):
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return data
- def _lev_prep(self, pair: str, leverage: float, side: BuySell):
+ def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT:
params = {'leverage': leverage}
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 489dc1b68..92eb4b58a 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -60,7 +60,6 @@ class Exchange:
_ft_has_default: Dict = {
"stoploss_on_exchange": False,
"order_time_in_force": ["GTC"],
- "time_in_force_parameter": "timeInForce",
"ohlcv_params": {},
"ohlcv_candle_limit": 500,
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
@@ -206,6 +205,8 @@ class Exchange:
and self._api_async.session):
logger.debug("Closing async ccxt session.")
self.loop.run_until_complete(self._api_async.close())
+ if self.loop and not self.loop.is_closed():
+ self.loop.close()
def validate_config(self, config):
# Check if timeframe is available
@@ -1019,10 +1020,10 @@ class Exchange:
# Order handling
- def _lev_prep(self, pair: str, leverage: float, side: BuySell):
+ def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT:
- self.set_margin_mode(pair, self.margin_mode)
- self._set_leverage(leverage, pair)
+ self.set_margin_mode(pair, self.margin_mode, accept_fail)
+ self._set_leverage(leverage, pair, accept_fail)
def _get_params(
self,
@@ -1034,8 +1035,7 @@ class Exchange:
) -> Dict:
params = self._params.copy()
if time_in_force != 'GTC' and ordertype != 'market':
- param = self._ft_has.get('time_in_force_parameter', '')
- params.update({param: time_in_force.upper()})
+ params.update({'timeInForce': time_in_force.upper()})
if reduceOnly:
params.update({'reduceOnly': True})
return params
@@ -1137,7 +1137,11 @@ class Exchange:
"sell" else (stop_price >= limit_rate))
# Ensure rate is less than stop price
if bad_stop_price:
- raise OperationalException(
+ # This can for example happen if the stop / liquidation price is set to 0
+ # Which is possible if a market-order closes right away.
+ # The InvalidOrderException will bubble up to exit_positions, where it will be
+ # handled gracefully.
+ raise InvalidOrderException(
"In stoploss limit order, stop price should be more than limit price. "
f"Stop price: {stop_price}, Limit price: {limit_rate}, "
f"Limit Price pct: {limit_price_pct}"
@@ -1204,7 +1208,7 @@ class Exchange:
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
- self._lev_prep(pair, leverage, side)
+ self._lev_prep(pair, leverage, side, accept_fail=True)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=limit_rate, params=params)
self._log_exchange_response('create_stoploss_order', order)
@@ -2529,7 +2533,6 @@ class Exchange:
self,
leverage: float,
pair: Optional[str] = None,
- trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
@@ -2547,7 +2550,7 @@ class Exchange:
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
- except ccxt.BadRequest as e:
+ except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
if not accept_fail:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py
index 03b568460..bf6d5b59c 100644
--- a/freqtrade/exchange/gate.py
+++ b/freqtrade/exchange/gate.py
@@ -75,8 +75,7 @@ class Gate(Exchange):
)
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
params['type'] = 'market'
- param = self._ft_has.get('time_in_force_parameter', '')
- params.update({param: 'IOC'})
+ params.update({'timeInForce': 'IOC'})
return params
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py
index 8a4f7f7e0..b1a19fa69 100644
--- a/freqtrade/exchange/kraken.py
+++ b/freqtrade/exchange/kraken.py
@@ -158,7 +158,6 @@ class Kraken(Exchange):
self,
leverage: float,
pair: Optional[str] = None,
- trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py
index e7d658d24..a4fcaeca0 100644
--- a/freqtrade/exchange/okx.py
+++ b/freqtrade/exchange/okx.py
@@ -1,14 +1,16 @@
import logging
-from typing import Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
-from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
+from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError,
+ TemporaryError)
from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier
+from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
@@ -24,11 +26,13 @@ class Okx(Exchange):
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
+ "stoploss_order_types": {"limit": "limit"},
+ "stoploss_on_exchange": True,
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
- "stop_price_type_field": "tpTriggerPxType",
+ "stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
@@ -121,10 +125,9 @@ class Okx(Exchange):
return params
@retrier
- def _lev_prep(self, pair: str, leverage: float, side: BuySell):
+ def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
- # TODO-lev: Test me properly (check mgnMode passed)
res = self._api.set_leverage(
leverage=leverage,
symbol=pair,
@@ -157,3 +160,78 @@ class Okx(Exchange):
pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['maxNotional'] / leverage
+
+ def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
+
+ params = self._params.copy()
+ # Verify if stopPrice works for your exchange!
+ params.update({'stopLossPrice': stop_price})
+
+ if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
+ params['tdMode'] = self.margin_mode.value
+ params['posSide'] = self._get_posSide(side, True)
+ return params
+
+ def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
+ """
+ OKX uses non-default stoploss price naming.
+ """
+ if not self._ft_has.get('stoploss_on_exchange'):
+ raise OperationalException(f"stoploss is not implemented for {self.name}.")
+
+ return (
+ order.get('stopLossPrice', None) is None
+ or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or
+ (side == "buy" and stop_loss < float(order['stopLossPrice'])))
+ )
+
+ def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
+ if self._config['dry_run']:
+ return self.fetch_dry_run_order(order_id)
+
+ try:
+ params1 = {'stop': True}
+ order_reg = self._api.fetch_order(order_id, pair, params=params1)
+ self._log_exchange_response('fetch_stoploss_order', order_reg)
+ return order_reg
+ except ccxt.OrderNotFound:
+ pass
+ params2 = {'stop': True, 'ordType': 'conditional'}
+ for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders,
+ self._api.fetch_canceled_orders):
+ try:
+ orders = method(pair, params=params2)
+ orders_f = [order for order in orders if order['id'] == order_id]
+ if orders_f:
+ order = orders_f[0]
+ if (order['status'] == 'closed'
+ and (real_order_id := order.get('info', {}).get('ordId')) is not None):
+ # Once a order triggered, we fetch the regular followup order.
+ order_reg = self.fetch_order(real_order_id, pair)
+ self._log_exchange_response('fetch_stoploss_order1', order_reg)
+ order_reg['id_stop'] = order_reg['id']
+ order_reg['id'] = order_id
+ order_reg['type'] = 'stoploss'
+ order_reg['status_stop'] = 'triggered'
+ return order_reg
+ order['type'] = 'stoploss'
+ return order
+ except ccxt.BaseError:
+ pass
+ raise RetryableOrderError(
+ f'StoplossOrder not found (pair: {pair} id: {order_id}).')
+
+ def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
+ if order['type'] == 'stop':
+ return safe_value_fallback2(order, order, 'id_stop', 'id')
+ return order['id']
+
+ def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
+ params1 = {'stop': True}
+ # 'ordType': 'conditional'
+ #
+ return self.cancel_order(
+ order_id=order_id,
+ pair=pair,
+ params=params1,
+ )
diff --git a/freqtrade/freqai/RL/Base3ActionRLEnv.py b/freqtrade/freqai/RL/Base3ActionRLEnv.py
index 83682263b..de7953226 100644
--- a/freqtrade/freqai/RL/Base3ActionRLEnv.py
+++ b/freqtrade/freqai/RL/Base3ActionRLEnv.py
@@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action)
self.total_reward += step_reward
- self.tensorboard_log(self.actions._member_names_[action])
+ self.tensorboard_log(self.actions._member_names_[action], category="actions")
trade_type = None
if self.is_tradesignal(action):
diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py
index b26ba988a..227122eaa 100644
--- a/freqtrade/freqai/RL/Base4ActionRLEnv.py
+++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py
@@ -48,7 +48,7 @@ class Base4ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action)
self.total_reward += step_reward
- self.tensorboard_log(self.actions._member_names_[action])
+ self.tensorboard_log(self.actions._member_names_[action], category="actions")
trade_type = None
if self.is_tradesignal(action):
diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py
index 6ce598dfb..70053858d 100644
--- a/freqtrade/freqai/RL/Base5ActionRLEnv.py
+++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py
@@ -49,7 +49,7 @@ class Base5ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action)
self.total_reward += step_reward
- self.tensorboard_log(self.actions._member_names_[action])
+ self.tensorboard_log(self.actions._member_names_[action], category="actions")
trade_type = None
if self.is_tradesignal(action):
diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py
index 60b65cc03..081d41202 100644
--- a/freqtrade/freqai/RL/BaseEnvironment.py
+++ b/freqtrade/freqai/RL/BaseEnvironment.py
@@ -137,7 +137,8 @@ class BaseEnvironment(gym.Env):
self.np_random, seed = seeding.np_random(seed)
return [seed]
- def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True):
+ def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None,
+ inc: Optional[bool] = None, category: str = "custom"):
"""
Function builds the tensorboard_metrics dictionary
to be parsed by the TensorboardCallback. This
@@ -149,17 +150,24 @@ class BaseEnvironment(gym.Env):
def calculate_reward(self, action: int) -> float:
if not self._is_valid(action):
- self.tensorboard_log("is_valid")
+ self.tensorboard_log("invalid")
return -2
:param metric: metric to be tracked and incremented
- :param value: value to increment `metric` by
- :param inc: sets whether the `value` is incremented or not
+ :param value: `metric` value
+ :param inc: (deprecated) sets whether the `value` is incremented or not
+ :param category: `metric` category
"""
- if not inc or metric not in self.tensorboard_metrics:
- self.tensorboard_metrics[metric] = value
+ increment = True if value is None else False
+ value = 1 if increment else value
+
+ if category not in self.tensorboard_metrics:
+ self.tensorboard_metrics[category] = {}
+
+ if not increment or metric not in self.tensorboard_metrics[category]:
+ self.tensorboard_metrics[category][metric] = value
else:
- self.tensorboard_metrics[metric] += value
+ self.tensorboard_metrics[category][metric] += value
def reset_tensorboard_log(self):
self.tensorboard_metrics = {}
diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py
index b596742e9..7f8c76956 100644
--- a/freqtrade/freqai/RL/TensorboardCallback.py
+++ b/freqtrade/freqai/RL/TensorboardCallback.py
@@ -13,7 +13,7 @@ class TensorboardCallback(BaseCallback):
episodic summary reports.
"""
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
- super(TensorboardCallback, self).__init__(verbose)
+ super().__init__(verbose)
self.model: Any = None
self.logger = None # type: Any
self.training_env: BaseEnvironment = None # type: ignore
@@ -46,14 +46,12 @@ class TensorboardCallback(BaseCallback):
local_info = self.locals["infos"][0]
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
- for info in local_info:
- if info not in ["episode", "terminal_observation"]:
- self.logger.record(f"_info/{info}", local_info[info])
+ for metric in local_info:
+ if metric not in ["episode", "terminal_observation"]:
+ self.logger.record(f"info/{metric}", local_info[metric])
- for info in tensorboard_metrics:
- if info in [action.name for action in self.actions]:
- self.logger.record(f"_actions/{info}", tensorboard_metrics[info])
- else:
- self.logger.record(f"_custom/{info}", tensorboard_metrics[info])
+ for category in tensorboard_metrics:
+ for metric in tensorboard_metrics[category]:
+ self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][metric])
return True
diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py
index 66923b5c2..52d487b08 100644
--- a/freqtrade/freqai/data_kitchen.py
+++ b/freqtrade/freqai/data_kitchen.py
@@ -251,7 +251,7 @@ class FreqaiDataKitchen:
(drop_index == 0) & (drop_index_labels == 0)
]
logger.info(
- f"dropped {len(unfiltered_df) - len(filtered_df)} training points"
+ f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
f" due to NaNs in populated dataset {len(unfiltered_df)}."
)
if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live:
@@ -675,7 +675,7 @@ class FreqaiDataKitchen:
]
logger.info(
- f"SVM tossed {len(y_pred) - kept_points.sum()}"
+ f"{self.pair}: SVM tossed {len(y_pred) - kept_points.sum()}"
f" test points from {len(y_pred)} total points."
)
@@ -949,7 +949,7 @@ class FreqaiDataKitchen:
if (len(do_predict) - do_predict.sum()) > 0:
logger.info(
- f"DI tossed {len(do_predict) - do_predict.sum()} predictions for "
+ f"{self.pair}: DI tossed {len(do_predict) - do_predict.sum()} predictions for "
"being too far from training data."
)
diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py
index 884849446..b657bd811 100644
--- a/freqtrade/freqai/freqai_interface.py
+++ b/freqtrade/freqai/freqai_interface.py
@@ -104,6 +104,10 @@ class IFreqaiModel(ABC):
self.data_provider: Optional[DataProvider] = None
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
self.can_short = True # overridden in start() with strategy.can_short
+ self.model: Any = None
+ if self.ft_params.get('principal_component_analysis', False) and self.continual_learning:
+ self.ft_params.update({'principal_component_analysis': False})
+ logger.warning('User tried to use PCA with continual learning. Deactivating PCA.')
record_params(config, self.full_path)
@@ -153,8 +157,7 @@ class IFreqaiModel(ABC):
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
dataframe = dk.remove_features_from_df(dk.return_dataframe)
else:
- logger.info(
- "Backtesting using historic predictions (live models)")
+ logger.info("Backtesting using historic predictions (live models)")
dk = self.start_backtesting_from_historic_predictions(
dataframe, metadata, self.dk)
dataframe = dk.return_dataframe
@@ -338,13 +341,14 @@ class IFreqaiModel(ABC):
except Exception as msg:
logger.warning(
f"Training {pair} raised exception {msg.__class__.__name__}. "
- f"Message: {msg}, skipping.")
+ f"Message: {msg}, skipping.", exc_info=True)
+ self.model = None
self.dd.pair_dict[pair]["trained_timestamp"] = int(
tr_train.stopts)
- if self.plot_features:
+ if self.plot_features and self.model is not None:
plot_feature_importance(self.model, pair, dk, self.plot_features)
- if self.save_backtest_models:
+ if self.save_backtest_models and self.model is not None:
logger.info('Saving backtest model to disk.')
self.dd.save_data(self.model, pair, dk)
else:
diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py
index 2a87151f9..e795703d4 100644
--- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py
+++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py
@@ -100,7 +100,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
"""
# first, penalize if the action is not valid
if not self._is_valid(action):
- self.tensorboard_log("is_valid")
+ self.tensorboard_log("invalid", category="actions")
return -2
pnl = self.get_unrealized_profit()
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index dfac11347..42db121e9 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -30,6 +30,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
+from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
+ RPCSellMsg)
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise
@@ -212,7 +214,8 @@ class FreqtradeBot(LoggingMixin):
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
self.strategy.gather_informative_pairs())
- strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
+ strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
+ current_time=datetime.now(timezone.utc))
self.strategy.analyze(self.active_pair_whitelist)
@@ -594,7 +597,7 @@ class FreqtradeBot(LoggingMixin):
stake_available = self.wallets.get_available_stake_amount()
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
- default_retval=None)(
+ default_retval=None, supress_error=True)(
trade=trade,
current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
current_profit=current_entry_profit, min_stake=min_entry_stake,
@@ -810,6 +813,9 @@ class FreqtradeBot(LoggingMixin):
precision_mode=self.exchange.precisionMode,
contract_size=self.exchange.get_contract_size(pair),
)
+ stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
+ trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True)
+
else:
# This is additional buy, we reset fee_open_currency so timeout checking can work
trade.is_open = True
@@ -819,7 +825,7 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj)
trade.recalc_trade_from_orders()
- Trade.query.session.add(trade)
+ Trade.session.add(trade)
Trade.commit()
# Updating wallets
@@ -851,7 +857,8 @@ class FreqtradeBot(LoggingMixin):
# Reset stoploss order id.
trade.stoploss_order_id = None
except InvalidOrderException:
- logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
+ logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} "
+ f"for pair {trade.pair}")
return trade
def get_valid_enter_price_and_stake(
@@ -943,7 +950,6 @@ class FreqtradeBot(LoggingMixin):
"""
Sends rpc notification when a entry order occurred.
"""
- msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
open_rate = order.safe_price
if open_rate is None:
@@ -954,9 +960,9 @@ class FreqtradeBot(LoggingMixin):
current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
- msg = {
+ msg: RPCBuyMsg = {
'trade_id': trade.id,
- 'type': msg_type,
+ 'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag,
'exchange': trade.exchange.capitalize(),
@@ -968,6 +974,7 @@ class FreqtradeBot(LoggingMixin):
'order_type': order_type,
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'],
+ 'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
'open_date': trade.open_date or datetime.utcnow(),
@@ -986,7 +993,7 @@ class FreqtradeBot(LoggingMixin):
current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False)
- msg = {
+ msg: RPCCancelMsg = {
'trade_id': trade.id,
'type': RPCMessageType.ENTRY_CANCEL,
'buy_tag': trade.enter_tag,
@@ -998,7 +1005,9 @@ class FreqtradeBot(LoggingMixin):
'limit': trade.open_rate,
'order_type': order_type,
'stake_amount': trade.stake_amount,
+ 'open_rate': trade.open_rate,
'stake_currency': self.config['stake_currency'],
+ 'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': trade.amount,
'open_date': trade.open_date,
@@ -1021,12 +1030,16 @@ class FreqtradeBot(LoggingMixin):
trades_closed = 0
for trade in trades:
try:
+ try:
+ if (self.strategy.order_types.get('stoploss_on_exchange') and
+ self.handle_stoploss_on_exchange(trade)):
+ trades_closed += 1
+ Trade.commit()
+ continue
- if (self.strategy.order_types.get('stoploss_on_exchange') and
- self.handle_stoploss_on_exchange(trade)):
- trades_closed += 1
- Trade.commit()
- continue
+ except InvalidOrderException as exception:
+ logger.warning(
+ f'Unable to handle stoploss on exchange for {trade.pair}: {exception}')
# Check if we can sell our current pair
if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
trades_closed += 1
@@ -1232,13 +1245,8 @@ class FreqtradeBot(LoggingMixin):
# cancelling the current stoploss on exchange first
logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
f"(orderid:{order['id']}) in order to add another one ...")
- try:
- co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
- trade.amount)
- trade.update_order(co)
- except InvalidOrderException:
- logger.exception(f"Could not cancel stoploss order {order['id']} "
- f"for pair {trade.pair}")
+
+ self.cancel_stoploss_on_exchange(trade)
# Create new stoploss order
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
@@ -1659,7 +1667,7 @@ class FreqtradeBot(LoggingMixin):
amount = trade.amount
gain = "profit" if profit_ratio > 0 else "loss"
- msg = {
+ msg: RPCSellMsg = {
'type': (RPCMessageType.EXIT_FILL if fill
else RPCMessageType.EXIT),
'trade_id': trade.id,
@@ -1685,6 +1693,7 @@ class FreqtradeBot(LoggingMixin):
'close_date': trade.close_date or datetime.utcnow(),
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'],
+ 'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency'),
'sub_trade': sub_trade,
'cumulative_profit': trade.realized_profit,
@@ -1715,7 +1724,7 @@ class FreqtradeBot(LoggingMixin):
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
- msg = {
+ msg: RPCSellCancelMsg = {
'type': RPCMessageType.EXIT_CANCEL,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(),
@@ -1737,6 +1746,7 @@ class FreqtradeBot(LoggingMixin):
'open_date': trade.open_date,
'close_date': trade.close_date or datetime.now(timezone.utc),
'stake_currency': self.config['stake_currency'],
+ 'base_currency': self.exchange.get_pair_base_currency(trade.pair),
'fiat_currency': self.config.get('fiat_display_currency', None),
'reason': reason,
'sub_trade': sub_trade,
@@ -1844,14 +1854,20 @@ class FreqtradeBot(LoggingMixin):
self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig:
- msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
- msg.update(prot_trig.to_json())
+ msg: RPCProtectionMsg = {
+ 'type': RPCMessageType.PROTECTION_TRIGGER,
+ 'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair),
+ **prot_trig.to_json() # type: ignore
+ }
self.rpc.send_msg(msg)
prot_trig_glb = self.protections.global_stop(side=side)
if prot_trig_glb:
- msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
- msg.update(prot_trig_glb.to_json())
+ msg = {
+ 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
+ 'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair),
+ **prot_trig_glb.to_json() # type: ignore
+ }
self.rpc.send_msg(msg)
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index 5e1e9b48a..c7b2a0d3c 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -203,9 +203,10 @@ class Backtesting:
# since a "perfect" stoploss-exit is assumed anyway
# And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False
+ # Update can_short flag
+ self._can_short = self.trading_mode != TradingMode.SPOT and strategy.can_short
self.strategy.ft_bot_start()
- strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False):
@@ -442,10 +443,6 @@ class Backtesting:
# Worst case: price ticks tiny bit above open and dives down.
stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(
(trade.stop_loss_pct or 0.0) / leverage))
- if is_short:
- assert stop_rate > row[LOW_IDX]
- else:
- assert stop_rate < row[HIGH_IDX]
# Limit lower-end to candle low to avoid exits below the low.
# This still remains "worst case" - but "worst realistic case".
@@ -526,7 +523,7 @@ class Backtesting:
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
stake_available = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
- default_retval=None)(
+ default_retval=None, supress_error=True)(
trade=trade, # type: ignore[arg-type]
current_time=current_date, current_rate=current_rate,
current_profit=current_profit, min_stake=min_stake,
@@ -744,7 +741,7 @@ class Backtesting:
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction, entry_tag=entry_tag,
- ) if self._can_short else 1.0
+ ) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
@@ -1034,6 +1031,9 @@ class Backtesting:
requested_stake=(
order.safe_remaining * order.ft_price / trade.leverage),
direction='short' if trade.is_short else 'long')
+ # Delete trade if no successful entries happened (if placing the new order failed)
+ if trade.open_order_id is None and trade.nr_of_successful_entries == 0:
+ return True
self.replaced_entry_orders += 1
else:
# assumption: there can't be multiple open entry orders at any given time
@@ -1159,6 +1159,8 @@ class Backtesting:
while current_time <= end_date:
open_trade_count_start = LocalTrade.bt_open_open_trade_count
self.check_abort()
+ strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
+ current_time=current_time)
for i, pair in enumerate(data):
row_index = indexes[pair]
row = self.validate_row(data, pair, row_index, current_time)
diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
index cf0650f7d..e2133a956 100644
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -1,4 +1,3 @@
-import io
import logging
from copy import deepcopy
from datetime import datetime, timezone
@@ -464,8 +463,8 @@ class HyperoptTools():
return
try:
- io.open(csv_file, 'w+').close()
- except IOError:
+ Path(csv_file).open('w+').close()
+ except OSError:
logger.error(f"Failed to create CSV file: {csv_file}")
return
diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index d718af2f4..2315c0acc 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -2,7 +2,9 @@
This module contains the class to persist trades into SQLite
"""
import logging
-from typing import Any, Dict
+import threading
+from contextvars import ContextVar
+from typing import Any, Dict, Final, Optional
from sqlalchemy import create_engine, inspect
from sqlalchemy.exc import NoSuchModuleError
@@ -19,6 +21,22 @@ from freqtrade.persistence.trade_model import Order, Trade
logger = logging.getLogger(__name__)
+REQUEST_ID_CTX_KEY: Final[str] = 'request_id'
+_request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(REQUEST_ID_CTX_KEY, default=None)
+
+
+def get_request_or_thread_id() -> Optional[str]:
+ """
+ Helper method to get either async context (for fastapi requests), or thread id
+ """
+ id = _request_id_ctx_var.get()
+ if id is None:
+ # when not in request context - use thread id
+ id = str(threading.current_thread().ident)
+
+ return id
+
+
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
@@ -53,13 +71,11 @@ def init_db(db_url: str) -> None:
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
# Scoped sessions proxy requests to the appropriate thread-local session.
- # We should use the scoped_session object - not a seperately initialized version
- Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False))
- Order._session = Trade._session
- PairLock._session = Trade._session
- Trade.query = Trade._session.query_property()
- Order.query = Trade._session.query_property()
- PairLock.query = Trade._session.query_property()
+ # Since we also use fastAPI, we need to make it aware of the request id, too
+ Trade.session = scoped_session(sessionmaker(
+ bind=engine, autoflush=False), scopefunc=get_request_or_thread_id)
+ Order.session = Trade.session
+ PairLock.session = Trade.session
previous_tables = inspect(engine).get_table_names()
ModelBase.metadata.create_all(engine)
diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py
index 1e5699145..1b254c2b2 100644
--- a/freqtrade/persistence/pairlock.py
+++ b/freqtrade/persistence/pairlock.py
@@ -1,8 +1,8 @@
from datetime import datetime, timezone
from typing import Any, ClassVar, Dict, Optional
-from sqlalchemy import String, or_
-from sqlalchemy.orm import Mapped, Query, QueryPropertyDescriptor, mapped_column
+from sqlalchemy import ScalarResult, String, or_, select
+from sqlalchemy.orm import Mapped, mapped_column
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.persistence.base import ModelBase, SessionType
@@ -13,8 +13,7 @@ class PairLock(ModelBase):
Pair Locks database model.
"""
__tablename__ = 'pairlocks'
- query: ClassVar[QueryPropertyDescriptor]
- _session: ClassVar[SessionType]
+ session: ClassVar[SessionType]
id: Mapped[int] = mapped_column(primary_key=True)
@@ -37,7 +36,8 @@ class PairLock(ModelBase):
f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})')
@staticmethod
- def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> Query:
+ def query_pair_locks(
+ pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']:
"""
Get all currently active locks for this pair
:param pair: Pair to check for. Returns all current locks if pair is empty
@@ -53,9 +53,11 @@ class PairLock(ModelBase):
else:
filters.append(PairLock.side == '*')
- return PairLock.query.filter(
- *filters
- )
+ return PairLock.session.scalars(select(PairLock).filter(*filters))
+
+ @staticmethod
+ def get_all_locks() -> ScalarResult['PairLock']:
+ return PairLock.session.scalars(select(PairLock))
def to_json(self) -> Dict[str, Any]:
return {
diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py
index 5ed131a9b..29169a50d 100644
--- a/freqtrade/persistence/pairlock_middleware.py
+++ b/freqtrade/persistence/pairlock_middleware.py
@@ -1,6 +1,8 @@
import logging
from datetime import datetime, timezone
-from typing import List, Optional
+from typing import List, Optional, Sequence
+
+from sqlalchemy import select
from freqtrade.exchange import timeframe_to_next_date
from freqtrade.persistence.models import PairLock
@@ -51,15 +53,15 @@ class PairLocks():
active=True
)
if PairLocks.use_db:
- PairLock.query.session.add(lock)
- PairLock.query.session.commit()
+ PairLock.session.add(lock)
+ PairLock.session.commit()
else:
PairLocks.locks.append(lock)
return lock
@staticmethod
- def get_pair_locks(
- pair: Optional[str], now: Optional[datetime] = None, side: str = '*') -> List[PairLock]:
+ def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None,
+ side: str = '*') -> Sequence[PairLock]:
"""
Get all currently active locks for this pair
:param pair: Pair to check for. Returns all current locks if pair is empty
@@ -106,7 +108,7 @@ class PairLocks():
for lock in locks:
lock.active = False
if PairLocks.use_db:
- PairLock.query.session.commit()
+ PairLock.session.commit()
@staticmethod
def unlock_reason(reason: str, now: Optional[datetime] = None) -> None:
@@ -126,11 +128,11 @@ class PairLocks():
PairLock.active.is_(True),
PairLock.reason == reason
]
- locks = PairLock.query.filter(*filters)
+ locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all()
for lock in locks:
logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.")
lock.active = False
- PairLock.query.session.commit()
+ PairLock.session.commit()
else:
# used in backtesting mode; don't show log messages for speed
locksb = PairLocks.get_pair_locks(None)
@@ -165,11 +167,11 @@ class PairLocks():
)
@staticmethod
- def get_all_locks() -> List[PairLock]:
+ def get_all_locks() -> Sequence[PairLock]:
"""
Return all locks, also locks with expired end date
"""
if PairLocks.use_db:
- return PairLock.query.all()
+ return PairLock.get_all_locks().all()
else:
return PairLocks.locks
diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
index 19d71aa7b..17117d436 100644
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -5,11 +5,11 @@ import logging
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from math import isclose
-from typing import Any, ClassVar, Dict, List, Optional, cast
+from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
-from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func
-from sqlalchemy.orm import (Mapped, Query, QueryPropertyDescriptor, lazyload, mapped_column,
- relationship)
+from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String,
+ UniqueConstraint, desc, func, select)
+from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship
from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES,
BuySell, LongShort)
@@ -36,8 +36,7 @@ class Order(ModelBase):
Mirrors CCXT Order structure
"""
__tablename__ = 'orders'
- query: ClassVar[QueryPropertyDescriptor]
- _session: ClassVar[SessionType]
+ session: ClassVar[SessionType]
# Uniqueness should be ensured over pair, order_id
# its likely that order_id is unique per Pair on some exchanges.
@@ -263,12 +262,12 @@ class Order(ModelBase):
return o
@staticmethod
- def get_open_orders() -> List['Order']:
+ def get_open_orders() -> Sequence['Order']:
"""
Retrieve open orders from the database
:return: List of open orders
"""
- return Order.query.filter(Order.ft_is_open.is_(True)).all()
+ return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all()
@staticmethod
def order_by_id(order_id: str) -> Optional['Order']:
@@ -276,7 +275,7 @@ class Order(ModelBase):
Retrieve order based on order_id
:return: Order or None
"""
- return Order.query.filter(Order.order_id == order_id).first()
+ return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first()
class LocalTrade():
@@ -561,6 +560,9 @@ class LocalTrade():
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id,
+ 'amount_precision': self.amount_precision,
+ 'price_precision': self.price_precision,
+ 'precision_mode': self.precision_mode,
'orders': orders,
}
@@ -1153,7 +1155,9 @@ class LocalTrade():
get open trade count
"""
if Trade.use_db:
- return Trade.query.filter(Trade.is_open.is_(True)).count()
+ return Trade.session.execute(
+ select(func.count(Trade.id)).filter(Trade.is_open.is_(True))
+ ).scalar_one()
else:
return LocalTrade.bt_open_open_trade_count
@@ -1186,8 +1190,7 @@ class Trade(ModelBase, LocalTrade):
Note: Fields must be aligned with LocalTrade class
"""
__tablename__ = 'trades'
- query: ClassVar[QueryPropertyDescriptor]
- _session: ClassVar[SessionType]
+ session: ClassVar[SessionType]
use_db: bool = True
@@ -1287,18 +1290,18 @@ class Trade(ModelBase, LocalTrade):
def delete(self) -> None:
for order in self.orders:
- Order.query.session.delete(order)
+ Order.session.delete(order)
- Trade.query.session.delete(self)
+ Trade.session.delete(self)
Trade.commit()
@staticmethod
def commit():
- Trade.query.session.commit()
+ Trade.session.commit()
@staticmethod
def rollback():
- Trade.query.session.rollback()
+ Trade.session.rollback()
@staticmethod
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
@@ -1332,7 +1335,7 @@ class Trade(ModelBase, LocalTrade):
)
@staticmethod
- def get_trades(trade_filter=None, include_orders: bool = True) -> Query['Trade']:
+ def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select:
"""
Helper function to query Trades using filters.
NOTE: Not supported in Backtesting.
@@ -1347,22 +1350,35 @@ class Trade(ModelBase, LocalTrade):
if trade_filter is not None:
if not isinstance(trade_filter, list):
trade_filter = [trade_filter]
- this_query = Trade.query.filter(*trade_filter)
+ this_query = select(Trade).filter(*trade_filter)
else:
- this_query = Trade.query
+ this_query = select(Trade)
if not include_orders:
# Don't load order relations
# Consider using noload or raiseload instead of lazyload
this_query = this_query.options(lazyload(Trade.orders))
return this_query
+ @staticmethod
+ def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']:
+ """
+ Helper function to query Trades using filters.
+ NOTE: Not supported in Backtesting.
+ :param trade_filter: Optional filter to apply to trades
+ Can be either a Filter object, or a List of filters
+ e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
+ e.g. `(trade_filter=Trade.id == trade_id)`
+ :return: unsorted query object
+ """
+ return Trade.session.scalars(Trade.get_trades_query(trade_filter, include_orders))
+
@staticmethod
def get_open_order_trades() -> List['Trade']:
"""
Returns all open trades
NOTE: Not supported in Backtesting.
"""
- return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
+ return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all())
@staticmethod
def get_open_trades_without_assigned_fees():
@@ -1392,11 +1408,12 @@ class Trade(ModelBase, LocalTrade):
Retrieves total realized profit
"""
if Trade.use_db:
- total_profit = Trade.query.with_entities(
- func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar()
+ total_profit: float = Trade.session.execute(
+ select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False))
+ ).scalar_one()
else:
- total_profit = sum(
- t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False))
+ total_profit = sum(t.close_profit_abs # type: ignore
+ for t in LocalTrade.get_trades_proxy(is_open=False))
return total_profit or 0
@staticmethod
@@ -1406,8 +1423,9 @@ class Trade(ModelBase, LocalTrade):
in stake currency
"""
if Trade.use_db:
- total_open_stake_amount = Trade.query.with_entities(
- func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
+ total_open_stake_amount = Trade.session.scalar(
+ select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True))
+ )
else:
total_open_stake_amount = sum(
t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
@@ -1423,15 +1441,18 @@ class Trade(ModelBase, LocalTrade):
if minutes:
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
filters.append(Trade.close_date >= start_date)
- pair_rates = Trade.query.with_entities(
- Trade.pair,
- func.sum(Trade.close_profit).label('profit_sum'),
- func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
- func.count(Trade.pair).label('count')
- ).filter(*filters)\
- .group_by(Trade.pair) \
- .order_by(desc('profit_sum_abs')) \
- .all()
+
+ pair_rates = Trade.session.execute(
+ select(
+ Trade.pair,
+ func.sum(Trade.close_profit).label('profit_sum'),
+ func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
+ func.count(Trade.pair).label('count')
+ ).filter(*filters)
+ .group_by(Trade.pair)
+ .order_by(desc('profit_sum_abs'))
+ ).all()
+
return [
{
'pair': pair,
@@ -1456,15 +1477,16 @@ class Trade(ModelBase, LocalTrade):
if (pair is not None):
filters.append(Trade.pair == pair)
- enter_tag_perf = Trade.query.with_entities(
- Trade.enter_tag,
- func.sum(Trade.close_profit).label('profit_sum'),
- func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
- func.count(Trade.pair).label('count')
- ).filter(*filters)\
- .group_by(Trade.enter_tag) \
- .order_by(desc('profit_sum_abs')) \
- .all()
+ enter_tag_perf = Trade.session.execute(
+ select(
+ Trade.enter_tag,
+ func.sum(Trade.close_profit).label('profit_sum'),
+ func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
+ func.count(Trade.pair).label('count')
+ ).filter(*filters)
+ .group_by(Trade.enter_tag)
+ .order_by(desc('profit_sum_abs'))
+ ).all()
return [
{
@@ -1488,16 +1510,16 @@ class Trade(ModelBase, LocalTrade):
filters: List = [Trade.is_open.is_(False)]
if (pair is not None):
filters.append(Trade.pair == pair)
-
- sell_tag_perf = Trade.query.with_entities(
- Trade.exit_reason,
- func.sum(Trade.close_profit).label('profit_sum'),
- func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
- func.count(Trade.pair).label('count')
- ).filter(*filters)\
- .group_by(Trade.exit_reason) \
- .order_by(desc('profit_sum_abs')) \
- .all()
+ sell_tag_perf = Trade.session.execute(
+ select(
+ Trade.exit_reason,
+ func.sum(Trade.close_profit).label('profit_sum'),
+ func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
+ func.count(Trade.pair).label('count')
+ ).filter(*filters)
+ .group_by(Trade.exit_reason)
+ .order_by(desc('profit_sum_abs'))
+ ).all()
return [
{
@@ -1521,18 +1543,18 @@ class Trade(ModelBase, LocalTrade):
filters: List = [Trade.is_open.is_(False)]
if (pair is not None):
filters.append(Trade.pair == pair)
-
- mix_tag_perf = Trade.query.with_entities(
- Trade.id,
- Trade.enter_tag,
- Trade.exit_reason,
- func.sum(Trade.close_profit).label('profit_sum'),
- func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
- func.count(Trade.pair).label('count')
- ).filter(*filters)\
- .group_by(Trade.id) \
- .order_by(desc('profit_sum_abs')) \
- .all()
+ mix_tag_perf = Trade.session.execute(
+ select(
+ Trade.id,
+ Trade.enter_tag,
+ Trade.exit_reason,
+ func.sum(Trade.close_profit).label('profit_sum'),
+ func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
+ func.count(Trade.pair).label('count')
+ ).filter(*filters)
+ .group_by(Trade.id)
+ .order_by(desc('profit_sum_abs'))
+ ).all()
return_list: List[Dict] = []
for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf:
@@ -1568,11 +1590,15 @@ class Trade(ModelBase, LocalTrade):
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
- best_pair = Trade.query.with_entities(
- Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
- ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \
- .group_by(Trade.pair) \
- .order_by(desc('profit_sum')).first()
+ best_pair = Trade.session.execute(
+ select(
+ Trade.pair,
+ func.sum(Trade.close_profit).label('profit_sum')
+ ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))
+ .group_by(Trade.pair)
+ .order_by(desc('profit_sum'))
+ ).first()
+
return best_pair
@staticmethod
@@ -1582,12 +1608,13 @@ class Trade(ModelBase, LocalTrade):
NOTE: Not supported in Backtesting.
:returns: Tuple containing (pair, profit_sum)
"""
- trading_volume = Order.query.with_entities(
- func.sum(Order.cost).label('volume')
- ).filter(
- Order.order_filled_date >= start_date,
- Order.status == 'closed'
- ).scalar()
+ trading_volume = Trade.session.execute(
+ select(
+ func.sum(Order.cost).label('volume')
+ ).filter(
+ Order.order_filled_date >= start_date,
+ Order.status == 'closed'
+ )).scalar_one()
return trading_volume
@staticmethod
@@ -1636,8 +1663,10 @@ class Trade(ModelBase, LocalTrade):
stop_loss=data["stop_loss_abs"],
stop_loss_pct=data["stop_loss_ratio"],
stoploss_order_id=data["stoploss_order_id"],
- stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000,
- tz=timezone.utc) if data["stoploss_last_update"] else None),
+ stoploss_last_update=(
+ datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
+ tz=timezone.utc)
+ if data["stoploss_last_update_timestamp"] else None),
initial_stop_loss=data["initial_stop_loss_abs"],
initial_stop_loss_pct=data["initial_stop_loss_ratio"],
min_rate=data["min_rate"],
diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py
index 1b2ee44da..e415c4911 100644
--- a/freqtrade/plot/plotting.py
+++ b/freqtrade/plot/plotting.py
@@ -1,4 +1,5 @@
import logging
+from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional
@@ -635,7 +636,7 @@ def load_and_plot_trades(config: Config):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange)
strategy.ft_bot_start()
- strategy.bot_loop_start()
+ strategy.bot_loop_start(datetime.now(timezone.utc))
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange']
trades = plot_elements['trades']
diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py
index 18621ccbd..7497b27f1 100644
--- a/freqtrade/rpc/api_server/api_schemas.py
+++ b/freqtrade/rpc/api_server/api_schemas.py
@@ -276,6 +276,10 @@ class TradeSchema(BaseModel):
funding_fees: Optional[float]
trading_mode: Optional[TradingMode]
+ amount_precision: Optional[float]
+ price_precision: Optional[float]
+ precision_mode: Optional[int]
+
class OpenTradeSchema(TradeSchema):
stoploss_current_dist: Optional[float]
diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py
index aed97367b..f5b1bcd74 100644
--- a/freqtrade/rpc/api_server/deps.py
+++ b/freqtrade/rpc/api_server/deps.py
@@ -1,9 +1,11 @@
-from typing import Any, Dict, Iterator, Optional
+from typing import Any, AsyncIterator, Dict, Optional
+from uuid import uuid4
from fastapi import Depends
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade
+from freqtrade.persistence.models import _request_id_ctx_var
from freqtrade.rpc.rpc import RPC, RPCException
from .webserver import ApiServer
@@ -15,12 +17,19 @@ def get_rpc_optional() -> Optional[RPC]:
return None
-def get_rpc() -> Optional[Iterator[RPC]]:
+async def get_rpc() -> Optional[AsyncIterator[RPC]]:
+
_rpc = get_rpc_optional()
if _rpc:
+ request_id = str(uuid4())
+ ctx_token = _request_id_ctx_var.set(request_id)
Trade.rollback()
- yield _rpc
- Trade.rollback()
+ try:
+ yield _rpc
+ finally:
+ Trade.session.remove()
+ _request_id_ctx_var.reset(ctx_token)
+
else:
raise RPCException('Bot is not in the correct state')
diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py
index b53662451..8030e303b 100644
--- a/freqtrade/rpc/api_server/webserver.py
+++ b/freqtrade/rpc/api_server/webserver.py
@@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
+from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -108,7 +109,7 @@ class ApiServer(RPCHandler):
cls._has_rpc = False
cls._rpc = None
- def send_msg(self, msg: Dict[str, Any]) -> None:
+ def send_msg(self, msg: RPCSendMsg) -> None:
"""
Publish the message to the message stream
"""
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 727fc5a37..2b5eb107c 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -5,7 +5,7 @@ import logging
from abc import abstractmethod
from datetime import date, datetime, timedelta, timezone
from math import isnan
-from typing import Any, Dict, Generator, List, Optional, Tuple, Union
+from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union
import arrow
import psutil
@@ -13,6 +13,7 @@ from dateutil.relativedelta import relativedelta
from dateutil.tz import tzlocal
from numpy import NAN, inf, int64, mean
from pandas import DataFrame, NaT
+from sqlalchemy import func, select
from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
@@ -29,6 +30,7 @@ from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
+from freqtrade.rpc.rpc_types import RPCSendMsg
from freqtrade.wallets import PositionWallet, Wallet
@@ -78,7 +80,7 @@ class RPCHandler:
""" Cleanup pending module resources """
@abstractmethod
- def send_msg(self, msg: Dict[str, str]) -> None:
+ def send_msg(self, msg: RPCSendMsg) -> None:
""" Sends a message to all registered rpc modules """
@@ -122,7 +124,8 @@ class RPC:
if config['max_open_trades'] != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'),
- 'stoploss_on_exchange': config.get('stoploss_on_exchange', False),
+ 'stoploss_on_exchange': config.get('order_types',
+ {}).get('stoploss_on_exchange', False),
'trailing_stop': config.get('trailing_stop'),
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
@@ -158,7 +161,7 @@ class RPC:
"""
# Fetch open trades
if trade_ids:
- trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
+ trades: Sequence[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
else:
trades = Trade.get_open_trades()
@@ -339,11 +342,13 @@ class RPC:
for day in range(0, timescale):
profitday = start_date - time_offset(day)
# Only query for necessary columns for performance reasons.
- trades = Trade.query.session.query(Trade.close_profit_abs).filter(
- Trade.is_open.is_(False),
- Trade.close_date >= profitday,
- Trade.close_date < (profitday + time_offset(1))
- ).order_by(Trade.close_date).all()
+ trades = Trade.session.execute(
+ select(Trade.close_profit_abs)
+ .filter(Trade.is_open.is_(False),
+ Trade.close_date >= profitday,
+ Trade.close_date < (profitday + time_offset(1)))
+ .order_by(Trade.close_date)
+ ).all()
curdayprofit = sum(
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
@@ -381,14 +386,19 @@ class RPC:
""" Returns the X last trades """
order_by: Any = Trade.id if order_by_id else Trade.close_date.desc()
if limit:
- trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
- order_by).limit(limit).offset(offset)
+ trades = Trade.session.scalars(
+ Trade.get_trades_query([Trade.is_open.is_(False)])
+ .order_by(order_by)
+ .limit(limit)
+ .offset(offset))
else:
- trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
- Trade.close_date.desc())
+ trades = Trade.session.scalars(
+ Trade.get_trades_query([Trade.is_open.is_(False)])
+ .order_by(Trade.close_date.desc()))
output = [trade.to_json() for trade in trades]
- total_trades = Trade.get_trades([Trade.is_open.is_(False)]).count()
+ total_trades = Trade.session.scalar(
+ select(func.count(Trade.id)).filter(Trade.is_open.is_(False)))
return {
"trades": output,
@@ -436,8 +446,8 @@ class RPC:
""" Returns cumulative profit statistics """
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True))
- trades: List[Trade] = Trade.get_trades(
- trade_filter, include_orders=False).order_by(Trade.id).all()
+ trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query(
+ trade_filter, include_orders=False).order_by(Trade.id)).all()
profit_all_coin = []
profit_all_ratio = []
@@ -946,12 +956,12 @@ class RPC:
def _rpc_delete_lock(self, lockid: Optional[int] = None,
pair: Optional[str] = None) -> Dict[str, Any]:
""" Delete specific lock(s) """
- locks = []
+ locks: Sequence[PairLock] = []
if pair:
locks = PairLocks.get_pair_locks(pair)
if lockid:
- locks = PairLock.query.filter(PairLock.id == lockid).all()
+ locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all()
for lock in locks:
lock.active = False
diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py
index c4d4fa2dd..1972ad6e5 100644
--- a/freqtrade/rpc/rpc_manager.py
+++ b/freqtrade/rpc/rpc_manager.py
@@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...)
"""
import logging
from collections import deque
-from typing import Any, Dict, List
+from typing import List
from freqtrade.constants import Config
from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType
from freqtrade.rpc import RPC, RPCHandler
+from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -58,7 +59,7 @@ class RPCManager:
mod.cleanup()
del mod
- def send_msg(self, msg: Dict[str, Any]) -> None:
+ def send_msg(self, msg: RPCSendMsg) -> None:
"""
Send given message to all registered rpc modules.
A message consists of one or more key value pairs of strings.
@@ -69,10 +70,6 @@ class RPCManager:
"""
if msg.get('type') not in NO_ECHO_MESSAGES:
logger.info('Sending rpc message: %s', msg)
- if 'pair' in msg:
- msg.update({
- 'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair'])
- })
for mod in self.registered_modules:
logger.debug('Forwarding message to rpc.%s', mod.name)
try:
diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py
new file mode 100644
index 000000000..3277a2d6e
--- /dev/null
+++ b/freqtrade/rpc/rpc_types.py
@@ -0,0 +1,128 @@
+from datetime import datetime
+from typing import Any, List, Literal, Optional, TypedDict, Union
+
+from freqtrade.constants import PairWithTimeframe
+from freqtrade.enums import RPCMessageType
+
+
+class RPCSendMsgBase(TypedDict):
+ pass
+ # ty1pe: Literal[RPCMessageType]
+
+
+class RPCStatusMsg(RPCSendMsgBase):
+ """Used for Status, Startup and Warning messages"""
+ type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING]
+ status: str
+
+
+class RPCStrategyMsg(RPCSendMsgBase):
+ """Used for Status, Startup and Warning messages"""
+ type: Literal[RPCMessageType.STRATEGY_MSG]
+ msg: str
+
+
+class RPCProtectionMsg(RPCSendMsgBase):
+ type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL]
+ id: int
+ pair: str
+ base_currency: Optional[str]
+ lock_time: str
+ lock_timestamp: int
+ lock_end_time: str
+ lock_end_timestamp: int
+ reason: str
+ side: str
+ active: bool
+
+
+class RPCWhitelistMsg(RPCSendMsgBase):
+ type: Literal[RPCMessageType.WHITELIST]
+ data: List[str]
+
+
+class __RPCBuyMsgBase(RPCSendMsgBase):
+ trade_id: int
+ buy_tag: Optional[str]
+ enter_tag: Optional[str]
+ exchange: str
+ pair: str
+ base_currency: str
+ leverage: Optional[float]
+ direction: str
+ limit: float
+ open_rate: float
+ order_type: Optional[str] # TODO: why optional??
+ stake_amount: float
+ stake_currency: str
+ fiat_currency: Optional[str]
+ amount: float
+ open_date: datetime
+ current_rate: Optional[float]
+ sub_trade: bool
+
+
+class RPCBuyMsg(__RPCBuyMsgBase):
+ type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]
+
+
+class RPCCancelMsg(__RPCBuyMsgBase):
+ type: Literal[RPCMessageType.ENTRY_CANCEL]
+ reason: str
+
+
+class RPCSellMsg(__RPCBuyMsgBase):
+ type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]
+ cumulative_profit: float
+ gain: str # Literal["profit", "loss"]
+ close_rate: float
+ profit_amount: float
+ profit_ratio: float
+ sell_reason: Optional[str]
+ exit_reason: Optional[str]
+ close_date: datetime
+ # current_rate: Optional[float]
+ order_rate: Optional[float]
+
+
+class RPCSellCancelMsg(__RPCBuyMsgBase):
+ type: Literal[RPCMessageType.EXIT_CANCEL]
+ reason: str
+ gain: str # Literal["profit", "loss"]
+ profit_amount: float
+ profit_ratio: float
+ sell_reason: Optional[str]
+ exit_reason: Optional[str]
+ close_date: datetime
+
+
+class _AnalyzedDFData(TypedDict):
+ key: PairWithTimeframe
+ df: Any
+ la: datetime
+
+
+class RPCAnalyzedDFMsg(RPCSendMsgBase):
+ """New Analyzed dataframe message"""
+ type: Literal[RPCMessageType.ANALYZED_DF]
+ data: _AnalyzedDFData
+
+
+class RPCNewCandleMsg(RPCSendMsgBase):
+ """New candle ping message, issued once per new candle/pair"""
+ type: Literal[RPCMessageType.NEW_CANDLE]
+ data: PairWithTimeframe
+
+
+RPCSendMsg = Union[
+ RPCStatusMsg,
+ RPCStrategyMsg,
+ RPCProtectionMsg,
+ RPCWhitelistMsg,
+ RPCBuyMsg,
+ RPCCancelMsg,
+ RPCSellMsg,
+ RPCSellCancelMsg,
+ RPCAnalyzedDFMsg,
+ RPCNewCandleMsg
+ ]
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 0c0b24f00..d79d8ea76 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -30,6 +30,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException, RPCHandler
+from freqtrade.rpc.rpc_types import RPCSendMsg
logger = logging.getLogger(__name__)
@@ -83,6 +84,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
self._send_msg(str(e))
except BaseException:
logger.exception('Exception occurred within Telegram module')
+ finally:
+ Trade.session.remove()
return wrapper
@@ -427,14 +430,14 @@ class Telegram(RPCHandler):
return None
return message
- def send_msg(self, msg: Dict[str, Any]) -> None:
+ def send_msg(self, msg: RPCSendMsg) -> None:
""" Send a message to telegram channel """
default_noti = 'on'
msg_type = msg['type']
noti = ''
- if msg_type == RPCMessageType.EXIT:
+ if msg['type'] == RPCMessageType.EXIT:
sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string
@@ -451,7 +454,7 @@ class Telegram(RPCHandler):
# Notification disabled
return
- message = self.compose_message(deepcopy(msg), msg_type)
+ message = self.compose_message(deepcopy(msg), msg_type) # type: ignore
if message:
self._send_msg(message, disable_notification=(noti == 'silent'))
@@ -1340,7 +1343,7 @@ class Telegram(RPCHandler):
message = tabulate({k: [v] for k, v in counts.items()},
headers=['current', 'max', 'total stake'],
tablefmt='simple')
- message = "
{}".format(message) + message = f"
{message}" logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, callback_path="update_count", @@ -1642,7 +1645,7 @@ class Telegram(RPCHandler): ]) else: reply_markup = InlineKeyboardMarkup([[]]) - msg += "\nUpdated: {}".format(datetime.now().ctime()) + msg += f"\nUpdated: {datetime.now().ctime()}" if not query.message: return chat_id = query.message.chat_id diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 0967de70d..14b881126 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -10,6 +10,7 @@ from requests import RequestException, post from freqtrade.constants import Config from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -41,7 +42,7 @@ class Webhook(RPCHandler): """ pass - def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]: + def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]: whconfig = self._config['webhook'] # Deprecated 2022.10 - only keep generic method. if msg['type'] in [RPCMessageType.ENTRY]: @@ -75,7 +76,7 @@ class Webhook(RPCHandler): return None return valuedict - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ try: @@ -113,7 +114,7 @@ class Webhook(RPCHandler): response = post(self._url, data=payload['data'], headers={'Content-Type': 'text/plain'}) else: - raise NotImplementedError('Unknown format: {}'.format(self._format)) + raise NotImplementedError(f'Unknown format: {self._format}') # Throw a RequestException if the post was not successful response.raise_for_status() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 96b2ac8ce..6d4a3036f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -251,11 +251,12 @@ class IStrategy(ABC, HyperStrategyMixin): """ pass - def bot_loop_start(self, **kwargs) -> None: + def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote resource for comparison) + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py new file mode 100644 index 000000000..2669dcc4a --- /dev/null +++ b/freqtrade/strategy/strategyupdater.py @@ -0,0 +1,255 @@ +import shutil +from pathlib import Path + +import ast_comments + +from freqtrade.constants import Config + + +class StrategyUpdater: + name_mapping = { + 'ticker_interval': 'timeframe', + 'buy': 'enter_long', + 'sell': 'exit_long', + 'buy_tag': 'enter_tag', + 'sell_reason': 'exit_reason', + + 'sell_signal': 'exit_signal', + 'custom_sell': 'custom_exit', + 'force_sell': 'force_exit', + 'emergency_sell': 'emergency_exit', + + # Strategy/config settings: + 'use_sell_signal': 'use_exit_signal', + 'sell_profit_only': 'exit_profit_only', + 'sell_profit_offset': 'exit_profit_offset', + 'ignore_roi_if_buy_signal': 'ignore_roi_if_entry_signal', + 'forcebuy_enable': 'force_entry_enable', + } + + function_mapping = { + 'populate_buy_trend': 'populate_entry_trend', + 'populate_sell_trend': 'populate_exit_trend', + 'custom_sell': 'custom_exit', + 'check_buy_timeout': 'check_entry_timeout', + 'check_sell_timeout': 'check_exit_timeout', + # '': '', + } + # order_time_in_force, order_types, unfilledtimeout + otif_ot_unfilledtimeout = { + 'buy': 'entry', + 'sell': 'exit', + } + + # create a dictionary that maps the old column names to the new ones + rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'} + + def start(self, config: Config, strategy_obj: dict) -> None: + """ + Run strategy updater + It updates a strategy to v3 with the help of the ast-module + :return: None + """ + + source_file = strategy_obj['location'] + strategies_backup_folder = Path.joinpath(config['user_data_dir'], "strategies_orig_updater") + target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel']) + + # read the file + with Path(source_file).open('r') as f: + old_code = f.read() + if not strategies_backup_folder.is_dir(): + Path(strategies_backup_folder).mkdir(parents=True, exist_ok=True) + + # backup original + # => currently no date after the filename, + # could get overridden pretty fast if this is fired twice! + # The folder is always the same and the file name too (currently). + shutil.copy(source_file, target_file) + + # update the code + new_code = self.update_code(old_code) + # write the modified code to the destination folder + with Path(source_file).open('w') as f: + f.write(new_code) + + # define the function to update the code + def update_code(self, code): + # parse the code into an AST + tree = ast_comments.parse(code) + + # use the AST to update the code + updated_code = self.modify_ast(tree) + + # return the modified code without executing it + return updated_code + + # function that uses the ast module to update the code + def modify_ast(self, tree): # noqa + # use the visitor to update the names and functions in the AST + NameUpdater().visit(tree) + + # first fix the comments, so it understands "\n" properly inside multi line comments. + ast_comments.fix_missing_locations(tree) + ast_comments.increment_lineno(tree, n=1) + + # generate the new code from the updated AST + # without indent {} parameters would just be written straight one after the other. + + # ast_comments would be amazing since this is the only solution that carries over comments, + # but it does currently not have an unparse function, hopefully in the future ... ! + # return ast_comments.unparse(tree) + + return ast_comments.unparse(tree) + + +# Here we go through each respective node, slice, elt, key ... to replace outdated entries. +class NameUpdater(ast_comments.NodeTransformer): + def generic_visit(self, node): + + # space is not yet transferred from buy/sell to entry/exit and thereby has to be skipped. + if isinstance(node, ast_comments.keyword): + if node.arg == "space": + return node + + # from here on this is the original function. + for field, old_value in ast_comments.iter_fields(node): + if isinstance(old_value, list): + new_values = [] + for value in old_value: + if isinstance(value, ast_comments.AST): + value = self.visit(value) + if value is None: + continue + elif not isinstance(value, ast_comments.AST): + new_values.extend(value) + continue + new_values.append(value) + old_value[:] = new_values + elif isinstance(old_value, ast_comments.AST): + new_node = self.visit(old_value) + if new_node is None: + delattr(node, field) + else: + setattr(node, field, new_node) + return node + + def visit_Expr(self, node): + if hasattr(node.value, "left") and hasattr(node.value.left, "id"): + node.value.left.id = self.check_dict(StrategyUpdater.name_mapping, node.value.left.id) + self.visit(node.value) + return node + + # Renames an element if contained inside a dictionary. + @staticmethod + def check_dict(current_dict: dict, element: str): + if element in current_dict: + element = current_dict[element] + return element + + def visit_arguments(self, node): + if isinstance(node.args, list): + for arg in node.args: + arg.arg = self.check_dict(StrategyUpdater.name_mapping, arg.arg) + return node + + def visit_Name(self, node): + # if the name is in the mapping, update it + node.id = self.check_dict(StrategyUpdater.name_mapping, node.id) + return node + + def visit_Import(self, node): + # do not update the names in import statements + return node + + def visit_ImportFrom(self, node): + # if hasattr(node, "module"): + # if node.module == "freqtrade.strategy.hyper": + # node.module = "freqtrade.strategy" + return node + + def visit_If(self, node: ast_comments.If): + for child in ast_comments.iter_child_nodes(node): + self.visit(child) + return node + + def visit_FunctionDef(self, node): + node.name = self.check_dict(StrategyUpdater.function_mapping, node.name) + self.generic_visit(node) + return node + + def visit_Attribute(self, node): + if ( + isinstance(node.value, ast_comments.Name) + and node.value.id == 'trade' + and node.attr == 'nr_of_successful_buys' + ): + node.attr = 'nr_of_successful_entries' + return node + + def visit_ClassDef(self, node): + # check if the class is derived from IStrategy + if any(isinstance(base, ast_comments.Name) and + base.id == 'IStrategy' for base in node.bases): + # check if the INTERFACE_VERSION variable exists + has_interface_version = any( + isinstance(child, ast_comments.Assign) and + isinstance(child.targets[0], ast_comments.Name) and + child.targets[0].id == 'INTERFACE_VERSION' + for child in node.body + ) + + # if the INTERFACE_VERSION variable does not exist, add it as the first child + if not has_interface_version: + node.body.insert(0, ast_comments.parse('INTERFACE_VERSION = 3').body[0]) + # otherwise, update its value to 3 + else: + for child in node.body: + if ( + isinstance(child, ast_comments.Assign) + and isinstance(child.targets[0], ast_comments.Name) + and child.targets[0].id == 'INTERFACE_VERSION' + ): + child.value = ast_comments.parse('3').body[0].value + self.generic_visit(node) + return node + + def visit_Subscript(self, node): + if isinstance(node.slice, ast_comments.Constant): + if node.slice.value in StrategyUpdater.rename_dict: + # Replace the slice attributes with the values from rename_dict + node.slice.value = StrategyUpdater.rename_dict[node.slice.value] + if hasattr(node.slice, "elts"): + self.visit_elts(node.slice.elts) + if hasattr(node.slice, "value"): + if hasattr(node.slice.value, "elts"): + self.visit_elts(node.slice.value.elts) + return node + + # elts can have elts (technically recursively) + def visit_elts(self, elts): + if isinstance(elts, list): + for elt in elts: + self.visit_elt(elt) + else: + self.visit_elt(elts) + return elts + + # sub function again needed since the structure itself is highly flexible ... + def visit_elt(self, elt): + if isinstance(elt, ast_comments.Constant) and elt.value in StrategyUpdater.rename_dict: + elt.value = StrategyUpdater.rename_dict[elt.value] + if hasattr(elt, "elts"): + self.visit_elts(elt.elts) + if hasattr(elt, "args"): + if isinstance(elt.args, ast_comments.arguments): + self.visit_elts(elt.args) + else: + for arg in elt.args: + self.visit_elts(arg) + return elt + + def visit_Constant(self, node): + node.value = self.check_dict(StrategyUpdater.otif_ot_unfilledtimeout, node.value) + node.value = self.check_dict(StrategyUpdater.name_mapping, node.value) + return node diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 488ca2fd7..bfbb20ec1 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -1,5 +1,5 @@ -def bot_loop_start(self, **kwargs) -> None: +def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks @@ -8,6 +8,7 @@ def bot_loop_start(self, **kwargs) -> None: For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, this simply does nothing. + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass diff --git a/freqtrade/util/binance_mig.py b/freqtrade/util/binance_mig.py index 708bb1db7..37a2d2ef1 100644 --- a/freqtrade/util/binance_mig.py +++ b/freqtrade/util/binance_mig.py @@ -1,6 +1,7 @@ import logging from packaging import version +from sqlalchemy import select from freqtrade.constants import Config from freqtrade.enums.tradingmode import TradingMode @@ -44,7 +45,7 @@ def _migrate_binance_futures_db(config: Config): # Should symbol be migrated too? # order.symbol = new_pair Trade.commit() - pls = PairLock.query.filter(PairLock.pair.notlike('%:%')) + pls = PairLock.session.scalars(select(PairLock).filter(PairLock.pair.notlike('%:%'))).all() for pl in pls: pl.pair = f"{pl.pair}:{config['stake_currency']}" # print(pls) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 3da4f038d..63797d462 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # QTPyLib: Quantitative Trading Python Library # https://github.com/ranaroussi/qtpylib # @@ -18,7 +16,6 @@ # limitations under the License. # -import sys import warnings from datetime import datetime, timedelta @@ -27,11 +24,6 @@ import pandas as pd from pandas.core.base import PandasObject -# ============================================= -# check min, python version -if sys.version_info < (3, 4): - raise SystemError("QTPyLib requires Python version >= 3.4") - # ============================================= warnings.simplefilter(action="ignore", category=RuntimeWarning) diff --git a/pyproject.toml b/pyproject.toml index 71687961d..baf707c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools >= 46.4.0", "wheel"] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 100 exclude = ''' @@ -48,10 +52,6 @@ ignore_errors = true module = "telegram.*" implicit_optional = true -[build-system] -requires = ["setuptools >= 46.4.0", "wheel"] -build-backend = "setuptools.build_meta" - [tool.pyright] include = ["freqtrade"] exclude = [ @@ -68,15 +68,19 @@ target-version = "py38" extend-select = [ "C90", # mccabe # "N", # pep8-naming - # "UP", # pyupgrade + "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable "YTT", # flake8-2020 + # "S", # flake8-bandit # "DTZ", # flake8-datetimez # "RSE", # flake8-raise # "TCH", # flake8-type-checking - "PTH", # flake8-use-pathlib + "PTH", # flake8-use-pathlib ] [tool.ruff.mccabe] max-complexity = 12 + +[tool.ruff.per-file-ignores] +"tests/*" = ["S"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d076777f..3324c11e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,11 +7,11 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.255 +ruff==0.0.259 mypy==1.1.1 -pre-commit==3.1.1 +pre-commit==3.2.1 pytest==7.2.2 -pytest-asyncio==0.20.3 +pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-random-order==1.1.0 @@ -22,11 +22,11 @@ time-machine==2.9.0 httpx==0.23.3 # Convert jupyter notebooks to markdown documents -nbconvert==7.2.9 +nbconvert==7.2.10 # mypy types types-cachetools==5.3.0.4 types-filelock==3.2.7 -types-requests==2.28.11.15 +types-requests==2.28.11.16 types-tabulate==0.9.0.1 types-python-dateutil==2.8.19.10 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index bc0be85e5..e6eae667c 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -5,7 +5,7 @@ # Required for freqai scikit-learn==1.1.3 joblib==1.2.0 -catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11' +catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 xgboost==1.7.4 tensorboard==2.12.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 904b5d661..2c7c27d98 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.9.0 +filelock==3.10.6 progressbar2==4.2.0 diff --git a/requirements.txt b/requirements.txt index 868fc9699..b888d9f6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,10 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.9.12 -cryptography==39.0.2 +ccxt==3.0.37 +cryptography==40.0.1 aiohttp==3.8.4 -SQLAlchemy==2.0.5.post1 +SQLAlchemy==2.0.7 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 @@ -26,17 +26,17 @@ pyarrow==11.0.0; platform_machine != 'armv7l' py_find_1st==1.1.5 # Load ticker files 30% faster -python-rapidjson==1.9 +python-rapidjson==1.10 # Properly format api responses -orjson==3.8.7 +orjson==3.8.8 # Notify systemd sdnotify==0.3.2 # API Server -fastapi==0.94.0 -pydantic==1.10.6 -uvicorn==0.21.0 +fastapi==0.95.0 +pydantic==1.10.7 +uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 psutil==5.9.4 @@ -55,3 +55,5 @@ schedule==1.1.0 #WS Messages websockets==10.4 janus==1.0.0 + +ast-comments==1.0.1 diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 0ba1924a7..318590b32 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -14,7 +14,8 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star start_hyperopt_show, start_install_ui, start_list_data, start_list_exchanges, start_list_markets, start_list_strategies, start_list_timeframes, start_new_strategy, start_show_trades, - start_test_pairlist, start_trading, start_webserver) + start_strategy_update, start_test_pairlist, start_trading, + start_webserver) from freqtrade.commands.db_commands import start_convert_db from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) @@ -1546,3 +1547,37 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog): start_convert_db(pargs) assert db_target_file.is_file() + + +def test_start_strategy_updater(mocker, tmpdir): + sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion') + teststrats = Path(__file__).parent.parent / 'strategy/strats' + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-path", + str(teststrats), + ] + pargs = get_args(args) + pargs['config'] = None + start_strategy_update(pargs) + # Number of strategies in the test directory + assert sc_mock.call_count == 11 + + sc_mock.reset_mock() + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-path", + str(teststrats), + "--strategy-list", + "StrategyTestV3", + "StrategyTestV2" + ] + pargs = get_args(args) + pargs['config'] = None + start_strategy_update(pargs) + # Number of strategies in the test directory + assert sc_mock.call_count == 2 diff --git a/tests/conftest.py b/tests/conftest.py index 3c10de4ec..0aa6e70a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -299,7 +299,7 @@ def create_mock_trades(fee, is_short: Optional[bool] = False, use_db: bool = Tru """ def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) is_short1 = is_short if is_short is not None else True @@ -332,11 +332,11 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): Create some fake trades ... """ if use_db: - Trade.query.session.rollback() + Trade.session.rollback() def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) @@ -366,7 +366,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): add_trade(trade) if use_db: - Trade.query.session.flush() + Trade.session.flush() def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True): @@ -375,7 +375,7 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool """ def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 345e3c299..2c5515f7c 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -98,7 +98,7 @@ def test_load_backtest_data_new_format(testdatadir): assert bt_data.equals(bt_data3) with pytest.raises(ValueError, match=r"File .* does not exist\."): - load_backtest_data(str("filename") + "nofile") + load_backtest_data("filename" + "nofile") with pytest.raises(ValueError, match=r"Unknown dataformat."): load_backtest_data(testdatadir / "backtest_results" / LAST_BT_RESULT_FN) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index f673ede6e..f19b15455 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -252,7 +252,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog): assert log_has_re(expected_text, caplog) -@pytest.mark.parametrize('datahandler', ['feather', 'parquet']) +@pytest.mark.parametrize('datahandler', ['parquet']) def test_datahandler_trades_not_supported(datahandler, testdatadir, ): dh = get_datahandler(testdatadir, datahandler) with pytest.raises(NotImplementedError): @@ -496,6 +496,58 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): assert unlinkmock.call_count == 2 +def test_featherdatahandler_trades_load(testdatadir): + dh = get_datahandler(testdatadir, 'feather') + trades = dh.trades_load('XRP/ETH') + assert isinstance(trades, list) + assert trades[0][0] == 1570752011620 + assert trades[-1][-1] == 0.1986231 + + trades1 = dh.trades_load('UNITTEST/NONEXIST') + assert trades1 == [] + + +def test_featherdatahandler_trades_store(testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) + dh = get_datahandler(testdatadir, 'feather') + trades = dh.trades_load('XRP/ETH') + + dh1 = get_datahandler(tmpdir1, 'feather') + dh1.trades_store('XRP/NEW', trades) + file = tmpdir1 / 'XRP_NEW-trades.feather' + assert file.is_file() + # Load trades back + trades_new = dh1.trades_load('XRP/NEW') + + assert len(trades_new) == len(trades) + assert trades[0][0] == trades_new[0][0] + assert trades[0][1] == trades_new[0][1] + # assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense + assert trades[0][3] == trades_new[0][3] + assert trades[0][4] == trades_new[0][4] + assert trades[0][5] == trades_new[0][5] + assert trades[0][6] == trades_new[0][6] + assert trades[-1][0] == trades_new[-1][0] + assert trades[-1][1] == trades_new[-1][1] + # assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense + assert trades[-1][3] == trades_new[-1][3] + assert trades[-1][4] == trades_new[-1][4] + assert trades[-1][5] == trades_new[-1][5] + assert trades[-1][6] == trades_new[-1][6] + + +def test_featherdatahandler_trades_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = get_datahandler(testdatadir, 'feather') + assert not dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 1 + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c967f0c89..24ad8bcc9 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -409,7 +409,7 @@ def test_init_with_refresh(default_conf, mocker) -> None: def test_file_dump_json_tofile(testdatadir) -> None: - file = testdatadir / 'test_{id}.json'.format(id=str(uuid.uuid4())) + file = testdatadir / f'test_{uuid.uuid4()}.json' data = {'bar': 'foo'} # check the file we will create does not exist diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 616910682..fda33b859 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -11,6 +11,19 @@ from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers +@pytest.mark.parametrize('side,type,time_in_force,expected', [ + ('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}), + ('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}), + ('buy', 'market', 'IOC', {}), + ('buy', 'limit', 'PO', {'timeInForce': 'PO'}), + ('sell', 'limit', 'PO', {'timeInForce': 'PO'}), + ('sell', 'market', 'PO', {}), + ]) +def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + assert exchange._get_params(side, type, 1, False, time_in_force) == expected + + @pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT]) @pytest.mark.parametrize('limitratio,expected,side', [ (None, 220 * 0.99, "sell"), @@ -39,7 +52,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -118,7 +131,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -542,7 +555,6 @@ def test__set_leverage_binance(mocker, default_conf): "set_leverage", pair="XRP/USDT", leverage=5.0, - trading_mode=TradingMode.FUTURES ) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 872cf5059..4a65b16d7 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -37,7 +37,7 @@ EXCHANGES = { 'stake_currency': 'USDT', 'use_ci_proxy': True, 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', 'hasQuoteVolumeFutures': True, @@ -66,7 +66,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': False, 'sample_order': [{ "symbol": "SOLUSDT", @@ -91,7 +91,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, }, @@ -99,7 +99,7 @@ EXCHANGES = { 'pair': 'XRP/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, 'sample_order': [ @@ -141,7 +141,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', 'hasQuoteVolumeFutures': True, @@ -215,7 +215,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', 'hasQuoteVolumeFutures': False, @@ -226,7 +226,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures_pair': 'BTC/USDT:USDT', 'futures': True, 'leverage_tiers_public': True, @@ -253,14 +253,14 @@ EXCHANGES = { 'pair': 'ETH/BTC', 'stake_currency': 'BTC', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': False, }, 'bitvavo': { 'pair': 'BTC/EUR', 'stake_currency': 'EUR', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': False, }, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d7f6a8b90..5350f4e3e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -113,18 +113,21 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + exchange.close() with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + exchange.close() with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 + exchange.close() def test_init(default_conf, mocker, caplog): @@ -1039,9 +1042,9 @@ def test_validate_ordertypes(default_conf, mocker): ('bybit', 'last', True), ('bybit', 'mark', True), ('bybit', 'index', True), - # ('okx', 'last', True), - # ('okx', 'mark', True), - # ('okx', 'index', True), + ('okx', 'last', True), + ('okx', 'mark', True), + ('okx', 'index', True), ('gate', 'last', True), ('gate', 'mark', True), ('gate', 'index', True), @@ -2248,7 +2251,6 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert res[pair2].at[0, 'open'] -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ @@ -2277,7 +2279,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ assert res[3] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog) - + exchange.close() # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", @@ -2292,15 +2294,17 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT, (arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() + with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical candle \(OHLCV\) data\..*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT, (arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() -@pytest.mark.asyncio async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): from freqtrade.exchange.common import _reset_logging_mixin _reset_logging_mixin() @@ -2341,9 +2345,9 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): # Expect the "returned exception" message 12 times (4 retries * 3 (loop)) assert num_log_has_re(msg, caplog) == 12 assert num_log_has_re(msg2, caplog) == 9 + exchange.close() -@pytest.mark.asyncio async def test__async_get_candle_history_empty(default_conf, mocker, caplog): """ Test empty exchange result """ ohlcv = [] @@ -2363,6 +2367,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog): assert res[2] == CandleType.SPOT assert res[3] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 + exchange.close() def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): @@ -2757,7 +2762,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na assert res_ohlcv[9][5] == 2.31452783 -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, fetch_trades_result): @@ -2785,8 +2789,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, assert exchange._api_async.fetch_trades.call_args[1]['limit'] == 1000 assert exchange._api_async.fetch_trades.call_args[1]['params'] == {'from': '123'} assert log_has_re(f"Fetching trades for pair {pair}, params: .*", caplog) + exchange.close() - exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_fetch_trades", "fetch_trades", pair='ABCD/BTC', since=None) @@ -2796,15 +2800,16 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical trade data\..*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name, fetch_trades_result): @@ -2839,6 +2844,7 @@ async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, e pair = 'ETH/USDT:USDT' res = await exchange._async_fetch_trades(pair, since=None, params=None) assert res[0][5] == 300 + exchange.close() @pytest.mark.asyncio @@ -3387,7 +3393,7 @@ def test_merge_ft_has_dict(default_conf, mocker): ex = Binance(default_conf) assert ex._ft_has != Exchange._ft_has_default assert ex.get_option('stoploss_on_exchange') - assert ex.get_option('order_time_in_force') == ['GTC', 'FOK', 'IOC'] + assert ex.get_option('order_time_in_force') == ['GTC', 'FOK', 'IOC', 'PO'] assert ex.get_option('trades_pagination') == 'id' assert ex.get_option('trades_pagination_arg') == 'fromId' @@ -3868,29 +3874,6 @@ def test_get_stake_amount_considering_leverage( stake_amount, leverage) == min_stake_with_lev -@pytest.mark.parametrize("exchange_name,trading_mode", [ - ("binance", TradingMode.FUTURES), -]) -def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): - - api_mock = MagicMock() - api_mock.set_leverage = MagicMock() - type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - default_conf['dry_run'] = False - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - exchange_name, - "_set_leverage", - "set_leverage", - pair="XRP/USDT", - leverage=5.0, - trading_mode=trading_mode - ) - - @pytest.mark.parametrize("margin_mode", [ (MarginMode.CROSS), (MarginMode.ISOLATED) @@ -4830,7 +4813,6 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name ) -@pytest.mark.asyncio @pytest.mark.parametrize('exchange_name', EXCHANGES) async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name): default_conf['exchange']['name'] = exchange_name diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 5e4fd7316..85d2ced9d 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -31,7 +31,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}, side=side, @@ -84,7 +84,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}, side='sell', leverage=1.0) diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index e0bb32b7c..07f3fb6a3 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -31,7 +31,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') if order_type == 'limit': - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={ 'stoploss': order_type, @@ -92,7 +92,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss': 'limit', 'stoploss_on_exchange_limit_ratio': 1.05}, diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index fce77f4c7..7a3fa22f0 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -2,11 +2,13 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import ccxt import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.exceptions import RetryableOrderError from freqtrade.exchange.exchange import timeframe_to_minutes -from tests.conftest import get_mock_coro, get_patched_exchange, log_has +from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -476,3 +478,116 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, exchange.load_leverage_tiers() assert log_has(logmsg, caplog) + + +def test__set_leverage_okx(mocker, default_conf): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange._lev_prep('BTC/USDT:USDT', 3.2, 'buy') + assert api_mock.set_leverage.call_count == 1 + # Leverage is rounded to 3. + assert api_mock.set_leverage.call_args_list[0][1]['leverage'] == 3.2 + assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT' + assert api_mock.set_leverage.call_args_list[0][1]['params'] == { + 'mgnMode': 'isolated', + 'posSide': 'net'} + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "okx", + "_lev_prep", + "set_leverage", + pair="XRP/USDT:USDT", + leverage=5.0, + side='buy' + ) + + +@pytest.mark.usefixtures("init_persistence") +def test_fetch_stoploss_order_okx(default_conf, mocker): + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_order = MagicMock() + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx') + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_order.call_args_list[0][0][0] == '1234' + assert api_mock.fetch_order.call_args_list[0][0][1] == 'ETH/BTC' + assert api_mock.fetch_order.call_args_list[0][1]['params'] == {'stop': True} + + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound) + api_mock.fetch_open_orders = MagicMock(return_value=[]) + api_mock.fetch_closed_orders = MagicMock(return_value=[]) + api_mock.fetch_canceled_orders = MagicMock(creturn_value=[]) + + with pytest.raises(RetryableOrderError): + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_open_orders.call_count == 1 + assert api_mock.fetch_closed_orders.call_count == 1 + assert api_mock.fetch_canceled_orders.call_count == 1 + + api_mock.fetch_order.reset_mock() + api_mock.fetch_open_orders.reset_mock() + api_mock.fetch_closed_orders.reset_mock() + api_mock.fetch_canceled_orders.reset_mock() + + api_mock.fetch_closed_orders = MagicMock(return_value=[ + { + 'id': '1234', + 'status': 'closed', + 'info': {'ordId': '123455'} + } + ]) + mocker.patch(f"{EXMS}.fetch_order", MagicMock(return_value={'id': '123455'})) + resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_open_orders.call_count == 1 + assert api_mock.fetch_closed_orders.call_count == 1 + assert api_mock.fetch_canceled_orders.call_count == 0 + + assert resp['id'] == '1234' + assert resp['id_stop'] == '123455' + assert resp['type'] == 'stoploss' + + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx') + dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={'id': '123455'})) + + api_mock.fetch_order.reset_mock() + api_mock.fetch_open_orders.reset_mock() + api_mock.fetch_closed_orders.reset_mock() + api_mock.fetch_canceled_orders.reset_mock() + resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC') + + assert api_mock.fetch_order.call_count == 0 + assert api_mock.fetch_open_orders.call_count == 0 + assert api_mock.fetch_closed_orders.call_count == 0 + assert api_mock.fetch_canceled_orders.call_count == 0 + assert dro_mock.call_count == 1 + + +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side): + exchange = get_patched_exchange(mocker, default_conf, id='okx') + order = { + 'type': 'stoploss', + 'price': 1500, + 'stopLossPrice': 1500, + } + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 2cb42c003..158dd04dc 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType +from freqtrade.enums import ExitType, TradingMode from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence.trade_model import LocalTrade from tests.conftest import EXMS, patch_exchange @@ -925,12 +925,14 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) + mocker.patch(f"{EXMS}.calculate_funding_fees", return_value=0) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) # TODO: Should we initialize this properly?? - backtesting._can_short = True + backtesting.trading_mode = TradingMode.MARGIN backtesting._set_strategy(backtesting.strategylist[0]) + backtesting._can_short = True backtesting.required_startup = 0 backtesting.strategy.advise_entry = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8dee45b6d..9dbda51b0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -344,7 +344,7 @@ def test_backtest_abort(default_conf, mocker, testdatadir) -> None: assert backtesting.progress.progress == 0 -def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: +def test_backtesting_start(default_conf, mocker, caplog) -> None: def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) @@ -367,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.bot_loop_start = MagicMock() + backtesting.strategy.bot_start = MagicMock() backtesting.start() # check the logs, that will contain the backtest result exists = [ @@ -376,7 +377,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: for line in exists: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None - assert backtesting.strategy.bot_loop_start.call_count == 1 + assert backtesting.strategy.bot_start.call_count == 1 + assert backtesting.strategy.bot_loop_start.call_count == 0 assert sbs.call_count == 1 assert sbc.call_count == 1 diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 9fc726bd1..0d57ff89a 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -10,7 +10,7 @@ from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType +from freqtrade.enums import ExitType, TradingMode from freqtrade.optimize.backtesting import Backtesting from tests.conftest import EXMS, patch_exchange @@ -108,9 +108,10 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera default_conf.update({ "stake_amount": 100.0, "dry_run_wallet": 1000.0, - "strategy": "StrategyTestV3" + "strategy": "StrategyTestV3", }) backtesting = Backtesting(default_conf) + backtesting.trading_mode = TradingMode.FUTURES backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) pair = 'XRP/USDT' diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 998798580..786720030 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -872,7 +872,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 @@ -922,7 +923,8 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 @@ -959,7 +961,8 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False + assert hyperopt.backtesting.strategy.bot_started is True assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index f71e6c492..0cc32baaf 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -236,7 +236,7 @@ def test_store_backtest_candles(testdatadir, mocker): assert dump_mock.call_count == 1 assert isinstance(dump_mock.call_args_list[0][0][0], Path) - assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') dump_mock.reset_mock() # mock file exporting @@ -245,7 +245,7 @@ def test_store_backtest_candles(testdatadir, mocker): assert dump_mock.call_count == 1 assert isinstance(dump_mock.call_args_list[0][0][0], Path) # result will be testdatadir / testresult-