Merge branch 'develop' into pr/asuiu/8296

This commit is contained in:
Matthias 2023-03-26 10:28:02 +02:00
commit 02078456fc
107 changed files with 5060 additions and 2328 deletions

View File

@ -16,7 +16,8 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
repository-projects: read
jobs:
build_linux:
@ -24,7 +25,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-20.04, ubuntu-22.04 ]
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
@ -115,7 +116,7 @@ jobs:
strategy:
matrix:
os: [ macos-latest ]
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
@ -212,7 +213,7 @@ jobs:
strategy:
matrix:
os: [ windows-latest ]
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
@ -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.6.4
uses: pypa/gh-action-pypi-publish@v1.8.1
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.6.4
uses: pypa/gh-action-pypi-publish@v1.8.1
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

View File

@ -8,7 +8,7 @@ repos:
# stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v0.991"
rev: "v1.0.1"
hooks:
- id: mypy
exclude: build_helpers
@ -17,7 +17,8 @@ repos:
- types-filelock==3.2.7
- types-requests==2.28.11.15
- types-tabulate==0.9.0.1
- types-python-dateutil==2.8.19.9
- types-python-dateutil==2.8.19.10
- SQLAlchemy==2.0.7
# stages: [push]
- repo: https://github.com/pycqa/isort
@ -29,7 +30,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.251'
rev: 'v0.0.255'
hooks:
- id: ruff

View File

@ -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

View File

@ -8,12 +8,17 @@ import yaml
pre_commit_file = Path('.pre-commit-config.yaml')
require_dev = Path('requirements-dev.txt')
require = Path('requirements.txt')
with require_dev.open('r') as rfile:
requirements = rfile.readlines()
with require.open('r') as rfile:
requirements.extend(rfile.readlines())
# Extract types only
type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')]
type_reqs = [r.strip('\n') for r in requirements if r.startswith(
'types-') or r.startswith('SQLAlchemy')]
with pre_commit_file.open('r') as file:
f = yaml.load(file, Loader=yaml.FullLoader)

View File

@ -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"

View File

@ -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

View File

@ -12,6 +12,9 @@ This page provides you some basic concepts on how Freqtrade works and operates.
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
* **Limit order**: Limit orders which execute at the defined limit price or better.
* **Market order**: Guaranteed to fill, may move price depending on the order size.
* **Current Profit**: Currently pending (unrealized) profit for this trade. This is mainly used throughout the bot and UI.
* **Realized Profit**: Already realized profit. Only relevant in combination with [partial exits](strategy-callbacks.md#adjust-trade-position) - which also explains the calculation logic for this.
* **Total Profit**: Combined realized and unrealized profit. The relative number (%) is calculated against the total investment in this trade.
## Fee handling

View File

@ -74,3 +74,8 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re
* `webhooksell`, `webhookexit` -> `exit`
* `webhooksellfill`, `webhookexitfill` -> `exit_fill`
* `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel`
## Removal of `populate_any_indicators`
version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details.

View File

@ -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. <br> **Datatype:** Float. <br> 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. <br> **Datatype:** Boolean. <br> Default: `False` (no reversal).
| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually. <br> **Datatype:** Boolean. <br> 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.<br> **Datatype:** Boolean. <br> 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.<br> **Datatype:** Integer. <br> Default: `0`.
### Data split parameters
@ -84,6 +84,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
| `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting. <br> **Datatype:** bool. <br> Default: `False`.
| `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[<shared layers>, dict(vf=[<non-shared value network layers>], pi=[<non-shared policy network layers>])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each.
| `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting. <br> **Datatype:** bool. <br> Default: `False`.
| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases) <br> **Datatype:** Boolean. <br> **Default:** `False`
### Additional parameters

View File

@ -176,9 +176,11 @@ As you begin to modify the strategy and the prediction model, you will quickly r
factor = 100
pair = self.pair.replace(':', '')
# you can use feature values from dataframe
# Assumes the shifted RSI indicator has been generated in the strategy.
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_"
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_"
f"{self.config['timeframe']}"].iloc[self._current_tick]
# reward agent for entering trades
@ -246,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

View File

@ -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):

View File

@ -71,6 +71,10 @@ pip install -r requirements-freqai.txt
!!! Note
Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform.
!!! Note "python 3.11"
Some dependencies (Catboost, Torch) currently don't support python 3.11. Freqtrade therefore only supports python 3.10 for these models/dependencies.
Tests involving these dependencies are skipped on 3.11.
### Usage with docker
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.

View File

@ -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

View File

@ -290,10 +290,8 @@ cd freqtrade
#### Freqtrade install: Conda Environment
Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
```bash
conda env create -n freqtrade-conda -f environment.yml
conda create --name freqtrade python=3.10
```
!!! Note "Creating Conda Environment"
@ -302,12 +300,9 @@ conda env create -n freqtrade-conda -f environment.yml
```bash
# choose your own packages
conda env create -n [name of the environment] [python version] [packages]
# point to file with packages
conda env create -n [name of the environment] -f [file]
```
#### Enter/exit freqtrade-conda environment
#### Enter/exit freqtrade environment
To check available environments, type
@ -319,7 +314,7 @@ Enter installed environment
```bash
# enter conda environment
conda activate freqtrade-conda
conda activate freqtrade
# exit conda environment - don't do it now
conda deactivate
@ -329,6 +324,7 @@ Install last python dependencies with pip
```bash
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install -e .
```
@ -336,7 +332,7 @@ Patch conda libta-lib (Linux only)
```bash
# Ensure that the environment is active!
conda activate freqtrade-conda
conda activate freqtrade
cd build_helpers
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
@ -355,8 +351,8 @@ conda env list
# activate base environment
conda activate
# activate freqtrade-conda environment
conda activate freqtrade-conda
# activate freqtrade environment
conda activate freqtrade
#deactivate any conda environments
conda deactivate

View File

@ -1,6 +1,6 @@
markdown==3.3.7
mkdocs==1.4.2
mkdocs-material==9.0.15
mkdocs-material==9.1.3
mdx_truly_sane_lists==1.3
pymdown-extensions==9.9.2
pymdown-extensions==9.10
jinja2==3.1.2

View File

@ -316,11 +316,11 @@ class AwesomeStrategy(IStrategy):
# evaluate highest to lowest, so that highest possible stop is used
if current_profit > 0.40:
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short)
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage)
elif current_profit > 0.25:
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short)
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage)
elif current_profit > 0.20:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1

View File

@ -881,7 +881,7 @@ All columns of the informative dataframe will be available on the returning data
### *stoploss_from_open()*
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price.
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point.
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
@ -889,6 +889,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%).
``` python
@ -907,7 +909,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
return 1
@ -1038,11 +1040,10 @@ from datetime import timedelta, datetime, timezone
# Within populate indicators (or populate_buy):
if self.config['runmode'].value in ('live', 'dry_run'):
# fetch closed trades for the last 2 days
trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=2),
Trade.is_open.is_(False),
]).all()
# fetch closed trades for the last 2 days
trades = Trade.get_trades_proxy(
pair=metadata['pair'], is_open=False,
open_date=datetime.now(timezone.utc) - timedelta(days=2))
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
sumprofit = sum(trade.close_profit for trade in trades)
if sumprofit < 0:

View File

@ -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.
```

View File

@ -26,7 +26,7 @@ Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version).
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows.
Other versions must be downloaded from the above link.
``` powershell

View File

@ -1,74 +0,0 @@
name: freqtrade
channels:
- conda-forge
# - defaults
dependencies:
# 1/4 req main
- python>=3.8,<=3.10
- numpy
- pandas
- pip
- py-find-1st
- aiohttp
- SQLAlchemy
- python-telegram-bot<20.0.0
- arrow
- cachetools
- requests
- urllib3
- jsonschema
- TA-Lib
- tabulate
- jinja2
- blosc
- sdnotify
- fastapi
- uvicorn
- pyjwt
- aiofiles
- psutil
- colorama
- questionary
- prompt-toolkit
- schedule
- python-dateutil
- joblib
- pyarrow
# ============================
# 2/4 req dev
- coveralls
- mypy
- pytest
- pytest-asyncio
- pytest-cov
- pytest-mock
- isort
- nbconvert
# ============================
# 3/4 req hyperopt
- scipy
- scikit-learn<1.2.0
- filelock
- scikit-optimize
- progressbar2
# ============================
# 4/4 req plot
- plotly
- jupyter
- pip:
- pycoingecko
# - py_find_1st
- tables
- pytest-random-order
- ccxt
- ruff
- -e .
# - python-rapidjso

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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.")

View File

@ -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}

View File

@ -588,6 +588,7 @@ CONF_SCHEMA = {
"rl_config": {
"type": "object",
"properties": {
"drop_ohlc_from_features": {"type": "boolean", "default": False},
"train_cycles": {"type": "integer"},
"max_trade_duration_candles": {"type": "integer"},
"add_state_info": {"type": "boolean", "default": False},

View File

@ -346,7 +346,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
return df_final[df_final['open_trades'] > max_open_trades]
def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
"""
Convert list of Trade objects to pandas Dataframe
:param trades: List of trade objects
@ -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

View File

@ -4,6 +4,7 @@ from enum import Enum
class RPCMessageType(str, Enum):
STATUS = 'status'
WARNING = 'warning'
EXCEPTION = 'exception'
STARTUP = 'startup'
ENTRY = 'entry'

View File

@ -7,6 +7,7 @@ from typing import Dict, List, Optional, Tuple
import arrow
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
@ -23,7 +24,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 +32,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",
@ -47,6 +49,26 @@ class Binance(Exchange):
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
) -> Dict:
params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
if (
time_in_force == 'PO'
and ordertype != 'market'
and self.trading_mode == TradingMode.SPOT
# Only spot can do post only orders
):
params.pop('timeInForce')
params['postOnly'] = True
return params
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES:

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,10 @@ class Bybit(Exchange):
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ohlcv_candle_limit": 200,
"ohlcv_has_history": False,
}
_ft_has_futures: Dict = {
"ohlcv_candle_limit": 200,
"ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
@ -115,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)

View File

@ -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
@ -69,6 +68,7 @@ class Exchange:
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
"ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True,
"tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers
"tickers_have_price": True,
"trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since",
@ -1020,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,
@ -1035,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
@ -1088,7 +1087,7 @@ class Exchange:
f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise ExchangeError(
raise InvalidOrderException(
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
@ -1138,8 +1137,15 @@ class Exchange:
"sell" else (stop_price >= limit_rate))
# Ensure rate is less than stop price
if bad_stop_price:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
# 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}"
)
return limit_rate
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
@ -1202,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)
@ -2527,7 +2533,6 @@ class Exchange:
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
@ -2545,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
@ -2756,10 +2761,10 @@ class Exchange:
raise OperationalException(
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
isolated_liq = None
liquidation_price = None
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
isolated_liq = self.dry_run_liquidation_price(
liquidation_price = self.dry_run_liquidation_price(
pair=pair,
open_rate=open_rate,
is_short=is_short,
@ -2774,16 +2779,16 @@ class Exchange:
positions = self.fetch_positions(pair)
if len(positions) > 0:
pos = positions[0]
isolated_liq = pos['liquidationPrice']
liquidation_price = pos['liquidationPrice']
if isolated_liq is not None:
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
isolated_liq = (
isolated_liq - buffer_amount
if liquidation_price is not None:
buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
liquidation_price_buffer = (
liquidation_price - buffer_amount
if is_short else
isolated_liq + buffer_amount
liquidation_price + buffer_amount
)
return isolated_liq
return max(liquidation_price_buffer, 0.0)
else:
return None

View File

@ -32,6 +32,7 @@ class Gate(Exchange):
_ft_has_futures: Dict = {
"needs_trading_fees": True,
"tickers_have_bid_ask": False,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type",
@ -74,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,

View File

@ -158,7 +158,6 @@ class Kraken(Exchange):
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""

View File

@ -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,
)

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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 = {}

View File

@ -114,6 +114,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
# normalize all data based on train_dataset only
prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk)
data_dictionary = dk.normalize_data(data_dictionary)
# data cleaning/analysis
@ -148,12 +149,8 @@ class BaseReinforcementLearningModel(IFreqaiModel):
env_info = self.pack_env_dict(dk.pair)
self.train_env = self.MyRLEnv(df=train_df,
prices=prices_train,
**env_info)
self.eval_env = Monitor(self.MyRLEnv(df=test_df,
prices=prices_test,
**env_info))
self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info)
self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info))
self.eval_callback = EvalCallback(self.eval_env, deterministic=True,
render=False, eval_freq=len(train_df),
best_model_save_path=str(dk.data_path))
@ -238,6 +235,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
filtered_dataframe, _ = dk.filter_features(
unfiltered_df, dk.training_features_list, training_filter=False
)
filtered_dataframe = self.drop_ohlc_from_df(filtered_dataframe, dk)
filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe)
dk.data_dictionary["prediction_features"] = filtered_dataframe
@ -285,7 +285,6 @@ class BaseReinforcementLearningModel(IFreqaiModel):
train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"]
# %-raw_volume_gen_shift-2_ETH/USDT_1h
# price data for model training and evaluation
tf = self.config['timeframe']
rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low',
@ -318,8 +317,24 @@ class BaseReinforcementLearningModel(IFreqaiModel):
prices_test.rename(columns=rename_dict, inplace=True)
prices_test.reset_index(drop=True)
train_df = self.drop_ohlc_from_df(train_df, dk)
test_df = self.drop_ohlc_from_df(test_df, dk)
return prices_train, prices_test
def drop_ohlc_from_df(self, df: DataFrame, dk: FreqaiDataKitchen):
"""
Given a dataframe, drop the ohlc data
"""
drop_list = ['%-raw_open', '%-raw_low', '%-raw_high', '%-raw_close']
if self.rl_config["drop_ohlc_from_features"]:
df.drop(drop_list, axis=1, inplace=True)
feature_list = dk.training_features_list
dk.training_features_list = [e for e in feature_list if e not in drop_list]
return df
def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any:
"""
Can be used by user if they are trying to limit_ram_usage *and*

View File

@ -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

View File

@ -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."
)
@ -1315,123 +1315,54 @@ class FreqaiDataKitchen:
dataframe: DataFrame = dataframe containing populated indicators
"""
# this is a hack to check if the user is using the populate_any_indicators function
# check if the user is using the deprecated populate_any_indicators function
new_version = inspect.getsource(strategy.populate_any_indicators) == (
inspect.getsource(IStrategy.populate_any_indicators))
if new_version:
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
if not new_version:
raise OperationalException(
"You are using the `populate_any_indicators()` function"
" which was deprecated on March 1, 2023. Please refer "
"to the strategy migration guide to use the new "
"feature_engineering_* methods: \n"
"https://www.freqtrade.io/en/stable/strategy_migration/#freqai-strategy \n"
"And the feature_engineering_* documentation: \n"
"https://www.freqtrade.io/en/latest/freqai-feature-engineering/"
)
for tf in tfs:
if tf not in base_dataframes:
base_dataframes[tf] = pd.DataFrame()
for p in pairs:
if p not in corr_dataframes:
corr_dataframes[p] = {}
if tf not in corr_dataframes[p]:
corr_dataframes[p][tf] = pd.DataFrame()
if not prediction_dataframe.empty:
dataframe = prediction_dataframe.copy()
else:
dataframe = base_dataframes[self.config["timeframe"]].copy()
corr_pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
dataframe = self.populate_features(dataframe.copy(), pair, strategy,
corr_dataframes, base_dataframes)
metadata = {"pair": pair}
dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata)
# ensure corr pairs are always last
for corr_pair in corr_pairs:
if pair == corr_pair:
continue # dont repeat anything from whitelist
if corr_pairs and do_corr_pairs:
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
corr_dataframes, base_dataframes, True)
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
self.get_unique_classes_from_labels(dataframe)
dataframe = self.remove_special_chars_from_feature_names(dataframe)
if self.config.get('reduce_df_footprint', False):
dataframe = reduce_dataframe_footprint(dataframe)
return dataframe
else:
# the user is using the populate_any_indicators functions which is deprecated
df = self.use_strategy_to_populate_indicators_old_version(
strategy, corr_dataframes, base_dataframes, pair,
prediction_dataframe, do_corr_pairs)
return df
def use_strategy_to_populate_indicators_old_version(
self,
strategy: IStrategy,
corr_dataframes: dict = {},
base_dataframes: dict = {},
pair: str = "",
prediction_dataframe: DataFrame = pd.DataFrame(),
do_corr_pairs: bool = True,
) -> DataFrame:
"""
Use the user defined strategy for populating indicators during retrain
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
(for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)
:param metadata: dict = strategy furnished pair metadata
:return:
dataframe: DataFrame = dataframe containing populated indicators
"""
# for prediction dataframe creation, we let dataprovider handle everything in the strategy
# so we create empty dictionaries, which allows us to pass None to
# `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe.
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
pairs: List[str] = self.freqai_config["feature_parameters"].get("include_corr_pairlist", [])
pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
for tf in tfs:
if tf not in base_dataframes:
base_dataframes[tf] = pd.DataFrame()
for p in pairs:
if p not in corr_dataframes:
corr_dataframes[p] = {}
if tf not in corr_dataframes[p]:
corr_dataframes[p][tf] = pd.DataFrame()
if not prediction_dataframe.empty:
dataframe = prediction_dataframe.copy()
for tf in tfs:
base_dataframes[tf] = None
for p in pairs:
if p not in corr_dataframes:
corr_dataframes[p] = {}
corr_dataframes[p][tf] = None
else:
dataframe = base_dataframes[self.config["timeframe"]].copy()
sgi = False
for tf in tfs:
if tf == tfs[-1]:
sgi = True # doing this last allows user to use all tf raw prices in labels
dataframe = strategy.populate_any_indicators(
pair,
dataframe.copy(),
tf,
informative=base_dataframes[tf],
set_generalized_indicators=sgi
)
corr_pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
dataframe = self.populate_features(dataframe.copy(), pair, strategy,
corr_dataframes, base_dataframes)
metadata = {"pair": pair}
dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata)
# ensure corr pairs are always last
for corr_pair in pairs:
for corr_pair in corr_pairs:
if pair == corr_pair:
continue # dont repeat anything from whitelist
for tf in tfs:
if pairs and do_corr_pairs:
dataframe = strategy.populate_any_indicators(
corr_pair,
dataframe.copy(),
tf,
informative=corr_dataframes[corr_pair][tf]
)
if corr_pairs and do_corr_pairs:
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
corr_dataframes, base_dataframes, True)
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
self.get_unique_classes_from_labels(dataframe)

View File

@ -1,4 +1,3 @@
import inspect
import logging
import threading
import time
@ -105,8 +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.warned_deprecated_populate_any_indicators = False
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)
@ -138,9 +139,6 @@ class IFreqaiModel(ABC):
self.data_provider = strategy.dp
self.can_short = strategy.can_short
# check if the strategy has deprecated populate_any_indicators function
self.check_deprecated_populate_any_indicators(strategy)
if self.live:
self.inference_timer('start')
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
@ -159,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
@ -344,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:
@ -491,7 +489,7 @@ class IFreqaiModel(ABC):
"strategy is furnishing the same features as the pretrained"
"model. In case of --strategy-list, please be aware that FreqAI "
"requires all strategies to maintain identical "
"populate_any_indicator() functions"
"feature_engineering_* functions"
)
def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None:
@ -603,7 +601,7 @@ class IFreqaiModel(ABC):
:param strategy: IStrategy = user defined strategy object
:param dk: FreqaiDataKitchen = non-persistent data container for current coin/loop
:param data_load_timerange: TimeRange = the amount of data to be loaded
for populate_any_indicators
for populating indicators
(larger than new_trained_timerange so that
new_trained_timerange does not contain any NaNs)
"""
@ -809,7 +807,7 @@ class IFreqaiModel(ABC):
logger.warning("Couldn't cache corr_pair dataframes for improved performance. "
"Consider ensuring that the full coin/stake, e.g. XYZ/USD, "
"is included in the column names when you are creating features "
"in `populate_any_indicators()`.")
"in `feature_engineering_*` functions.")
self.get_corr_dataframes = not bool(self.corr_dataframes)
elif self.corr_dataframes:
dataframe = dk.attach_corr_pair_columns(
@ -936,26 +934,6 @@ class IFreqaiModel(ABC):
dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred")
return dk
def check_deprecated_populate_any_indicators(self, strategy: IStrategy):
"""
Check and warn if the deprecated populate_any_indicators function is used.
:param strategy: strategy object
"""
if not self.warned_deprecated_populate_any_indicators:
self.warned_deprecated_populate_any_indicators = True
old_version = inspect.getsource(strategy.populate_any_indicators) != (
inspect.getsource(IStrategy.populate_any_indicators))
if old_version:
logger.warning("DEPRECATION WARNING: "
"You are using the deprecated populate_any_indicators function. "
"This function will raise an error on March 1 2023. "
"Please update your strategy by using "
"the new feature_engineering functions. See \n"
"https://www.freqtrade.io/en/latest/freqai-feature-engineering/"
"for details.")
# Following methods which are overridden by user made prediction models.
# See freqai/prediction_models/CatboostPredictionModel.py for an example.

View File

@ -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()

View File

@ -133,13 +133,13 @@ class FreqtradeBot(LoggingMixin):
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
self.protections = ProtectionManager(self.config, self.strategy.protections)
def notify_status(self, msg: str) -> None:
def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None:
"""
Public method for users of this class (worker, etc.) to send notifications
via RPC about changes in the bot status.
"""
self.rpc.send_msg({
'type': RPCMessageType.STATUS,
'type': msg_type,
'status': msg
})
@ -586,7 +586,7 @@ class FreqtradeBot(LoggingMixin):
min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_entry_rate,
self.strategy.stoploss)
0.0)
min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_exit_rate,
self.strategy.stoploss)
@ -594,7 +594,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,
@ -633,7 +633,7 @@ class FreqtradeBot(LoggingMixin):
return
remaining = (trade.amount - amount) * current_exit_rate
if remaining < min_exit_stake:
if min_exit_stake and remaining < min_exit_stake:
logger.info(f"Remaining amount of {remaining} would be smaller "
f"than the minimum of {min_exit_stake}.")
return
@ -700,7 +700,8 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_)
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_,
pos_adjust)
if not stake_amount:
return False
@ -809,6 +810,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
@ -818,7 +822,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
@ -850,7 +854,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(
@ -860,7 +865,12 @@ class FreqtradeBot(LoggingMixin):
trade: Optional[Trade],
order_adjust: bool,
leverage_: Optional[float],
pos_adjust: bool,
) -> Tuple[float, float, float]:
"""
Validate and eventually adjust (within limits) limit, amount and leverage
:return: Tuple with (price, amount, leverage)
"""
if price:
enter_limit_requested = price
@ -906,7 +916,9 @@ class FreqtradeBot(LoggingMixin):
# We do however also need min-stake to determine leverage, therefore this is ignored as
# edge-case for now.
min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair, enter_limit_requested, self.strategy.stoploss, leverage)
pair, enter_limit_requested,
self.strategy.stoploss if not pos_adjust else 0.0,
leverage)
max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, enter_limit_requested, leverage)
@ -1013,12 +1025,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
@ -1122,8 +1138,7 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully')
self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple(
exit_type=ExitType.EMERGENCY_EXIT))
self.emergency_exit(trade, stop_price)
except ExchangeError:
trade.stoploss_order_id = None
@ -1225,13 +1240,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):
@ -1281,13 +1291,16 @@ class FreqtradeBot(LoggingMixin):
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergency exiting trade {trade}, as the exit order '
f'timed out {max_timeouts} times.')
try:
self.execute_trade_exit(
trade, order['price'],
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception:
logger.warning(
f'Unable to emergency sell trade {trade.pair}: {exception}')
self.emergency_exit(trade, order['price'])
def emergency_exit(self, trade: Trade, price: float) -> None:
try:
self.execute_trade_exit(
trade, price,
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception:
logger.warning(
f'Unable to emergency exit trade {trade.pair}: {exception}')
def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
"""
@ -1314,7 +1327,7 @@ class FreqtradeBot(LoggingMixin):
default_retval=order_obj.price)(
trade=trade, order=order_obj, pair=trade.pair,
current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
current_order_rate=order_obj.price, entry_tag=trade.enter_tag,
current_order_rate=order_obj.safe_price, entry_tag=trade.enter_tag,
side=trade.entry_side)
replacing = True
@ -1330,7 +1343,8 @@ class FreqtradeBot(LoggingMixin):
# place new order only if new price is supplied
self.execute_entry(
pair=trade.pair,
stake_amount=(order_obj.remaining * order_obj.price / trade.leverage),
stake_amount=(
order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
price=adjusted_entry_price,
trade=trade,
is_short=trade.is_short,
@ -1344,6 +1358,8 @@ class FreqtradeBot(LoggingMixin):
"""
for trade in Trade.get_open_order_trades():
if not trade.open_order_id:
continue
try:
order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
except (ExchangeError):
@ -1368,6 +1384,9 @@ class FreqtradeBot(LoggingMixin):
"""
was_trade_fully_canceled = False
side = trade.entry_side.capitalize()
if not trade.open_order_id:
logger.warning(f"No open order for {trade}.")
return False
# Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
@ -1454,34 +1473,32 @@ class FreqtradeBot(LoggingMixin):
return False
try:
co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
order = self.exchange.cancel_order_with_result(order['id'], trade.pair,
trade.amount)
except InvalidOrderException:
logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return False
trade.close_rate = None
trade.close_rate_requested = None
trade.close_profit = None
trade.close_profit_abs = None
# Set exit_reason for fill message
exit_reason_prev = trade.exit_reason
trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
self.update_trade_state(trade, trade.open_order_id, co)
# Order might be filled above in odd timing issues.
if co.get('status') in ('canceled', 'cancelled'):
if order.get('status') in ('canceled', 'cancelled'):
trade.exit_reason = None
trade.open_order_id = None
else:
trade.exit_reason = exit_reason_prev
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
cancelled = True
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
self.update_trade_state(trade, trade.open_order_id, order)
trade.open_order_id = None
trade.exit_reason = None
self.update_trade_state(trade, trade.open_order_id, order)
logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
trade.open_order_id = None
trade.close_rate = None
trade.close_rate_requested = None
self._notify_exit_cancel(
trade,
@ -1639,7 +1656,7 @@ class FreqtradeBot(LoggingMixin):
profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate)
profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate)
else:
order_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
order_rate = trade.safe_close_rate
profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit)
profit_ratio = trade.calc_profit_ratio(order_rate)
amount = trade.amount
@ -1694,7 +1711,7 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException(
f"Order_obj not found for {order_id}. This should not have happened.")
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_rate: float = trade.safe_close_rate
profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
@ -1737,7 +1754,8 @@ class FreqtradeBot(LoggingMixin):
#
def update_trade_state(
self, trade: Trade, order_id: str, action_order: Optional[Dict[str, Any]] = None,
self, trade: Trade, order_id: Optional[str],
action_order: Optional[Dict[str, Any]] = None,
stoploss_order: bool = False, send_msg: bool = True) -> bool:
"""
Checks trades with open orders and updates the amount if necessary

View File

@ -6,8 +6,7 @@ import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Optional, Union
from typing.io import IO
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
from urllib.parse import urlparse
import orjson
@ -103,7 +102,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None:
logger.debug(f'done joblib dump to "{filename}"')
def json_load(datafile: IO) -> Any:
def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any:
"""
load data with rapidjson
Use this to have a consistent experience,

View File

@ -93,7 +93,7 @@ class Backtesting:
if self.config.get('strategy_list'):
if self.config.get('freqai', {}).get('enabled', False):
logger.warning("Using --strategy-list with FreqAI REQUIRES all strategies "
"to have identical populate_any_indicators.")
"to have identical feature_engineering_* functions.")
for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config)
stratconf['strategy'] = strat
@ -440,11 +440,8 @@ class Backtesting:
side_1 * abs(self.strategy.trailing_stop_positive / leverage)))
else:
# Worst case: price ticks tiny bit above open and dives down.
stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(trade.stop_loss_pct / leverage))
if is_short:
assert stop_rate > row[LOW_IDX]
else:
assert stop_rate < row[HIGH_IDX]
stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(
(trade.stop_loss_pct or 0.0) / leverage))
# Limit lower-end to candle low to avoid exits below the low.
# This still remains "worst case" - but "worst realistic case".
@ -472,7 +469,7 @@ class Backtesting:
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
roi_rate = trade.open_rate * roi / leverage
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1)
close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1)
if is_short:
is_new_roi = row[OPEN_IDX] < close_rate
else:
@ -525,7 +522,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,
@ -563,7 +560,7 @@ class Backtesting:
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
if pos_trade is not None:
order = pos_trade.orders[-1]
if self._get_order_filled(order.price, row):
if self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_date, trade)
trade.recalc_trade_from_orders()
self.wallets.update()
@ -664,6 +661,7 @@ class Backtesting:
side=trade.exit_side,
order_type=order_type,
status="open",
ft_price=close_rate,
price=close_rate,
average=close_rate,
amount=amount,
@ -747,7 +745,7 @@ class Backtesting:
leverage = min(max(leverage, 1.0), max_leverage)
min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair, propose_rate, -0.05, leverage=leverage) or 0
pair, propose_rate, -0.05 if not pos_adjust else 0.0, leverage=leverage) or 0
max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, propose_rate, leverage=leverage)
stake_available = self.wallets.get_available_stake_amount()
@ -887,6 +885,7 @@ class Backtesting:
order_date=current_time,
order_filled_date=current_time,
order_update_date=current_time,
ft_price=propose_rate,
price=propose_rate,
average=propose_rate,
amount=amount,
@ -895,7 +894,7 @@ class Backtesting:
cost=stake_amount + trade.fee_open,
)
trade.orders.append(order)
if pos_adjust and self._get_order_filled(order.price, row):
if pos_adjust and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_time, trade)
else:
trade.open_order_id = str(self.order_id_counter)
@ -1008,15 +1007,15 @@ class Backtesting:
# only check on new candles for open entry orders
if order.side == trade.entry_side and current_time > order.order_date_utc:
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
default_retval=order.price)(
default_retval=order.ft_price)(
trade=trade, # type: ignore[arg-type]
order=order, pair=trade.pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], current_order_rate=order.price,
proposed_rate=row[OPEN_IDX], current_order_rate=order.ft_price,
entry_tag=trade.enter_tag, side=trade.trade_direction
) # default value is current order price
# cancel existing order whenever a new rate is requested (or None)
if requested_rate == order.price:
if requested_rate == order.ft_price:
# assumption: there can't be multiple open entry orders at any given time
return False
else:
@ -1028,7 +1027,8 @@ class Backtesting:
if requested_rate:
self._enter_trade(pair=trade.pair, row=row, trade=trade,
requested_rate=requested_rate,
requested_stake=(order.remaining * order.price / trade.leverage),
requested_stake=(
order.safe_remaining * order.ft_price / trade.leverage),
direction='short' if trade.is_short else 'long')
self.replaced_entry_orders += 1
else:
@ -1095,7 +1095,7 @@ class Backtesting:
for trade in list(LocalTrade.bt_trades_open_pp[pair]):
# 3. Process entry orders.
order = trade.select_order(trade.entry_side, is_open=True)
if order and self._get_order_filled(order.price, row):
if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
self.wallets.update()
@ -1106,7 +1106,7 @@ class Backtesting:
# 5. Process exit orders.
order = trade.select_order(trade.exit_side, is_open=True)
if order and self._get_order_filled(order.price, row):
if order and self._get_order_filled(order.ft_price, row):
order.close_bt_order(current_time, trade)
trade.open_order_id = None
sub_trade = order.safe_amount_after_fee != trade.amount
@ -1115,7 +1115,7 @@ class Backtesting:
trade.recalc_trade_from_orders()
else:
trade.close_date = current_time
trade.close(order.price, show_msg=False)
trade.close(order.ft_price, show_msg=False)
# logger.debug(f"{pair} - Backtesting exit {trade}")
LocalTrade.close_bt_trade(trade)

View File

@ -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

View File

@ -1,7 +1,9 @@
from typing import Any
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import DeclarativeBase, Session, scoped_session
_DECL_BASE: Any = declarative_base()
SessionType = scoped_session[Session]
class ModelBase(DeclarativeBase):
pass

View File

@ -2,6 +2,9 @@
This module contains the class to persist trades into SQLite
"""
import logging
import threading
from contextvars import ContextVar
from typing import Any, Dict, Final, Optional
from sqlalchemy import create_engine, inspect
from sqlalchemy.exc import NoSuchModuleError
@ -9,7 +12,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.pool import StaticPool
from freqtrade.exceptions import OperationalException
from freqtrade.persistence.base import _DECL_BASE
from freqtrade.persistence.base import ModelBase
from freqtrade.persistence.migrations import check_migrate
from freqtrade.persistence.pairlock import PairLock
from freqtrade.persistence.trade_model import Order, Trade
@ -18,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'
@ -29,7 +48,7 @@ def init_db(db_url: str) -> None:
:param db_url: Database to use
:return: None
"""
kwargs = {}
kwargs: Dict[str, Any] = {}
if db_url == 'sqlite:///':
raise OperationalException(
@ -52,12 +71,12 @@ 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))
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()
_DECL_BASE.metadata.create_all(engine)
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
ModelBase.metadata.create_all(engine)
check_migrate(engine, decl_base=ModelBase, previous_tables=previous_tables)

View File

@ -1,33 +1,34 @@
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from typing import Any, ClassVar, Dict, Optional
from sqlalchemy import Boolean, Column, DateTime, Integer, String, or_
from sqlalchemy.orm import Query
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 _DECL_BASE
from freqtrade.persistence.base import ModelBase, SessionType
class PairLock(_DECL_BASE):
class PairLock(ModelBase):
"""
Pair Locks database model.
"""
__tablename__ = 'pairlocks'
session: ClassVar[SessionType]
id = Column(Integer, primary_key=True)
id: Mapped[int] = mapped_column(primary_key=True)
pair = Column(String(25), nullable=False, index=True)
pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True)
# lock direction - long, short or * (for both)
side = Column(String(25), nullable=False, default="*")
reason = Column(String(255), nullable=True)
side: Mapped[str] = mapped_column(String(25), nullable=False, default="*")
reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Time the pair was locked (start time)
lock_time = Column(DateTime(), nullable=False)
lock_time: Mapped[datetime] = mapped_column(nullable=False)
# Time until the pair is locked (end time)
lock_end_time = Column(DateTime(), nullable=False, index=True)
lock_end_time: Mapped[datetime] = mapped_column(nullable=False, index=True)
active = Column(Boolean, nullable=False, default=True, index=True)
active: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
def __repr__(self):
def __repr__(self) -> str:
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
return (
@ -35,7 +36,8 @@ class PairLock(_DECL_BASE):
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
@ -51,9 +53,11 @@ class PairLock(_DECL_BASE):
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 {

View File

@ -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,15 +128,15 @@ 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
locks = PairLocks.get_pair_locks(None)
for lock in locks:
locksb = PairLocks.get_pair_locks(None)
for lock in locksb:
if lock.reason == reason:
lock.active = False
@ -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

View File

@ -5,11 +5,11 @@ import logging
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from math import isclose
from typing import Any, Dict, List, Optional
from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
UniqueConstraint, desc, func)
from sqlalchemy.orm import Query, lazyload, 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)
@ -17,14 +17,14 @@ from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import amount_to_contract_precision, price_to_precision
from freqtrade.leverage import interest
from freqtrade.persistence.base import _DECL_BASE
from freqtrade.persistence.base import ModelBase, SessionType
from freqtrade.util import FtPrecise
logger = logging.getLogger(__name__)
class Order(_DECL_BASE):
class Order(ModelBase):
"""
Order database model
Keeps a record of all orders placed on the exchange
@ -36,41 +36,43 @@ class Order(_DECL_BASE):
Mirrors CCXT Order structure
"""
__tablename__ = 'orders'
session: ClassVar[SessionType]
# Uniqueness should be ensured over pair, order_id
# its likely that order_id is unique per Pair on some exchanges.
__table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),)
id = Column(Integer, primary_key=True)
ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True)
trade = relationship("Trade", back_populates="orders")
trade: Mapped[List["Trade"]] = relationship("Trade", back_populates="orders")
# order_side can only be 'buy', 'sell' or 'stoploss'
ft_order_side = Column(String(25), nullable=False)
ft_pair = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
ft_amount = Column(Float(), nullable=False)
ft_price = Column(Float(), nullable=False)
ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False)
ft_pair: Mapped[str] = mapped_column(String(25), nullable=False)
ft_is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
ft_amount: Mapped[float] = mapped_column(Float(), nullable=False)
ft_price: Mapped[float] = mapped_column(Float(), nullable=False)
order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True)
order_type = Column(String(50), nullable=True)
side = Column(String(25), nullable=True)
price = Column(Float(), nullable=True)
average = Column(Float(), nullable=True)
amount = Column(Float(), nullable=True)
filled = Column(Float(), nullable=True)
remaining = Column(Float(), nullable=True)
cost = Column(Float(), nullable=True)
stop_price = Column(Float(), nullable=True)
order_date = Column(DateTime(), nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime(), nullable=True)
order_update_date = Column(DateTime(), nullable=True)
order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
symbol: Mapped[Optional[str]] = mapped_column(String(25), nullable=True)
# TODO: type: order_type type is Optional[str]
order_type: Mapped[str] = mapped_column(String(50), nullable=True)
side: Mapped[str] = mapped_column(String(25), nullable=True)
price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
average: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
amount: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
filled: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
remaining: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
stop_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
order_date: Mapped[datetime] = mapped_column(nullable=True, default=datetime.utcnow)
order_filled_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
order_update_date: Mapped[Optional[datetime]] = mapped_column(nullable=True)
funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
funding_fee = Column(Float(), nullable=True)
ft_fee_base = Column(Float(), nullable=True)
ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
@property
def order_date_utc(self) -> datetime:
@ -96,6 +98,10 @@ class Order(_DECL_BASE):
def safe_filled(self) -> float:
return self.filled if self.filled is not None else self.amount or 0.0
@property
def safe_cost(self) -> float:
return self.cost or 0.0
@property
def safe_remaining(self) -> float:
return (
@ -113,8 +119,9 @@ class Order(_DECL_BASE):
def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
f'side={self.side}, order_type={self.order_type}, status={self.status})')
return (f"Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, "
f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, "
f"order_type={self.order_type}, status={self.status})")
def update_from_ccxt_object(self, order):
"""
@ -151,7 +158,7 @@ class Order(_DECL_BASE):
self.order_update_date = datetime.now(timezone.utc)
def to_ccxt_object(self) -> Dict[str, Any]:
order = {
order: Dict[str, Any] = {
'id': self.order_id,
'symbol': self.ft_pair,
'price': self.price,
@ -213,7 +220,7 @@ class Order(_DECL_BASE):
# Assumes backtesting will use date_last_filled_utc to calculate future funding fees.
self.funding_fee = trade.funding_fees
if (self.ft_order_side == trade.entry_side):
if (self.ft_order_side == trade.entry_side and self.price):
trade.open_rate = self.price
trade.recalc_trade_from_orders()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
@ -255,12 +262,12 @@ class Order(_DECL_BASE):
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']:
@ -268,7 +275,7 @@ class Order(_DECL_BASE):
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():
@ -293,15 +300,15 @@ class LocalTrade():
exchange: str = ''
pair: str = ''
base_currency: str = ''
stake_currency: str = ''
base_currency: Optional[str] = ''
stake_currency: Optional[str] = ''
is_open: bool = True
fee_open: float = 0.0
fee_open_cost: Optional[float] = None
fee_open_currency: str = ''
fee_close: float = 0.0
fee_open_currency: Optional[str] = ''
fee_close: Optional[float] = 0.0
fee_close_cost: Optional[float] = None
fee_close_currency: str = ''
fee_close_currency: Optional[str] = ''
open_rate: float = 0.0
open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value
@ -311,7 +318,7 @@ class LocalTrade():
close_profit: Optional[float] = None
close_profit_abs: Optional[float] = None
stake_amount: float = 0.0
max_stake_amount: float = 0.0
max_stake_amount: Optional[float] = 0.0
amount: float = 0.0
amount_requested: Optional[float] = None
open_date: datetime
@ -320,9 +327,9 @@ class LocalTrade():
# absolute value of the stop loss
stop_loss: float = 0.0
# percentage value of the stop loss
stop_loss_pct: float = 0.0
stop_loss_pct: Optional[float] = 0.0
# absolute value of the initial stop loss
initial_stop_loss: float = 0.0
initial_stop_loss: Optional[float] = 0.0
# percentage value of the initial stop loss
initial_stop_loss_pct: Optional[float] = None
# stoploss order id which is on exchange
@ -330,12 +337,12 @@ class LocalTrade():
# last update time of the stoploss order on exchange
stoploss_last_update: Optional[datetime] = None
# absolute value of the highest reached price
max_rate: float = 0.0
max_rate: Optional[float] = None
# Lowest price reached
min_rate: float = 0.0
exit_reason: str = ''
exit_order_status: str = ''
strategy: str = ''
min_rate: Optional[float] = None
exit_reason: Optional[str] = ''
exit_order_status: Optional[str] = ''
strategy: Optional[str] = ''
enter_tag: Optional[str] = None
timeframe: Optional[int] = None
@ -511,6 +518,8 @@ class LocalTrade():
'close_timestamp': int(self.close_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None,
'realized_profit': self.realized_profit or 0.0,
# Close-profit corresponds to relative realized_profit ratio
'realized_profit_ratio': self.close_profit or None,
'close_rate': self.close_rate,
'close_rate_requested': self.close_rate_requested,
'close_profit': self.close_profit, # Deprecated
@ -551,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,
}
@ -592,7 +604,7 @@ class LocalTrade():
self.stop_loss_pct = -1 * abs(percent)
def adjust_stop_loss(self, current_price: float, stoploss: float,
def adjust_stop_loss(self, current_price: float, stoploss: Optional[float],
initial: bool = False, refresh: bool = False) -> None:
"""
This adjusts the stop loss to it's most recently observed setting
@ -601,7 +613,7 @@ class LocalTrade():
:param initial: Called to initiate stop_loss.
Skips everything if self.stop_loss is already set.
"""
if initial and not (self.stop_loss is None or self.stop_loss == 0):
if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)):
# Don't modify if called with initial and nothing to do
return
refresh = True if refresh and self.nr_of_successful_entries == 1 else False
@ -640,7 +652,7 @@ class LocalTrade():
f"initial_stop_loss={self.initial_stop_loss:.8f}, "
f"stop_loss={self.stop_loss:.8f}. "
f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.")
def update_trade(self, order: Order) -> None:
"""
@ -792,10 +804,10 @@ class LocalTrade():
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise:
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: Optional[float]) -> FtPrecise:
close_trade = amount * FtPrecise(rate)
fees = close_trade * FtPrecise(fee)
fees = close_trade * FtPrecise(fee or 0.0)
if self.is_short:
return close_trade + fees
@ -1059,10 +1071,14 @@ class LocalTrade():
return len(self.select_filled_orders('sell'))
@property
def sell_reason(self) -> str:
def sell_reason(self) -> Optional[str]:
""" DEPRECATED! Please use exit_reason instead."""
return self.exit_reason
@property
def safe_close_rate(self) -> float:
return self.close_rate or self.close_rate_requested or 0.0
@staticmethod
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
open_date: Optional[datetime] = None,
@ -1074,6 +1090,11 @@ class LocalTrade():
In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result.
:param pair: Filter by pair
:param is_open: Filter by open/closed status
:param open_date: Filter by open_date (filters via trade.open_date > input)
:param close_date: Filter by close_date (filters via trade.close_date > input)
Will implicitly only return closed trades.
:return: unsorted List[Trade]
"""
@ -1124,7 +1145,7 @@ class LocalTrade():
@staticmethod
def get_open_trades() -> List[Any]:
"""
Query trades from persistence layer
Retrieve open trades
"""
return Trade.get_trades_proxy(is_open=True)
@ -1134,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
@ -1159,7 +1182,7 @@ class LocalTrade():
logger.info(f"New stoploss: {trade.stop_loss}.")
class Trade(_DECL_BASE, LocalTrade):
class Trade(ModelBase, LocalTrade):
"""
Trade database model.
Also handles updating and querying trades
@ -1167,79 +1190,97 @@ class Trade(_DECL_BASE, LocalTrade):
Note: Fields must be aligned with LocalTrade class
"""
__tablename__ = 'trades'
session: ClassVar[SessionType]
use_db: bool = True
id = Column(Integer, primary_key=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True) # type: ignore
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan",
lazy="selectin", innerjoin=True)
orders: Mapped[List[Order]] = relationship(
"Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin",
innerjoin=True) # type: ignore
exchange = Column(String(25), nullable=False)
pair = Column(String(25), nullable=False, index=True)
base_currency = Column(String(25), nullable=True)
stake_currency = Column(String(25), nullable=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float(), nullable=False, default=0.0)
fee_open_cost = Column(Float(), nullable=True)
fee_open_currency = Column(String(25), nullable=True)
fee_close = Column(Float(), nullable=False, default=0.0)
fee_close_cost = Column(Float(), nullable=True)
fee_close_currency = Column(String(25), nullable=True)
open_rate: float = Column(Float())
open_rate_requested = Column(Float())
exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore
pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore
base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore
stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore
is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) # type: ignore
fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore
fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
fee_open_currency: Mapped[Optional[str]] = mapped_column(
String(25), nullable=True) # type: ignore
fee_close: Mapped[Optional[float]] = mapped_column(
Float(), nullable=False, default=0.0) # type: ignore
fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
fee_close_currency: Mapped[Optional[str]] = mapped_column(
String(25), nullable=True) # type: ignore
open_rate: Mapped[float] = mapped_column(Float()) # type: ignore
open_rate_requested: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True) # type: ignore
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float())
close_rate: Optional[float] = Column(Float())
close_rate_requested = Column(Float())
realized_profit = Column(Float(), default=0.0)
close_profit = Column(Float())
close_profit_abs = Column(Float())
stake_amount = Column(Float(), nullable=False)
max_stake_amount = Column(Float())
amount = Column(Float())
amount_requested = Column(Float())
open_date = Column(DateTime(), nullable=False, default=datetime.utcnow)
close_date = Column(DateTime())
open_order_id = Column(String(255))
open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True) # type: ignore
close_rate: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore
close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore
realized_profit: Mapped[float] = mapped_column(
Float(), default=0.0, nullable=True) # type: ignore
close_profit: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore
close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore
stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) # type: ignore
max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore
amount: Mapped[float] = mapped_column(Float()) # type: ignore
amount_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore
open_date: Mapped[datetime] = mapped_column(
nullable=False, default=datetime.utcnow) # type: ignore
close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore
open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # type: ignore
# absolute value of the stop loss
stop_loss = Column(Float(), nullable=True, default=0.0)
stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore
# percentage value of the stop loss
stop_loss_pct = Column(Float(), nullable=True)
stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
# absolute value of the initial stop loss
initial_stop_loss = Column(Float(), nullable=True, default=0.0)
initial_stop_loss: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True, default=0.0) # type: ignore
# percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float(), nullable=True)
initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True) # type: ignore
# stoploss order id which is on exchange
stoploss_order_id = Column(String(255), nullable=True, index=True)
stoploss_order_id: Mapped[Optional[str]] = mapped_column(
String(255), nullable=True, index=True) # type: ignore
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime(), nullable=True)
stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore
# absolute value of the highest reached price
max_rate = Column(Float(), nullable=True, default=0.0)
max_rate: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True, default=0.0) # type: ignore
# Lowest price reached
min_rate = Column(Float(), nullable=True)
exit_reason = Column(String(100), nullable=True)
exit_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True)
enter_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True)
min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
exit_reason: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore
exit_order_status: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True) # type: ignore
strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore
enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore
timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore
trading_mode = Column(Enum(TradingMode), nullable=True)
amount_precision = Column(Float(), nullable=True)
price_precision = Column(Float(), nullable=True)
precision_mode = Column(Integer, nullable=True)
contract_size = Column(Float(), nullable=True)
trading_mode: Mapped[TradingMode] = mapped_column(
Enum(TradingMode), nullable=True) # type: ignore
amount_precision: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True) # type: ignore
price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore
contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
# Leverage trading properties
leverage = Column(Float(), nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False)
liquidation_price = Column(Float(), nullable=True)
leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) # type: ignore
is_short: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore
liquidation_price: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True) # type: ignore
# Margin Trading Properties
interest_rate = Column(Float(), nullable=False, default=0.0)
interest_rate: Mapped[float] = mapped_column(
Float(), nullable=False, default=0.0) # type: ignore
# Futures properties
funding_fees = Column(Float(), nullable=True, default=None)
funding_fees: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True, default=None) # type: ignore
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -1249,18 +1290,18 @@ class Trade(_DECL_BASE, 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,
@ -1285,7 +1326,7 @@ class Trade(_DECL_BASE, LocalTrade):
trade_filter.append(Trade.close_date > close_date)
if is_open is not None:
trade_filter.append(Trade.is_open.is_(is_open))
return Trade.get_trades(trade_filter).all()
return cast(List[LocalTrade], Trade.get_trades(trade_filter).all())
else:
return LocalTrade.get_trades_proxy(
pair=pair, is_open=is_open,
@ -1294,7 +1335,7 @@ class Trade(_DECL_BASE, LocalTrade):
)
@staticmethod
def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select:
"""
Helper function to query Trades using filters.
NOTE: Not supported in Backtesting.
@ -1309,22 +1350,35 @@ class Trade(_DECL_BASE, 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():
@ -1354,11 +1408,12 @@ class Trade(_DECL_BASE, 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
@ -1368,8 +1423,9 @@ class Trade(_DECL_BASE, 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))
@ -1381,19 +1437,22 @@ class Trade(_DECL_BASE, LocalTrade):
Returns List of dicts containing all Trades, including profit and trade count
NOTE: Not supported in Backtesting.
"""
filters = [Trade.is_open.is_(False)]
filters: List = [Trade.is_open.is_(False)]
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,
@ -1414,19 +1473,20 @@ class Trade(_DECL_BASE, LocalTrade):
NOTE: Not supported in Backtesting.
"""
filters = [Trade.is_open.is_(False)]
filters: List = [Trade.is_open.is_(False)]
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 [
{
@ -1447,19 +1507,19 @@ class Trade(_DECL_BASE, LocalTrade):
NOTE: Not supported in Backtesting.
"""
filters = [Trade.is_open.is_(False)]
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 [
{
@ -1480,21 +1540,21 @@ class Trade(_DECL_BASE, LocalTrade):
NOTE: Not supported in Backtesting.
"""
filters = [Trade.is_open.is_(False)]
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:
@ -1530,11 +1590,15 @@ class Trade(_DECL_BASE, 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
@ -1544,12 +1608,13 @@ class Trade(_DECL_BASE, 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
@ -1598,8 +1663,10 @@ class Trade(_DECL_BASE, 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"],

View File

@ -5,6 +5,7 @@ import logging
from typing import Any, Dict, Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Ticker
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -22,6 +23,12 @@ class SpreadFilter(IPairList):
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
self._enabled = self._max_spread_ratio != 0
if not self._exchange.get_option('tickers_have_bid_ask'):
raise OperationalException(
f"{self.name} requires exchange to have bid/ask data for tickers, "
"which is not available for the selected exchange / trading mode."
)
@property
def needstickers(self) -> bool:
"""

View File

@ -250,6 +250,7 @@ class TradeSchema(BaseModel):
profit_fiat: Optional[float]
realized_profit: float
realized_profit_ratio: Optional[float]
exit_reason: Optional[str]
exit_order_status: Optional[str]
@ -275,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]
@ -285,6 +290,7 @@ class OpenTradeSchema(TradeSchema):
current_rate: float
total_profit_abs: float
total_profit_fiat: Optional[float]
total_profit_ratio: Optional[float]
open_order: Optional[str]
@ -309,7 +315,7 @@ class LockModel(BaseModel):
lock_timestamp: int
pair: str
side: str
reason: str
reason: Optional[str]
class Locks(BaseModel):

View File

@ -42,7 +42,8 @@ logger = logging.getLogger(__name__)
# 2.22: Add FreqAI to backtesting
# 2.23: Allow plot config request in webserver mode
# 2.24: Add cancel_open_order endpoint
API_VERSION = 2.24
# 2.25: Add several profit values to /status endpoint
API_VERSION = 2.25
# Public API, requires no auth.
router_public = APIRouter()

View File

@ -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')

View File

@ -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
@ -122,7 +123,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 +160,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()
@ -189,9 +191,14 @@ class RPC:
else:
# Closed trade ...
current_rate = trade.close_rate
current_profit = trade.close_profit
current_profit_abs = trade.close_profit_abs
current_profit = trade.close_profit or 0.0
current_profit_abs = trade.close_profit_abs or 0.0
total_profit_abs = trade.realized_profit + current_profit_abs
total_profit_ratio: Optional[float] = None
if trade.max_stake_amount:
total_profit_ratio = (
(total_profit_abs / trade.max_stake_amount) * trade.leverage
)
# Calculate fiat profit
if not isnan(current_profit_abs) and self._fiat_converter:
@ -224,6 +231,7 @@ class RPC:
total_profit_abs=total_profit_abs,
total_profit_fiat=total_profit_fiat,
total_profit_ratio=total_profit_ratio,
stoploss_current_dist=stoploss_current_dist,
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
@ -333,11 +341,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)
@ -373,21 +383,27 @@ class RPC:
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
""" Returns the X last trades """
order_by = Trade.id if order_by_id else Trade.close_date.desc()
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()).all()
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.session.scalar(
select(func.count(Trade.id)).filter(Trade.is_open.is_(False)))
return {
"trades": output,
"trades_count": len(output),
"offset": offset,
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
"total_trades": total_trades,
}
def _rpc_stats(self) -> Dict[str, Any]:
@ -401,7 +417,7 @@ class RPC:
return 'losses'
else:
return 'draws'
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
# Sell reason
exit_reasons = {}
for trade in trades:
@ -410,7 +426,7 @@ class RPC:
exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
# Duration
dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []}
for trade in trades:
if trade.close_date is not None and trade.open_date is not None:
trade_dur = (trade.close_date - trade.open_date).total_seconds()
@ -429,8 +445,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 = []
@ -449,11 +465,11 @@ class RPC:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_ratio = trade.close_profit
profit_abs = trade.close_profit_abs
profit_ratio = trade.close_profit or 0.0
profit_abs = trade.close_profit_abs or 0.0
profit_closed_coin.append(profit_abs)
profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0:
if profit_ratio >= 0:
winning_trades += 1
winning_profit += profit_abs
else:
@ -506,7 +522,7 @@ class RPC:
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'profit_abs': trade.close_profit_abs}
for trade in trades if not trade.is_open])
for trade in trades if not trade.is_open and trade.close_date])
max_drawdown_abs = 0.0
max_drawdown = 0.0
if len(trades_df) > 0:
@ -785,7 +801,8 @@ class RPC:
# check if valid pair
# check if pair already has an open pair
trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
trade: Optional[Trade] = Trade.get_trades(
[Trade.is_open.is_(True), Trade.pair == pair]).first()
is_short = (order_side == SignalDirection.SHORT)
if trade:
is_short = trade.is_short
@ -938,12 +955,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

View File

@ -83,6 +83,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
@ -321,31 +323,33 @@ class Telegram(RPCHandler):
and self._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
msg['profit_extra'] = (
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}")
msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}"
else:
msg['profit_extra'] = ''
msg['profit_extra'] = (
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
f"{msg['profit_extra']})")
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
is_sub_trade = msg.get('sub_trade')
is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit')
profit_prefix = ('Sub ' if is_sub_profit
else 'Cumulative ') if is_sub_trade else ''
profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else ''
cp_extra = ''
exit_wording = 'Exited' if is_fill else 'Exiting'
if is_sub_profit and is_sub_trade:
if self._rpc._fiat_converter:
cp_fiat = self._rpc._fiat_converter.convert_amount(
msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency'])
cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}"
else:
cp_extra = ''
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
f"{msg['stake_currency']}{cp_extra}`)\n"
exit_wording = f"Partially {exit_wording.lower()}"
cp_extra = (
f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} "
f"{msg['stake_currency']}{cp_extra}`)\n"
)
message = (
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n"
f"{self._add_analyzed_candle(msg['pair'])}"
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
@ -364,7 +368,7 @@ class Telegram(RPCHandler):
elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
if msg.get('sub_trade'):
if is_sub_trade:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
@ -412,6 +416,9 @@ class Telegram(RPCHandler):
elif msg_type == RPCMessageType.WARNING:
message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
elif msg_type == RPCMessageType.EXCEPTION:
# Errors will contain exceptions, which are wrapped in tripple ticks.
message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}"
elif msg_type == RPCMessageType.STARTUP:
message = f"{msg['status']}"
@ -486,7 +493,9 @@ class Telegram(RPCHandler):
if order_nr == 1:
lines.append(f"*{wording} #{order_nr}:*")
lines.append(
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
f"*Amount:* {cur_entry_amount} "
f"({round_coin_value(order['cost'], quote_currency)})"
)
lines.append(f"*Average Price:* {cur_entry_average}")
else:
sum_stake = 0
@ -506,14 +515,14 @@ class Telegram(RPCHandler):
if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg profit")
lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg Profit")
if is_open:
lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"])))
lines.append(f"*Amount:* {cur_entry_amount} "
f"({round_coin_value(order['cost'], quote_currency)})")
lines.append(f"*Average {wording} Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)")
f"({price_to_1st_entry:.2%} from 1st entry Rate)")
lines.append(f"*Order filled:* {order['order_filled_date']}")
# TODO: is this really useful?
@ -561,8 +570,12 @@ class Telegram(RPCHandler):
for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry']
and not o['ft_order_side'] == 'stoploss'])
r['exit_reason'] = r.get('exit_reason', "")
r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency'])
r['max_stake_amount_r'] = round_coin_value(
r['max_stake_amount'] or r['stake_amount'], r['quote_currency'])
r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency'])
r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency'])
r['total_profit_abs_r'] = round_coin_value(
@ -574,29 +587,37 @@ class Telegram(RPCHandler):
f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}"
+ " ` ({leverage}x)`" if r.get('leverage') else "",
"*Amount:* `{amount} ({stake_amount_r})`",
"*Total invested:* `{max_stake_amount_r}`" if position_adjust else "",
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
]
if position_adjust:
max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "")
lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str)
lines.extend([
"*Number of Entries:* `{num_entries}" + max_buy_str + "`",
"*Number of Exits:* `{num_exits}`"
])
lines.extend([
"*Open Rate:* `{open_rate:.8f}`",
"*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "",
"*Open Date:* `{open_date}`",
"*Close Date:* `{close_date}`" if r['close_date'] else "",
"*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "",
" \n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "",
("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *")
+ "`{profit_ratio:.2%}` `({profit_abs_r})`",
])
if r['is_open']:
if r.get('realized_profit'):
lines.append("*Realized Profit:* `{realized_profit_r}`")
lines.append("*Total Profit:* `{total_profit_abs_r}` ")
lines.extend([
"*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`",
"*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`"
])
# Append empty line to improve readability
lines.append(" ")
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss
@ -1055,10 +1076,14 @@ class Telegram(RPCHandler):
query.answer()
query.edit_message_text(text="Force exit canceled.")
return
trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
query.answer()
query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
self._force_exit_action(trade_id)
if trade:
query.edit_message_text(
text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
self._force_exit_action(trade_id)
else:
query.edit_message_text(text=f"Trade {trade_id} not found.")
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel':
@ -1317,7 +1342,7 @@ class Telegram(RPCHandler):
message = tabulate({k: [v] for k, v in counts.items()},
headers=['current', 'max', 'total stake'],
tablefmt='simple')
message = "<pre>{}</pre>".format(message)
message = f"<pre>{message}</pre>"
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_count",
@ -1619,7 +1644,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

View File

@ -58,6 +58,7 @@ class Webhook(RPCHandler):
valuedict = whconfig.get('webhookexitcancel')
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.EXCEPTION,
RPCMessageType.WARNING):
valuedict = whconfig.get('webhookstatus')
elif msg['type'].value in whconfig:
@ -112,7 +113,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()

View File

@ -86,37 +86,41 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
def stoploss_from_open(
open_relative_stop: float,
current_profit: float,
is_short: bool = False
is_short: bool = False,
leverage: float = 1.0
) -> float:
"""
Given the current profit, and a desired stop loss value relative to the open price,
Given the current profit, and a desired stop loss value relative to the trade entry price,
return a stop loss value that is relative to the current price, and which can be
returned from `custom_stoploss`.
The requested stop can be positive for a stop above the open price, or negative for
a stop below the open price. The return value is always >= 0.
`open_relative_stop` will be considered as adjusted for leverage if leverage is provided..
Returns 0 if the resulting stop price would be above/below (longs/shorts) the current price
:param open_relative_stop: Desired stop loss percentage relative to open price
:param open_relative_stop: Desired stop loss percentage, relative to the open price,
adjusted for leverage
:param current_profit: The current profit percentage
:param is_short: When true, perform the calculation for short instead of long
:param leverage: Leverage to use for the calculation
:return: Stop loss value relative to current price
"""
# formula is undefined for current_profit -1 (longs) or 1 (shorts), return maximum value
if (current_profit == -1 and not is_short) or (is_short and current_profit == 1):
_current_profit = current_profit / leverage
if (_current_profit == -1 and not is_short) or (is_short and _current_profit == 1):
return 1
if is_short is True:
stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit))
stoploss = -1 + ((1 - open_relative_stop / leverage) / (1 - _current_profit))
else:
stoploss = 1 - ((1 + open_relative_stop) / (1 + current_profit))
stoploss = 1 - ((1 + open_relative_stop / leverage) / (1 + _current_profit))
# negative stoploss values indicate the requested stop price is higher/lower
# (long/short) than the current price
return max(stoploss, 0.0)
return max(stoploss * leverage, 0.0)
def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float:

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -12,7 +12,7 @@ import sdnotify
from freqtrade import __version__
from freqtrade.configuration import Configuration
from freqtrade.constants import PROCESS_THROTTLE_SECS, RETRY_TIMEOUT, Config
from freqtrade.enums import State
from freqtrade.enums import RPCMessageType, State
from freqtrade.exceptions import OperationalException, TemporaryError
from freqtrade.exchange import timeframe_to_next_date
from freqtrade.freqtradebot import FreqtradeBot
@ -185,7 +185,10 @@ class Worker:
tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.'
self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}')
self.freqtrade.notify_status(
f'*OperationalException:*\n```\n{tb}```\n {hint}',
msg_type=RPCMessageType.EXCEPTION
)
logger.exception('OperationalException. Stopping trader ...')
self.freqtrade.state = State.STOPPED

View File

@ -1,3 +1,7 @@
[build-system]
requires = ["setuptools >= 46.4.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 100
exclude = '''
@ -35,6 +39,9 @@ warn_unused_ignores = true
exclude = [
'^build_helpers\.py$'
]
plugins = [
"sqlalchemy.ext.mypy.plugin"
]
[[tool.mypy.overrides]]
module = "tests.*"
@ -45,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 = [
@ -65,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"]

View File

@ -7,11 +7,11 @@
-r docs/requirements-docs.txt
coveralls==3.3.1
ruff==0.0.253
mypy==1.0.1
pre-commit==3.1.1
pytest==7.2.1
pytest-asyncio==0.20.3
ruff==0.0.257
mypy==1.1.1
pre-commit==3.2.0
pytest==7.2.2
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-tabulate==0.9.0.1
types-python-dateutil==2.8.19.9
types-python-dateutil==2.8.19.10

View File

@ -2,9 +2,9 @@
-r requirements-freqai.txt
# Required for freqai-rl
torch==1.13.1
stable-baselines3==1.7.0
sb3-contrib==1.7.0
torch==1.13.1; python_version < '3.11'
stable-baselines3==1.7.0; python_version < '3.11'
sb3-contrib==1.7.0; python_version < '3.11'
# Gym is forced to this version by stable-baselines3.
setuptools==65.5.1 # Should be removed when gym is fixed.
gym==0.21
gym==0.21; python_version < '3.11'

View File

@ -5,7 +5,7 @@
# Required for freqai
scikit-learn==1.1.3
joblib==1.2.0
catboost==1.1.1; platform_machine != 'aarch64'
catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11'
lightgbm==3.3.5
xgboost==1.7.4
tensorboard==2.12.0

View File

@ -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.0
progressbar2==4.2.0

View File

@ -2,15 +2,15 @@ numpy==1.24.2
pandas==1.5.3
pandas-ta==0.3.14b
ccxt==2.8.54
cryptography==39.0.1
ccxt==3.0.23
cryptography==39.0.2
aiohttp==3.8.4
SQLAlchemy==1.4.46
SQLAlchemy==2.0.7
python-telegram-bot==13.15
arrow==1.2.3
cachetools==4.2.2
requests==2.28.2
urllib3==1.26.14
urllib3==1.26.15
jsonschema==4.17.3
TA-Lib==0.4.25
technical==1.4.0
@ -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.6
orjson==3.8.7
# Notify systemd
sdnotify==0.3.2
# API Server
fastapi==0.92.0
pydantic==1.10.5
uvicorn==0.20.0
fastapi==0.95.0
pydantic==1.10.6
uvicorn==0.21.1
pyjwt==2.6.0
aiofiles==23.1.0
psutil==5.9.4
@ -45,7 +45,7 @@ psutil==5.9.4
colorama==0.4.6
# Building config files interactively
questionary==1.10.0
prompt-toolkit==3.0.37
prompt-toolkit==3.0.38
# Extensions to datetime library
python-dateutil==2.8.2
@ -55,3 +55,5 @@ schedule==1.1.0
#WS Messages
websockets==10.4
janus==1.0.0
ast-comments==1.0.1

View File

@ -340,11 +340,13 @@ class FtRestClient():
:param limit: Limit result to the last n candles.
:return: json object
"""
return self._get("pair_candles", params={
params = {
"pair": pair,
"timeframe": timeframe,
"limit": limit,
})
}
if limit:
params['limit'] = limit
return self._get("pair_candles", params=params)
def pair_history(self, pair, timeframe, strategy, timerange=None):
"""Return historic, analyzed dataframe

View File

@ -17,6 +17,7 @@ classifiers =
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Operating System :: MacOS
Operating System :: Unix
Topic :: Office/Business :: Financial :: Investment

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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', {'postOnly': True}),
('sell', 'limit', 'PO', {'postOnly': True}),
('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
)

View File

@ -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,
},
@ -463,7 +463,9 @@ class TestCCXTExchange():
if exchangename == 'gate':
# TODO: Gate is unstable here at the moment, ignoring the limit partially.
return
for val in [1, 2, 5, 25, 100]:
for val in [1, 2, 5, 25, 50, 100]:
if val > 50 and exchangename == 'bybit':
continue
l2 = exch.fetch_l2_order_book(pair, val)
if not l2_limit_range or val in l2_limit_range:
if val > 50:

View File

@ -13,8 +13,8 @@ from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
InvalidOrderException, OperationalException, PricingError,
TemporaryError)
InsufficientFundsError, InvalidOrderException,
OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision,
date_minus_candles, market_is_active, price_to_precision,
timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
@ -1052,9 +1052,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),
@ -1612,13 +1612,13 @@ def test_sell_prod(default_conf, mocker, exchange_name):
assert api_mock.create_order.call_args[0][4] == 200
# test exception handling
with pytest.raises(DependencyException):
with pytest.raises(InsufficientFundsError):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200,
leverage=1.0)
with pytest.raises(DependencyException):
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200,
@ -3400,7 +3400,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'
@ -3881,29 +3881,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)

View File

@ -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)

View File

@ -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},

View File

@ -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)

View File

@ -78,7 +78,9 @@ def make_rl_config(conf):
"rr": 1,
"profit_aim": 0.02,
"win_reward_factor": 2
}}
},
"drop_ohlc_from_features": False
}
return conf

View File

@ -35,8 +35,8 @@ def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, c
args = get_args(args)
bt_config = setup_optimize_configuration(args, RunMode.BACKTEST)
Backtesting(bt_config)
assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical '
'populate_any_indicators.', caplog)
assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical',
caplog)
Backtesting.cleanup()

View File

@ -1,5 +1,6 @@
import platform
import shutil
import sys
from pathlib import Path
from unittest.mock import MagicMock
@ -17,6 +18,10 @@ from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_h
from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config
def is_py11() -> bool:
return sys.version_info >= (3, 11)
def is_arm() -> bool:
machine = platform.machine()
return "arm" in machine or "aarch64" in machine
@ -27,6 +32,17 @@ def is_mac() -> bool:
return "Darwin" in machine
def can_run_model(model: str) -> None:
if (is_arm() or is_py11()) and "Catboost" in model:
pytest.skip("CatBoost is not supported on ARM")
if is_mac() and not is_arm() and 'Reinforcement' in model:
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
if is_py11() and 'Reinforcement' in model:
pytest.skip("Reinforcement learning currently not available on python 3.11.")
@pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [
('LightGBMRegressor', True, False, True, True, False, 0),
('XGBoostRegressor', False, True, False, True, False, 10),
@ -41,12 +57,7 @@ def is_mac() -> bool:
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
dbscan, float32, can_short, shuffle, buffer):
if is_arm() and model == 'CatboostRegressor':
pytest.skip("CatBoost is not supported on ARM")
if is_mac() and not is_arm() and 'Reinforcement' in model:
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
can_run_model(model)
model_save_ext = 'joblib'
freqai_conf.update({"freqaimodel": model})
freqai_conf.update({"timerange": "20180110-20180130"})
@ -57,13 +68,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
freqai_conf['freqai']['feature_parameters'].update({"shuffle_after_split": shuffle})
freqai_conf['freqai']['feature_parameters'].update({"buffer_train_data_candles": buffer})
if 'ReinforcementLearner' in model:
model_save_ext = 'zip'
freqai_conf = make_rl_config(freqai_conf)
# test the RL guardrails
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
if 'ReinforcementLearner' in model:
model_save_ext = 'zip'
freqai_conf = make_rl_config(freqai_conf)
@ -73,6 +77,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
if 'test_3ac' in model or 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
@ -117,7 +122,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat")
])
def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat):
if is_arm() and 'Catboost' in model:
if (is_arm() or is_py11()) and 'Catboost' in model:
pytest.skip("CatBoost is not supported on ARM")
freqai_conf.update({"timerange": "20180110-20180130"})
@ -159,7 +164,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s
'XGBoostRFClassifier',
])
def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
if is_arm() and model == 'CatboostClassifier':
if (is_arm() or is_py11()) and model == 'CatboostClassifier':
pytest.skip("CatBoost is not supported on ARM")
freqai_conf.update({"freqaimodel": model})
@ -206,13 +211,11 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
],
)
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog):
can_run_model(model)
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
freqai_conf['runmode'] = RunMode.BACKTEST
if is_arm() and "Catboost" in model:
pytest.skip("CatBoost is not supported on ARM")
if is_mac() and 'Reinforcement' in model:
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
Trade.use_db = False
freqai_conf.update({"freqaimodel": model})
@ -509,6 +512,8 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers):
if is_mac():
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
if is_py11():
pytest.skip("Reinforcement learning currently not available on python 3.11.")
freqai_conf.update({"freqaimodel": "ReinforcementLearner"})
freqai_conf.update({"timerange": "20180110-20180130"})

View File

@ -924,7 +924,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
mocker.patch(f"{EXMS}.get_fee", return_value=0.0)
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('freqtrade.exchange.binance.Binance.get_max_leverage', return_value=100)
mocker.patch(f"{EXMS}.get_max_leverage", return_value=100)
patch_exchange(mocker)
frame = _build_backtest_dataframe(data.data)
backtesting = Backtesting(default_conf)

View File

@ -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-<timestamp>_signals.pkl
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()

View File

@ -4,7 +4,7 @@ from pathlib import Path
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine, text
from sqlalchemy import create_engine, select, text
from freqtrade.constants import DEFAULT_DB_PROD_URL
from freqtrade.enums import TradingMode
@ -21,8 +21,8 @@ spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURE
def test_init_create_session(default_conf):
# Check if init create a session
init_db(default_conf['db_url'])
assert hasattr(Trade, '_session')
assert 'scoped_session' in type(Trade._session).__name__
assert hasattr(Trade, 'session')
assert 'scoped_session' in type(Trade.session).__name__
def test_init_custom_db_url(default_conf, tmpdir):
@ -34,7 +34,7 @@ def test_init_custom_db_url(default_conf, tmpdir):
init_db(default_conf['db_url'])
assert Path(filename).is_file()
r = Trade._session.execute(text("PRAGMA journal_mode"))
r = Trade.session.execute(text("PRAGMA journal_mode"))
assert r.first() == ('wal',)
@ -235,8 +235,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
# Run init to test migration
init_db(default_conf['db_url'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
trades = Trade.session.scalars(select(Trade).filter(Trade.id == 1)).all()
assert len(trades) == 1
trade = trades[0]
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
@ -404,9 +405,9 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
init_db(default_conf['db_url'])
assert len(PairLock.query.all()) == 2
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
assert len(PairLock.get_all_locks().all()) == 2
assert len(PairLock.session.scalars(select(PairLock).filter(PairLock.pair == '*')).all()) == 1
pairlocks = PairLock.session.scalars(select(PairLock).filter(PairLock.pair == 'ETH/BTC')).all()
assert len(pairlocks) == 1
pairlocks[0].pair == 'ETH/BTC'
pairlocks[0].side == '*'

View File

@ -4,6 +4,7 @@ from types import FunctionType
import arrow
import pytest
from sqlalchemy import select
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import TradingMode
@ -1329,70 +1330,78 @@ def test_to_json(fee):
open_rate=0.123,
exchange='binance',
enter_tag=None,
open_order_id='dry_run_buy_12345'
open_order_id='dry_run_buy_12345',
precision_mode=1,
amount_precision=8.0,
price_precision=7.0,
)
result = trade.to_json()
assert isinstance(result, dict)
assert result == {'trade_id': None,
'pair': 'ADA/USDT',
'base_currency': 'ADA',
'quote_currency': 'USDT',
'is_open': None,
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'open_order_id': 'dry_run_buy_12345',
'close_date': None,
'close_timestamp': None,
'open_rate': 0.123,
'open_rate_requested': None,
'open_trade_value': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None,
'close_rate_requested': None,
'amount': 123.0,
'amount_requested': 123.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': None,
'trade_duration_s': None,
'realized_profit': 0.0,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'exit_reason': None,
'exit_order_status': None,
'stop_loss_abs': None,
'stop_loss_ratio': None,
'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'min_rate': None,
'max_rate': None,
'strategy': None,
'enter_tag': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'orders': [],
}
assert result == {
'trade_id': None,
'pair': 'ADA/USDT',
'base_currency': 'ADA',
'quote_currency': 'USDT',
'is_open': None,
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'open_order_id': 'dry_run_buy_12345',
'close_date': None,
'close_timestamp': None,
'open_rate': 0.123,
'open_rate_requested': None,
'open_trade_value': 15.1668225,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'close_rate': None,
'close_rate_requested': None,
'amount': 123.0,
'amount_requested': 123.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': None,
'trade_duration_s': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'exit_reason': None,
'exit_order_status': None,
'stop_loss_abs': None,
'stop_loss_ratio': None,
'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'min_rate': None,
'max_rate': None,
'strategy': None,
'enter_tag': None,
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'amount_precision': 8.0,
'price_precision': 7.0,
'precision_mode': 1,
'orders': [],
}
# Simulate dry_run entries
trade = Trade(
@ -1408,69 +1417,77 @@ def test_to_json(fee):
close_rate=0.125,
enter_tag='buys_signal_001',
exchange='binance',
precision_mode=2,
amount_precision=7.0,
price_precision=8.0,
)
result = trade.to_json()
assert isinstance(result, dict)
assert result == {'trade_id': None,
'pair': 'XRP/BTC',
'base_currency': 'XRP',
'quote_currency': 'BTC',
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'close_timestamp': int(trade.close_date.timestamp() * 1000),
'open_rate': 0.123,
'close_rate': 0.125,
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': 60,
'trade_duration_s': 3600,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'realized_profit': 0.0,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None,
'max_rate': None,
'min_rate': None,
'open_order_id': None,
'open_rate_requested': None,
'open_trade_value': 12.33075,
'exit_reason': None,
'exit_order_status': None,
'strategy': None,
'enter_tag': 'buys_signal_001',
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'orders': [],
}
assert result == {
'trade_id': None,
'pair': 'XRP/BTC',
'base_currency': 'XRP',
'quote_currency': 'BTC',
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
'open_timestamp': int(trade.open_date.timestamp() * 1000),
'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
'close_timestamp': int(trade.close_date.timestamp() * 1000),
'open_rate': 0.123,
'close_rate': 0.125,
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': 60,
'trade_duration_s': 3600,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None,
'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'close_rate_requested': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
'fee_open': 0.0025,
'fee_open_cost': None,
'fee_open_currency': None,
'is_open': None,
'max_rate': None,
'min_rate': None,
'open_order_id': None,
'open_rate_requested': None,
'open_trade_value': 12.33075,
'exit_reason': None,
'exit_order_status': None,
'strategy': None,
'enter_tag': 'buys_signal_001',
'timeframe': None,
'exchange': 'binance',
'leverage': None,
'interest_rate': None,
'liquidation_price': None,
'is_short': None,
'trading_mode': None,
'funding_fees': None,
'amount_precision': 7.0,
'price_precision': 8.0,
'precision_mode': 2,
'orders': [],
}
def test_stoploss_reinitialization(default_conf, fee):
@ -1492,7 +1509,7 @@ def test_stoploss_reinitialization(default_conf, fee):
assert trade.stop_loss_pct == -0.05
assert trade.initial_stop_loss == 0.95
assert trade.initial_stop_loss_pct == -0.05
Trade.query.session.add(trade)
Trade.session.add(trade)
Trade.commit()
# Lower stoploss
@ -1554,7 +1571,7 @@ def test_stoploss_reinitialization_leverage(default_conf, fee):
assert trade.stop_loss_pct == -0.1
assert trade.initial_stop_loss == 0.98
assert trade.initial_stop_loss_pct == -0.1
Trade.query.session.add(trade)
Trade.session.add(trade)
Trade.commit()
# Lower stoploss
@ -1616,7 +1633,7 @@ def test_stoploss_reinitialization_short(default_conf, fee):
assert trade.stop_loss_pct == -0.1
assert trade.initial_stop_loss == 1.02
assert trade.initial_stop_loss_pct == -0.1
Trade.query.session.add(trade)
Trade.session.add(trade)
Trade.commit()
# Lower stoploss
Trade.stoploss_reinitialization(-0.15)
@ -1791,17 +1808,17 @@ def test_get_trades_proxy(fee, use_db, is_short):
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('is_short', [True, False])
def test_get_trades__query(fee, is_short):
query = Trade.get_trades([])
query = Trade.get_trades_query([])
# without orders there should be no join issued.
query1 = Trade.get_trades([], include_orders=False)
query1 = Trade.get_trades_query([], include_orders=False)
# Empty "with-options -> default - selectin"
assert query._with_options == ()
assert query1._with_options != ()
create_mock_trades(fee, is_short)
query = Trade.get_trades([])
query1 = Trade.get_trades([], include_orders=False)
query = Trade.get_trades_query([])
query1 = Trade.get_trades_query([], include_orders=False)
assert query._with_options == ()
assert query1._with_options != ()
@ -2014,6 +2031,7 @@ def test_Trade_object_idem():
'get_open_trades_without_assigned_fees',
'get_open_order_trades',
'get_trades',
'get_trades_query',
'get_exit_reason_performance',
'get_enter_tag_performance',
'get_mix_tag_performance',
@ -2440,8 +2458,9 @@ def test_select_filled_orders(fee):
def test_order_to_ccxt(limit_buy_order_open):
order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy')
order.query.session.add(order)
Order.query.session.commit()
order.ft_trade_id = 1
order.session.add(order)
Order.session.commit()
order_resp = Order.order_by_id(limit_buy_order_open['id'])
assert order_resp
@ -2543,7 +2562,7 @@ def test_recalc_trade_from_orders_dca(data) -> None:
leverage=1.0,
trading_mode=TradingMode.SPOT
)
Trade.query.session.add(trade)
Trade.session.add(trade)
for idx, (order, result) in enumerate(data['orders']):
amount = order[1]
@ -2572,11 +2591,11 @@ def test_recalc_trade_from_orders_dca(data) -> None:
trade.recalc_trade_from_orders()
Trade.commit()
orders1 = Order.query.all()
orders1 = Order.session.scalars(select(Order)).all()
assert orders1
assert len(orders1) == idx + 1
trade = Trade.query.first()
trade = Trade.session.scalars(select(Trade)).first()
assert trade
assert len(trade.orders) == idx + 1
if idx < len(data) - 1:
@ -2593,6 +2612,6 @@ def test_recalc_trade_from_orders_dca(data) -> None:
assert pytest.approx(trade.close_profit_abs) == data['end_profit']
assert pytest.approx(trade.close_profit) == data['end_profit_ratio']
assert not trade.is_open
trade = Trade.query.first()
trade = Trade.session.scalars(select(Trade)).first()
assert trade
assert trade.open_order_id is None

View File

@ -50,8 +50,8 @@ def test_trade_fromjson():
"stop_loss_ratio": -0.216,
"stop_loss_pct": -21.6,
"stoploss_order_id": null,
"stoploss_last_update": null,
"stoploss_last_update_timestamp": null,
"stoploss_last_update": "2022-10-18 09:13:42",
"stoploss_last_update_timestamp": 1666077222000,
"initial_stop_loss_abs": 0.1981,
"initial_stop_loss_ratio": -0.216,
"initial_stop_loss_pct": -21.6,

View File

@ -711,8 +711,8 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None:
def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
if hasattr(Trade, 'query'):
del Trade.query
if hasattr(Trade, 'session'):
del Trade.session
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
exchange = get_patched_exchange(mocker, whitelist_conf)
pm = PairListManager(exchange, whitelist_conf, MagicMock())
@ -828,6 +828,12 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N
match=r'Exchange does not support fetchTickers, .*'):
get_patched_freqtradebot(mocker, default_conf)
mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
mocker.patch(f'{EXMS}.get_option', MagicMock(return_value=False))
with pytest.raises(OperationalException,
match=r'.*requires exchange to have bid/ask data'):
get_patched_freqtradebot(mocker, default_conf)
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):

View File

@ -14,7 +14,7 @@ def test_PairLocks(use_db):
PairLocks.use_db = use_db
# No lock should be present
if use_db:
assert len(PairLock.query.all()) == 0
assert len(PairLock.get_all_locks().all()) == 0
assert PairLocks.use_db == use_db
@ -88,13 +88,13 @@ def test_PairLocks(use_db):
if use_db:
locks = PairLocks.get_all_locks()
locks_db = PairLock.query.all()
locks_db = PairLock.get_all_locks().all()
assert len(locks) == len(locks_db)
assert len(locks_db) > 0
else:
# Nothing was pushed to the database
assert len(PairLocks.get_all_locks()) > 0
assert len(PairLock.query.all()) == 0
assert len(PairLock.get_all_locks().all()) == 0
# Reset use-db variable
PairLocks.reset_locks()
PairLocks.use_db = True
@ -107,7 +107,7 @@ def test_PairLocks_getlongestlock(use_db):
# No lock should be present
PairLocks.use_db = use_db
if use_db:
assert len(PairLock.query.all()) == 0
assert len(PairLock.get_all_locks().all()) == 0
assert PairLocks.use_db == use_db
@ -139,7 +139,7 @@ def test_PairLocks_reason(use_db):
PairLocks.use_db = use_db
# No lock should be present
if use_db:
assert len(PairLock.query.all()) == 0
assert len(PairLock.get_all_locks().all()) == 0
assert PairLocks.use_db == use_db

View File

@ -74,7 +74,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
trade.close(close_price)
trade.exit_reason = exit_reason
Trade.query.session.add(trade)
Trade.session.add(trade)
Trade.commit()
return trade

View File

@ -4,6 +4,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock
import pytest
from numpy import isnan
from sqlalchemy import select
from freqtrade.edge import PairInfo
from freqtrade.enums import SignalDirection, State, TradingMode
@ -50,7 +51,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'amount': 91.07468123,
'amount_requested': 91.07468124,
'stake_amount': 0.001,
'max_stake_amount': ANY,
'max_stake_amount': None,
'trade_duration': None,
'trade_duration_s': None,
'close_profit': None,
@ -76,8 +77,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10376381,
'open_order': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'total_profit_abs': -4.09e-06,
'total_profit_fiat': ANY,
'total_profit_ratio': None,
'exchange': 'binance',
'leverage': 1.0,
'interest_rate': 0.0,
@ -85,6 +88,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT,
'amount_precision': 8.0,
'price_precision': 8.0,
'precision_mode': 2,
'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
@ -122,17 +128,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'profit_pct': 0.0,
'profit_abs': 0.0,
'total_profit_abs': 0.0,
'stop_loss_abs': 0.0,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_current_dist': -1.099e-05,
'stoploss_current_dist_ratio': -1.0,
'stoploss_current_dist_pct': pytest.approx(-100.0),
'stoploss_entry_dist': -0.0010025,
'stoploss_entry_dist_ratio': -1.0,
'initial_stop_loss_abs': 0.0,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'open_order': '(limit buy rem=91.07468123)',
})
response_unfilled['orders'][0].update({
@ -167,6 +162,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
results = rpc._rpc_trade_status()
response = deepcopy(gen_response)
response.update({
'max_stake_amount': 0.001,
'total_profit_ratio': pytest.approx(-0.00409),
})
assert results[0] == response
mocker.patch(f'{EXMS}.get_rate',
@ -180,10 +179,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_current_dist': ANY,
'stoploss_current_dist_ratio': ANY,
'stoploss_current_dist_pct': ANY,
'max_stake_amount': 0.001,
'profit_ratio': ANY,
'profit_pct': ANY,
'profit_abs': ANY,
'total_profit_abs': ANY,
'total_profit_ratio': ANY,
'current_rate': ANY,
})
assert results[0] == response_norate
@ -346,7 +347,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
with pytest.raises(RPCException, match='invalid argument'):
rpc._rpc_delete('200')
trades = Trade.query.all()
trades = Trade.session.scalars(select(Trade)).all()
trades[1].stoploss_order_id = '1234'
trades[2].stoploss_order_id = '1234'
assert len(trades) > 2
@ -709,7 +710,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
mocker.patch(f'{EXMS}._dry_is_price_crossed', MagicMock(return_value=False))
freqtradebot.enter_positions()
# make an limit-buy open trade
trade = Trade.query.filter(Trade.id == '3').first()
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first()
filled_amount = trade.amount / 2
# Fetch order - it's open first, and closed after cancel_order is called.
mocker.patch(
@ -745,7 +746,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
freqtradebot.config['max_open_trades'] = 3
freqtradebot.enter_positions()
trade = Trade.query.filter(Trade.id == '2').first()
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
@ -763,7 +764,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
trade = Trade.query.filter(Trade.id == '3').first()
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first()
# make an limit-sell open trade
mocker.patch(

View File

@ -14,6 +14,7 @@ from fastapi import FastAPI, WebSocketDisconnect
from fastapi.exceptions import HTTPException
from fastapi.testclient import TestClient
from requests.auth import _basic_auth_str
from sqlalchemy import select
from freqtrade.__init__ import __version__
from freqtrade.enums import CandleType, RunMode, State, TradingMode
@ -624,7 +625,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short):
assert rc.json()['offset'] == 0
create_mock_trades(fee, is_short=is_short)
Trade.query.session.flush()
Trade.session.flush()
rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc)
@ -652,7 +653,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short):
assert_response(rc, 404)
assert rc.json()['detail'] == 'Trade not found.'
Trade.query.session.rollback()
Trade.rollback()
create_mock_trades(fee, is_short=is_short)
rc = client_get(client, f"{BASE_URI}/trade/3")
@ -677,7 +678,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
create_mock_trades(fee, is_short=is_short)
ftbot.strategy.order_types['stoploss_on_exchange'] = True
trades = Trade.query.all()
trades = Trade.session.scalars(select(Trade)).all()
trades[1].stoploss_order_id = '1234'
Trade.commit()
assert len(trades) > 2
@ -685,7 +686,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
rc = client_delete(client, f"{BASE_URI}/trades/1")
assert_response(rc)
assert rc.json()['result_msg'] == 'Deleted trade 1. Closed 1 open orders.'
assert len(trades) - 1 == len(Trade.query.all())
assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all())
assert cancel_mock.call_count == 1
cancel_mock.reset_mock()
@ -694,11 +695,11 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
assert_response(rc, 502)
assert cancel_mock.call_count == 0
assert len(trades) - 1 == len(Trade.query.all())
assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all())
rc = client_delete(client, f"{BASE_URI}/trades/2")
assert_response(rc)
assert rc.json()['result_msg'] == 'Deleted trade 2. Closed 2 open orders.'
assert len(trades) - 2 == len(Trade.query.all())
assert len(trades) - 2 == len(Trade.session.scalars(select(Trade)).all())
assert stoploss_mock.call_count == 1
rc = client_delete(client, f"{BASE_URI}/trades/502")
@ -943,7 +944,7 @@ def test_api_performance(botclient, fee):
)
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
Trade.query.session.add(trade)
Trade.session.add(trade)
trade = Trade(
pair='XRP/ETH',
@ -960,7 +961,7 @@ def test_api_performance(botclient, fee):
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
Trade.query.session.add(trade)
Trade.session.add(trade)
Trade.commit()
rc = client_get(client, f"{BASE_URI}/performance")
@ -1012,7 +1013,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'profit_fiat': ANY,
'total_profit_abs': ANY,
'total_profit_fiat': ANY,
'total_profit_ratio': ANY,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'current_rate': current_rate,
'open_date': ANY,
'open_timestamp': ANY,
@ -1063,6 +1066,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'liquidation_price': None,
'funding_fees': None,
'trading_mode': ANY,
'amount_precision': None,
'price_precision': None,
'precision_mode': None,
'orders': [ANY],
}
@ -1179,7 +1185,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
ftbot.config['force_entry_enable'] = True
fbuy_mock = MagicMock(return_value=None)
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock)
rc = client_post(client, f"{BASE_URI}/{endpoint}",
data={"pair": "ETH/BTC"})
assert_response(rc)
@ -1205,7 +1211,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
strategy=CURRENT_TEST_STRATEGY,
trading_mode=TradingMode.SPOT
))
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock)
rc = client_post(client, f"{BASE_URI}/{endpoint}",
data={"pair": "ETH/BTC"})
@ -1243,6 +1249,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'profit_abs': None,
'profit_fiat': None,
'realized_profit': 0.0,
'realized_profit_ratio': None,
'fee_close': 0.0025,
'fee_close_cost': None,
'fee_close_currency': None,
@ -1267,6 +1274,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'liquidation_price': None,
'funding_fees': None,
'trading_mode': 'spot',
'amount_precision': None,
'price_precision': None,
'precision_mode': None,
'orders': [],
}
@ -1287,7 +1297,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
data={"tradeid": "1"})
assert_response(rc, 502)
assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"}
Trade.query.session.rollback()
Trade.rollback()
create_mock_trades(fee)
trade = Trade.get_trades([Trade.id == 5]).first()
@ -1296,7 +1306,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
data={"tradeid": "5", "ordertype": "market", "amount": 23})
assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 5.'}
Trade.query.session.rollback()
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
assert pytest.approx(trade.amount) == 100
@ -1306,7 +1316,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
data={"tradeid": "5"})
assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 5.'}
Trade.query.session.rollback()
Trade.rollback()
trade = Trade.get_trades([Trade.id == 5]).first()
assert trade.is_open is False

View File

@ -14,6 +14,7 @@ import arrow
import pytest
import time_machine
from pandas import DataFrame
from sqlalchemy import select
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
from telegram.error import BadRequest, NetworkError, TelegramError
@ -198,6 +199,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'current_rate': 1.098e-05,
'amount': 90.99181074,
'stake_amount': 90.99181074,
'max_stake_amount': 90.99181074,
'buy_tag': None,
'enter_tag': None,
'close_profit_ratio': None,
@ -279,6 +281,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
assert msg_mock.call_count == 4
msg = msg_mock.call_args_list[0][0][0]
assert re.search(r'Number of Entries.*2', msg)
assert re.search(r'Number of Exits.*0', msg)
assert re.search(r'Average Entry Price', msg)
assert re.search(r'Order filled', msg)
assert re.search(r'Close Date:', msg) is None
@ -300,8 +303,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
create_mock_trades(fee)
trades = Trade.get_trades([Trade.is_open.is_(False)])
trade = trades[0]
trade = Trade.get_trades([Trade.is_open.is_(False)]).first()
context = MagicMock()
context.args = [str(trade.id)]
telegram._status(update=update, context=context)
@ -650,7 +652,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
# Since we loaded the last 12 months, any month should appear
assert str('-09') in msg_mock.call_args_list[0][0][0]
assert '-09' in msg_mock.call_args_list[0][0][0]
# Try invalid data
msg_mock.reset_mock()
@ -669,11 +671,12 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac
context = MagicMock()
context.args = ["february"]
telegram._monthly(update=update, context=context)
assert str('Monthly Profit over the last 6 months</b>:') in msg_mock.call_args_list[0][0][0]
assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee,
limit_sell_order_usdt, mocker) -> None:
def test_telegram_profit_handle(
default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee,
limit_sell_order_usdt, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
mocker.patch.multiple(
EXMS,
@ -691,7 +694,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
trade = Trade.session.scalars(select(Trade)).first()
context = MagicMock()
# Test with invalid 2nd argument (should silently pass)
@ -708,6 +711,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f
# Update the ticker with a market going up
mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up)
# Simulate fulfilled LIMIT_SELL order for trade
trade = Trade.session.scalars(select(Trade)).first()
oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.orders.append(oobj)
@ -944,7 +948,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee,
# Create some test data
freqtradebot.enter_positions()
trade = Trade.query.first()
trade = Trade.session.scalars(select(Trade)).first()
assert trade
# Increase the price and sell it
@ -1019,7 +1023,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee,
fetch_ticker=ticker_sell_down
)
trade = Trade.query.first()
trade = Trade.session.scalars(select(Trade)).first()
assert trade
# /forceexit 1
@ -1209,7 +1213,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
fbuy_mock = MagicMock(return_value=None)
mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock)
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock)
telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf)
patch_get_signal(freqtradebot)
@ -1226,7 +1230,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None:
# Reset and retry with specified price
fbuy_mock = MagicMock(return_value=None)
mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock)
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock)
# /forcelong ETH/BTC 0.055
context = MagicMock()
context.args = ["ETH/BTC", "0.055"]
@ -1255,7 +1259,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
fbuy_mock = MagicMock(return_value=None)
mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock)
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock)
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
@ -1728,14 +1732,14 @@ def test_version_handle(default_conf, update, mocker) -> None:
telegram._version(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
freqtradebot.strategy.version = lambda: '1.1.1'
telegram._version(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
@ -2011,7 +2015,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'sub_trade': True,
})
assert msg_mock.call_args[0][0] == (
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
'\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n'
'*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
'*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
'*Enter Tag:* `buy_signal1`\n'

View File

@ -7,6 +7,7 @@ from datetime import datetime
from pandas import DataFrame
from freqtrade.persistence.trade_model import Order
from freqtrade.strategy.interface import IStrategy
@ -35,7 +36,7 @@ class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata)
def check_buy_timeout(self, pair: str, trade, order: dict,
def check_buy_timeout(self, pair: str, trade, order: Order,
current_time: datetime, **kwargs) -> bool:
return False
@ -44,6 +45,6 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata)
def check_sell_timeout(self, pair: str, trade, order: dict,
def check_sell_timeout(self, pair: str, trade, order: Order,
current_time: datetime, **kwargs) -> bool:
return False

View File

@ -197,7 +197,7 @@ class StrategyTestV3(IStrategy):
if current_profit < -0.0075:
orders = trade.select_filled_orders(trade.entry_side)
return round(orders[0].cost, 0)
return round(orders[0].safe_cost, 0)
return None

Some files were not shown because too many files have changed in this diff Show More