Compare commits
3 Commits
use-parque
...
2023.2
Author | SHA1 | Date | |
---|---|---|---|
|
a31045874e | ||
|
25724ef729 | ||
|
46458bf5eb |
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@@ -16,8 +16,7 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
repository-projects: read
|
||||
|
||||
jobs:
|
||||
build_linux:
|
||||
|
||||
@@ -25,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -91,14 +90,14 @@ jobs:
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
flake8
|
||||
|
||||
- name: Sort imports (isort)
|
||||
run: |
|
||||
isort --check .
|
||||
|
||||
- name: Run Ruff
|
||||
run: |
|
||||
ruff check --format=github .
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
mypy freqtrade scripts tests
|
||||
@@ -116,7 +115,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-latest ]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -187,14 +186,14 @@ jobs:
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Flake8
|
||||
run: |
|
||||
flake8
|
||||
|
||||
- name: Sort imports (isort)
|
||||
run: |
|
||||
isort --check .
|
||||
|
||||
- name: Run Ruff
|
||||
run: |
|
||||
ruff check --format=github .
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
mypy freqtrade scripts
|
||||
@@ -213,7 +212,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest ]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -249,9 +248,9 @@ jobs:
|
||||
freqtrade create-userdir --userdir user_data
|
||||
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all
|
||||
|
||||
- name: Run Ruff
|
||||
- name: Flake8
|
||||
run: |
|
||||
ruff check --format=github .
|
||||
flake8
|
||||
|
||||
- name: Mypy
|
||||
run: |
|
||||
@@ -322,6 +321,7 @@ 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.8.1
|
||||
uses: pypa/gh-action-pypi-publish@v1.6.4
|
||||
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.8.1
|
||||
uses: pypa/gh-action-pypi-publish@v1.6.4
|
||||
if: (github.event_name == 'release')
|
||||
with:
|
||||
user: __token__
|
||||
@@ -466,13 +466,12 @@ 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]
|
||||
@@ -495,9 +494,8 @@ 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
|
||||
|
||||
|
@@ -8,17 +8,16 @@ repos:
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.0.1"
|
||||
rev: "v0.991"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.4
|
||||
- types-cachetools==5.3.0.0
|
||||
- types-filelock==3.2.7
|
||||
- types-requests==2.28.11.15
|
||||
- types-tabulate==0.9.0.1
|
||||
- types-python-dateutil==2.8.19.10
|
||||
- SQLAlchemy==2.0.7
|
||||
- types-requests==2.28.11.13
|
||||
- types-tabulate==0.9.0.0
|
||||
- types-python-dateutil==2.8.19.6
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -28,12 +27,6 @@ repos:
|
||||
name: isort (python)
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.255'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
|
@@ -45,17 +45,16 @@ pytest tests/test_<file_name>.py::test_<method_name>
|
||||
|
||||
### 2. Test if your code is PEP8 compliant
|
||||
|
||||
#### Run Ruff
|
||||
#### Run Flake8
|
||||
|
||||
```bash
|
||||
ruff .
|
||||
flake8 freqtrade tests scripts
|
||||
```
|
||||
|
||||
We receive a lot of code that fails the `ruff` checks.
|
||||
We receive a lot of code that fails the `flake8` checks.
|
||||
To help with that, we encourage you to install the git pre-commit
|
||||
hook that will warn you when you try to commit code that fails these checks.
|
||||
|
||||
you can manually run pre-commit with `pre-commit run -a`.
|
||||
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||
|
||||
##### Additional styles applied
|
||||
|
||||
|
@@ -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 '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 \
|
||||
&& 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 \
|
||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||
&& make
|
||||
if [ $? -ne 0 ]; then
|
||||
|
@@ -8,17 +8,12 @@ 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-') or r.startswith('SQLAlchemy')]
|
||||
type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')]
|
||||
|
||||
with pre_commit_file.open('r') as file:
|
||||
f = yaml.load(file, Loader=yaml.FullLoader)
|
||||
|
@@ -3,10 +3,6 @@
|
||||
# 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
|
||||
@@ -18,6 +14,7 @@ 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}"
|
||||
|
||||
@@ -41,13 +38,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
|
||||
@@ -62,6 +59,7 @@ 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
|
||||
@@ -84,30 +82,14 @@ 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"
|
||||
|
@@ -2,8 +2,6 @@
|
||||
|
||||
# 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
|
||||
@@ -13,6 +11,7 @@ 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
|
||||
|
@@ -12,9 +12,6 @@ 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
|
||||
|
||||
|
@@ -74,8 +74,3 @@ 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.
|
||||
|
@@ -24,7 +24,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt
|
||||
To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||
Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
|
||||
|
||||
This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`.
|
||||
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||
|
||||
Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing.
|
||||
This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine.
|
||||
|
@@ -84,7 +84,6 @@ 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
|
||||
|
||||
|
@@ -176,11 +176,9 @@ 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_{pair}_"
|
||||
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_"
|
||||
f"{self.config['timeframe']}"].iloc[self._current_tick]
|
||||
|
||||
# reward agent for entering trades
|
||||
@@ -248,13 +246,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("invalid")
|
||||
self.tensorboard_log("is_valid")
|
||||
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)`. In this case the metric values are not incremented.
|
||||
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.
|
||||
|
||||
### Choosing a base environment
|
||||
|
||||
|
@@ -71,10 +71,6 @@ 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.
|
||||
|
@@ -290,8 +290,10 @@ cd freqtrade
|
||||
|
||||
#### Freqtrade install: Conda Environment
|
||||
|
||||
Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
|
||||
|
||||
```bash
|
||||
conda create --name freqtrade python=3.10
|
||||
conda env create -n freqtrade-conda -f environment.yml
|
||||
```
|
||||
|
||||
!!! Note "Creating Conda Environment"
|
||||
@@ -300,9 +302,12 @@ conda create --name freqtrade python=3.10
|
||||
```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 environment
|
||||
#### Enter/exit freqtrade-conda environment
|
||||
|
||||
To check available environments, type
|
||||
|
||||
@@ -314,7 +319,7 @@ Enter installed environment
|
||||
|
||||
```bash
|
||||
# enter conda environment
|
||||
conda activate freqtrade
|
||||
conda activate freqtrade-conda
|
||||
|
||||
# exit conda environment - don't do it now
|
||||
conda deactivate
|
||||
@@ -324,7 +329,6 @@ 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 .
|
||||
```
|
||||
|
||||
@@ -332,7 +336,7 @@ Patch conda libta-lib (Linux only)
|
||||
|
||||
```bash
|
||||
# Ensure that the environment is active!
|
||||
conda activate freqtrade
|
||||
conda activate freqtrade-conda
|
||||
|
||||
cd build_helpers
|
||||
bash install_ta-lib.sh ${CONDA_PREFIX} nosudo
|
||||
@@ -351,8 +355,8 @@ conda env list
|
||||
# activate base environment
|
||||
conda activate
|
||||
|
||||
# activate freqtrade environment
|
||||
conda activate freqtrade
|
||||
# activate freqtrade-conda environment
|
||||
conda activate freqtrade-conda
|
||||
|
||||
#deactivate any conda environments
|
||||
conda deactivate
|
||||
|
@@ -1,6 +1,6 @@
|
||||
markdown==3.3.7
|
||||
mkdocs==1.4.2
|
||||
mkdocs-material==9.1.3
|
||||
mkdocs-material==9.0.13
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==9.10
|
||||
pymdown-extensions==9.9.2
|
||||
jinja2==3.1.2
|
||||
|
@@ -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, leverage=trade.leverage)
|
||||
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short)
|
||||
elif current_profit > 0.25:
|
||||
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short)
|
||||
elif current_profit > 0.20:
|
||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage)
|
||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||
|
||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||
return 1
|
||||
|
@@ -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 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.
|
||||
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.
|
||||
|
||||
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
|
||||
|
||||
@@ -889,8 +889,6 @@ 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
|
||||
|
||||
@@ -909,7 +907,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, leverage=trade.leverage)
|
||||
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
|
||||
|
||||
return 1
|
||||
|
||||
@@ -956,14 +954,12 @@ In some situations it may be confusing to deal with stops relative to current ra
|
||||
|
||||
## Additional data (Wallets)
|
||||
|
||||
The strategy provides access to the `wallets` object. This contains the current balances on the exchange.
|
||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||
|
||||
!!! Note "Backtesting / Hyperopt"
|
||||
Wallets behaves differently depending on the function it's called.
|
||||
Within `populate_*()` methods, it'll return the full wallet as configured.
|
||||
Within [callbacks](strategy-callbacks.md), you'll get the wallet state corresponding to the actual simulated wallet at that point in the simulation process.
|
||||
!!! Note
|
||||
Wallets is not available during backtesting / hyperopt.
|
||||
|
||||
Please always check if `wallets` is available to avoid failures during backtesting.
|
||||
Please always check if `Wallets` is available to avoid failures during backtesting.
|
||||
|
||||
``` python
|
||||
if self.wallets:
|
||||
@@ -1041,9 +1037,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_proxy(
|
||||
pair=metadata['pair'], is_open=False,
|
||||
open_date=datetime.now(timezone.utc) - timedelta(days=2))
|
||||
trades = Trade.get_trades([Trade.pair == metadata['pair'],
|
||||
Trade.open_date > datetime.utcnow() - timedelta(days=2),
|
||||
Trade.is_open.is_(False),
|
||||
]).all()
|
||||
# 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:
|
||||
|
@@ -152,7 +152,7 @@ You can create your own keyboard in `config.json`:
|
||||
!!! Note "Supported Commands"
|
||||
Only the following commands are allowed. Command arguments are not supported!
|
||||
|
||||
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir`
|
||||
`/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`
|
||||
|
||||
## Telegram commands
|
||||
|
||||
@@ -179,7 +179,6 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/count` | Displays number of trades used and available
|
||||
| `/locks` | Show currently locked pairs.
|
||||
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
||||
| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed.
|
||||
| **Modify Trade states** |
|
||||
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||
@@ -243,7 +242,7 @@ Enter Tag is configurable via Strategy.
|
||||
> **Enter Tag:** Awesome Long Signal
|
||||
> **Open Rate:** `0.00007489`
|
||||
> **Current Rate:** `0.00007489`
|
||||
> **Unrealized Profit:** `12.95%`
|
||||
> **Current Profit:** `12.95%`
|
||||
> **Stoploss:** `0.00007389 (-0.02%)`
|
||||
|
||||
### /status table
|
||||
@@ -417,27 +416,3 @@ ARDR/ETH 0.366667 0.143059 -0.01
|
||||
### /version
|
||||
|
||||
> **Version:** `0.14.3`
|
||||
|
||||
### /marketdir
|
||||
|
||||
If a market direction is provided the command updates the user managed variable that represents the current market direction.
|
||||
This variable is not set to any valid market direction on bot startup and must be set by the user. The example below is for `/marketdir long`:
|
||||
|
||||
```
|
||||
Successfully updated marketdirection from none to long.
|
||||
```
|
||||
|
||||
If no market direction is provided the command outputs the currently set market directions. The example below is for `/marketdir`:
|
||||
|
||||
```
|
||||
Currently set marketdirection: even
|
||||
```
|
||||
|
||||
You can use the market direction in your strategy via `self.market_direction`.
|
||||
|
||||
!!! Warning "Bot restarts"
|
||||
Please note that the market direction is not persisted, and will be reset after a bot restart/reload.
|
||||
|
||||
!!! Danger "Backtesting"
|
||||
As this value/variable is intended to be changed manually in dry/live trading.
|
||||
Strategies using `market_direction` will probably not produce reliable, reproducible results (changes to this variable will not be reflected for backtesting). Use at your own risk.
|
||||
|
@@ -955,47 +955,3 @@ 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.
|
||||
|
||||
```
|
||||
|
@@ -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, 3.10 and 3.11) and for 64bit Windows.
|
||||
Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows.
|
||||
Other versions must be downloaded from the above link.
|
||||
|
||||
``` powershell
|
||||
|
@@ -0,0 +1,75 @@
|
||||
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
|
||||
- flake8
|
||||
- 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
|
||||
- flake8-tidy-imports
|
||||
- -e .
|
||||
# - python-rapidjso
|
||||
|
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.3.dev'
|
||||
__version__ = '2023.2'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
@@ -22,6 +22,5 @@ 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
|
||||
|
@@ -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
|
||||
|
@@ -111,13 +111,10 @@ 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",
|
||||
"strategy-updater"]
|
||||
"plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"]
|
||||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
|
||||
ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||
|
||||
|
||||
class Arguments:
|
||||
"""
|
||||
@@ -201,8 +198,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_strategy_update,
|
||||
start_test_pairlist, start_trading, start_webserver)
|
||||
start_plot_profit, start_show_trades, start_test_pairlist,
|
||||
start_trading, start_webserver)
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
# Use custom message when no subhandler is added
|
||||
@@ -443,11 +440,3 @@ 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)
|
||||
|
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from freqtrade.configuration import TimeRange, setup_utils_configuration
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
|
||||
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
|
||||
refresh_backtest_trades_data)
|
||||
@@ -20,24 +20,15 @@ from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _data_download_sanity(config: Config) -> None:
|
||||
if 'days' in config and 'timerange' in config:
|
||||
raise OperationalException("--days and --timerange are mutually exclusive. "
|
||||
"You can only specify one or the other.")
|
||||
|
||||
if 'pairs' not in config:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
|
||||
|
||||
def start_download_data(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Download data (former download_backtest_data.py script)
|
||||
"""
|
||||
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||
|
||||
_data_download_sanity(config)
|
||||
if 'days' in config and 'timerange' in config:
|
||||
raise OperationalException("--days and --timerange are mutually exclusive. "
|
||||
"You can only specify one or the other.")
|
||||
timerange = TimeRange()
|
||||
if 'days' in config:
|
||||
time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d")
|
||||
@@ -49,6 +40,11 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
# Remove stake-currency to skip checks which are not relevant for datadownload
|
||||
config['stake_currency'] = ''
|
||||
|
||||
if 'pairs' not in config:
|
||||
raise OperationalException(
|
||||
"Downloading data requires a list of pairs. "
|
||||
"Please check the documentation on how to configure this.")
|
||||
|
||||
pairs_not_available: List[str] = []
|
||||
|
||||
# Init exchange
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func
|
||||
|
||||
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.get_all_locks():
|
||||
for pairlock in PairLock.query:
|
||||
pairlock_count += 1
|
||||
make_transient(pairlock)
|
||||
session_target.add(pairlock)
|
||||
session_target.commit()
|
||||
|
||||
# Update sequences
|
||||
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)))
|
||||
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()
|
||||
|
||||
set_sequence_ids(session_target.get_bind(),
|
||||
trade_id=max_trade_id,
|
||||
|
@@ -1,55 +0,0 @@
|
||||
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.")
|
@@ -27,7 +27,10 @@ def _extend_validator(validator_class):
|
||||
if 'default' in subschema:
|
||||
instance.setdefault(prop, subschema['default'])
|
||||
|
||||
yield from validate_properties(validator, properties, instance, schema)
|
||||
for error in validate_properties(
|
||||
validator, properties, instance, schema,
|
||||
):
|
||||
yield error
|
||||
|
||||
return validators.extend(
|
||||
validator_class, {'properties': set_defaults}
|
||||
|
@@ -58,7 +58,7 @@ def load_config_file(path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
try:
|
||||
# Read config from stdin if requested in the options
|
||||
with Path(path).open() if path != '-' else sys.stdin as file:
|
||||
with open(path) if path != '-' else sys.stdin as file:
|
||||
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
|
||||
except FileNotFoundError:
|
||||
raise OperationalException(
|
||||
|
@@ -588,7 +588,6 @@ 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},
|
||||
|
@@ -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: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame:
|
||||
def trade_list_to_dataframe(trades: 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(list(Trade.get_trades(filters).all()))
|
||||
trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
|
||||
|
||||
return trades
|
||||
|
||||
|
@@ -24,7 +24,7 @@ def _load_signal_candles(backtest_dir: Path):
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
||||
|
||||
try:
|
||||
with scpf.open("rb") as scp:
|
||||
scp = open(scpf, "rb")
|
||||
signal_candles = joblib.load(scp)
|
||||
logger.info(f"Loaded signal candles: {str(scpf)}")
|
||||
except Exception as e:
|
||||
|
@@ -5,7 +5,6 @@ from freqtrade.enums.exitchecktuple import ExitCheckTuple
|
||||
from freqtrade.enums.exittype import ExitType
|
||||
from freqtrade.enums.hyperoptstate import HyperoptState
|
||||
from freqtrade.enums.marginmode import MarginMode
|
||||
from freqtrade.enums.marketstatetype import MarketDirection
|
||||
from freqtrade.enums.ordertypevalue import OrderTypeValues
|
||||
from freqtrade.enums.pricetype import PriceType
|
||||
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
|
||||
|
@@ -13,9 +13,6 @@ class CandleType(str, Enum):
|
||||
FUNDING_RATE = "funding_rate"
|
||||
# BORROW_RATE = "borrow_rate" # * unimplemented
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
@staticmethod
|
||||
def from_string(value: str) -> 'CandleType':
|
||||
if not value:
|
||||
|
@@ -1,15 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MarketDirection(Enum):
|
||||
"""
|
||||
Enum for various market directions.
|
||||
"""
|
||||
LONG = "long"
|
||||
SHORT = "short"
|
||||
EVEN = "even"
|
||||
NONE = "none"
|
||||
|
||||
def __str__(self):
|
||||
# convert to string
|
||||
return self.value
|
@@ -4,7 +4,6 @@ from enum import Enum
|
||||
class RPCMessageType(str, Enum):
|
||||
STATUS = 'status'
|
||||
WARNING = 'warning'
|
||||
EXCEPTION = 'exception'
|
||||
STARTUP = 'startup'
|
||||
|
||||
ENTRY = 'entry'
|
||||
@@ -38,8 +37,5 @@ class RPCRequestType(str, Enum):
|
||||
WHITELIST = 'whitelist'
|
||||
ANALYZED_DF = 'analyzed_df'
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE)
|
||||
|
@@ -10,9 +10,6 @@ class SignalType(Enum):
|
||||
ENTER_SHORT = "enter_short"
|
||||
EXIT_SHORT = "exit_short"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
|
||||
class SignalTagType(Enum):
|
||||
"""
|
||||
@@ -21,13 +18,7 @@ class SignalTagType(Enum):
|
||||
ENTER_TAG = "enter_tag"
|
||||
EXIT_TAG = "exit_tag"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
||||
|
||||
class SignalDirection(str, Enum):
|
||||
LONG = 'long'
|
||||
SHORT = 'short'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name.lower()}"
|
||||
|
@@ -7,7 +7,6 @@ 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
|
||||
@@ -24,7 +23,7 @@ class Binance(Exchange):
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"order_time_in_force": ['GTC', 'FOK', 'IOC'],
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
@@ -32,7 +31,6 @@ 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",
|
||||
@@ -49,26 +47,6 @@ 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:
|
||||
@@ -217,7 +195,7 @@ class Binance(Exchange):
|
||||
leverage_tiers_path = (
|
||||
Path(__file__).parent / 'binance_leverage_tiers.json'
|
||||
)
|
||||
with leverage_tiers_path.open() as json_file:
|
||||
with open(leverage_tiers_path) as json_file:
|
||||
return json_load(json_file)
|
||||
else:
|
||||
try:
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -27,10 +27,11 @@ class Bybit(Exchange):
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 200,
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"ohlcv_has_history": False,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"ohlcv_candle_limit": 200,
|
||||
"ohlcv_has_history": True,
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"funding_fee_timeframe": "8h",
|
||||
@@ -114,7 +115,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, accept_fail: bool = False):
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
params = {'leverage': leverage}
|
||||
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
|
||||
|
@@ -60,6 +60,7 @@ 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
|
||||
@@ -68,7 +69,6 @@ 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",
|
||||
@@ -1018,10 +1018,10 @@ class Exchange:
|
||||
|
||||
# Order handling
|
||||
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
self.set_margin_mode(pair, self.margin_mode, accept_fail)
|
||||
self._set_leverage(leverage, pair, accept_fail)
|
||||
self.set_margin_mode(pair, self.margin_mode)
|
||||
self._set_leverage(leverage, pair)
|
||||
|
||||
def _get_params(
|
||||
self,
|
||||
@@ -1033,7 +1033,8 @@ class Exchange:
|
||||
) -> Dict:
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'GTC' and ordertype != 'market':
|
||||
params.update({'timeInForce': time_in_force.upper()})
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: time_in_force.upper()})
|
||||
if reduceOnly:
|
||||
params.update({'reduceOnly': True})
|
||||
return params
|
||||
@@ -1085,7 +1086,7 @@ class Exchange:
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
raise ExchangeError(
|
||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
@@ -1135,15 +1136,8 @@ class Exchange:
|
||||
"sell" else (stop_price >= limit_rate))
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_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}"
|
||||
)
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
return limit_rate
|
||||
|
||||
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||
@@ -1206,7 +1200,7 @@ class Exchange:
|
||||
|
||||
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
|
||||
|
||||
self._lev_prep(pair, leverage, side, accept_fail=True)
|
||||
self._lev_prep(pair, leverage, side)
|
||||
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)
|
||||
@@ -1967,8 +1961,7 @@ class Exchange:
|
||||
cache: bool, drop_incomplete: bool) -> DataFrame:
|
||||
# keeping last candle time as last refreshed time of the pair
|
||||
if ticks and cache:
|
||||
idx = -2 if drop_incomplete and len(ticks) > 1 else -1
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000
|
||||
self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
|
||||
# keeping parsed dataframe in cache
|
||||
ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
|
||||
drop_incomplete=drop_incomplete)
|
||||
@@ -2041,9 +2034,7 @@ class Exchange:
|
||||
# Timeframe in seconds
|
||||
interval_in_sec = timeframe_to_seconds(timeframe)
|
||||
plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
|
||||
# current,active candle open date
|
||||
now = int(timeframe_to_prev_date(timeframe).timestamp())
|
||||
return plr < now
|
||||
return plr < arrow.utcnow().int_timestamp
|
||||
|
||||
@retrier_async
|
||||
async def _async_get_candle_history(
|
||||
@@ -2531,6 +2522,7 @@ class Exchange:
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
@@ -2548,7 +2540,7 @@ class Exchange:
|
||||
self._log_exchange_response('set_leverage', res)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
|
||||
except ccxt.BadRequest as e:
|
||||
if not accept_fail:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -2759,10 +2751,10 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
|
||||
|
||||
liquidation_price = None
|
||||
isolated_liq = None
|
||||
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
||||
|
||||
liquidation_price = self.dry_run_liquidation_price(
|
||||
isolated_liq = self.dry_run_liquidation_price(
|
||||
pair=pair,
|
||||
open_rate=open_rate,
|
||||
is_short=is_short,
|
||||
@@ -2777,16 +2769,16 @@ class Exchange:
|
||||
positions = self.fetch_positions(pair)
|
||||
if len(positions) > 0:
|
||||
pos = positions[0]
|
||||
liquidation_price = pos['liquidationPrice']
|
||||
isolated_liq = pos['liquidationPrice']
|
||||
|
||||
if liquidation_price is not None:
|
||||
buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
|
||||
liquidation_price_buffer = (
|
||||
liquidation_price - buffer_amount
|
||||
if isolated_liq is not None:
|
||||
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
||||
isolated_liq = (
|
||||
isolated_liq - buffer_amount
|
||||
if is_short else
|
||||
liquidation_price + buffer_amount
|
||||
isolated_liq + buffer_amount
|
||||
)
|
||||
return max(liquidation_price_buffer, 0.0)
|
||||
return isolated_liq
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@@ -32,7 +32,6 @@ 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",
|
||||
@@ -75,7 +74,8 @@ class Gate(Exchange):
|
||||
)
|
||||
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
||||
params['type'] = 'market'
|
||||
params.update({'timeInForce': 'IOC'})
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: 'IOC'})
|
||||
return params
|
||||
|
||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||
|
@@ -158,6 +158,7 @@ class Kraken(Exchange):
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
|
@@ -64,7 +64,6 @@ class Kucoin(Exchange):
|
||||
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
|
||||
# Since we rely on status heavily, we must set it to 'open' here.
|
||||
# ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553)
|
||||
if not self._config['dry_run']:
|
||||
res['type'] = ordertype
|
||||
res['status'] = 'open'
|
||||
return res
|
||||
|
@@ -1,16 +1,14 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import 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, RetryableOrderError,
|
||||
TemporaryError)
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, 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__)
|
||||
@@ -26,13 +24,11 @@ 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": "slTriggerPxType",
|
||||
"stop_price_type_field": "tpTriggerPxType",
|
||||
"stop_price_type_value_mapping": {
|
||||
PriceType.LAST: "last",
|
||||
PriceType.MARK: "index",
|
||||
@@ -125,9 +121,10 @@ class Okx(Exchange):
|
||||
return params
|
||||
|
||||
@retrier
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
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,
|
||||
@@ -160,78 +157,3 @@ 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,
|
||||
)
|
||||
|
@@ -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], category="actions")
|
||||
self.tensorboard_log(self.actions._member_names_[action])
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
|
@@ -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], category="actions")
|
||||
self.tensorboard_log(self.actions._member_names_[action])
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
|
@@ -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], category="actions")
|
||||
self.tensorboard_log(self.actions._member_names_[action])
|
||||
|
||||
trade_type = None
|
||||
if self.is_tradesignal(action):
|
||||
|
@@ -137,8 +137,7 @@ class BaseEnvironment(gym.Env):
|
||||
self.np_random, seed = seeding.np_random(seed)
|
||||
return [seed]
|
||||
|
||||
def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None,
|
||||
inc: Optional[bool] = None, category: str = "custom"):
|
||||
def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True):
|
||||
"""
|
||||
Function builds the tensorboard_metrics dictionary
|
||||
to be parsed by the TensorboardCallback. This
|
||||
@@ -150,24 +149,17 @@ class BaseEnvironment(gym.Env):
|
||||
|
||||
def calculate_reward(self, action: int) -> float:
|
||||
if not self._is_valid(action):
|
||||
self.tensorboard_log("invalid")
|
||||
self.tensorboard_log("is_valid")
|
||||
return -2
|
||||
|
||||
:param metric: metric to be tracked and incremented
|
||||
:param value: `metric` value
|
||||
:param inc: (deprecated) sets whether the `value` is incremented or not
|
||||
:param category: `metric` category
|
||||
:param value: value to increment `metric` by
|
||||
:param inc: sets whether the `value` is incremented or not
|
||||
"""
|
||||
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
|
||||
if not inc or metric not in self.tensorboard_metrics:
|
||||
self.tensorboard_metrics[metric] = value
|
||||
else:
|
||||
self.tensorboard_metrics[category][metric] += value
|
||||
self.tensorboard_metrics[metric] += value
|
||||
|
||||
def reset_tensorboard_log(self):
|
||||
self.tensorboard_metrics = {}
|
||||
|
@@ -114,7 +114,6 @@ 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
|
||||
@@ -149,8 +148,12 @@ 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))
|
||||
@@ -235,9 +238,6 @@ 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,6 +285,7 @@ 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',
|
||||
@@ -317,24 +318,8 @@ 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*
|
||||
|
@@ -13,7 +13,7 @@ class TensorboardCallback(BaseCallback):
|
||||
episodic summary reports.
|
||||
"""
|
||||
def __init__(self, verbose=1, actions: Type[Enum] = BaseActions):
|
||||
super().__init__(verbose)
|
||||
super(TensorboardCallback, self).__init__(verbose)
|
||||
self.model: Any = None
|
||||
self.logger = None # type: Any
|
||||
self.training_env: BaseEnvironment = None # type: ignore
|
||||
@@ -46,12 +46,14 @@ class TensorboardCallback(BaseCallback):
|
||||
local_info = self.locals["infos"][0]
|
||||
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
|
||||
|
||||
for metric in local_info:
|
||||
if metric not in ["episode", "terminal_observation"]:
|
||||
self.logger.record(f"info/{metric}", local_info[metric])
|
||||
for info in local_info:
|
||||
if info not in ["episode", "terminal_observation"]:
|
||||
self.logger.record(f"_info/{info}", local_info[info])
|
||||
|
||||
for category in tensorboard_metrics:
|
||||
for metric in tensorboard_metrics[category]:
|
||||
self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][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])
|
||||
|
||||
return True
|
||||
|
@@ -74,8 +74,8 @@ class FreqaiDataDrawer:
|
||||
self.historic_predictions: Dict[str, DataFrame] = {}
|
||||
self.full_path = full_path
|
||||
self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl")
|
||||
self.historic_predictions_folder = Path(self.full_path / "historic_predictions")
|
||||
self.historic_predictions_bkp_folder = Path(self.full_path / "historic_predictions_backup")
|
||||
self.historic_predictions_bkp_path = Path(
|
||||
self.full_path / "historic_predictions.backup.pkl")
|
||||
self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
|
||||
self.global_metadata_path = Path(self.full_path / "global_metadata.json")
|
||||
self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
|
||||
@@ -126,7 +126,7 @@ class FreqaiDataDrawer:
|
||||
"""
|
||||
exists = self.global_metadata_path.is_file()
|
||||
if exists:
|
||||
with self.global_metadata_path.open("r") as fp:
|
||||
with open(self.global_metadata_path, "r") as fp:
|
||||
metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
return metatada_dict
|
||||
return {}
|
||||
@@ -139,7 +139,7 @@ class FreqaiDataDrawer:
|
||||
"""
|
||||
exists = self.pair_dictionary_path.is_file()
|
||||
if exists:
|
||||
with self.pair_dictionary_path.open("r") as fp:
|
||||
with open(self.pair_dictionary_path, "r") as fp:
|
||||
self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
else:
|
||||
logger.info("Could not find existing datadrawer, starting from scratch")
|
||||
@@ -152,7 +152,7 @@ class FreqaiDataDrawer:
|
||||
if self.freqai_info.get('write_metrics_to_disk', False):
|
||||
exists = self.metric_tracker_path.is_file()
|
||||
if exists:
|
||||
with self.metric_tracker_path.open("r") as fp:
|
||||
with open(self.metric_tracker_path, "r") as fp:
|
||||
self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
logger.info("Loading existing metric tracker from disk.")
|
||||
else:
|
||||
@@ -163,12 +163,11 @@ class FreqaiDataDrawer:
|
||||
Locate and load a previously saved historic predictions.
|
||||
:return: bool - whether or not the drawer was located
|
||||
"""
|
||||
exists = self.historic_predictions_folder.exists()
|
||||
convert = self.historic_predictions_path.is_file()
|
||||
|
||||
exists = self.historic_predictions_path.is_file()
|
||||
if exists:
|
||||
try:
|
||||
self.load_historic_predictions_from_folder()
|
||||
with open(self.historic_predictions_path, "rb") as fp:
|
||||
self.historic_predictions = cloudpickle.load(fp)
|
||||
logger.info(
|
||||
f"Found existing historic predictions at {self.full_path}, but beware "
|
||||
"that statistics may be inaccurate if the bot has been offline for "
|
||||
@@ -176,70 +175,41 @@ class FreqaiDataDrawer:
|
||||
)
|
||||
except EOFError:
|
||||
logger.warning(
|
||||
'Historical prediction files were corrupted. Trying to load backup files.')
|
||||
self.load_historic_predictions_from_folder()
|
||||
logger.warning('FreqAI successfully loaded the backup '
|
||||
'historical predictions files.')
|
||||
|
||||
elif not exists and convert:
|
||||
logger.info("Converting your historic predictions pkl to parquet"
|
||||
"to improve performance.")
|
||||
with Path.open(self.historic_predictions_path, "rb") as fp:
|
||||
'Historical prediction file was corrupted. Trying to load backup file.')
|
||||
with open(self.historic_predictions_bkp_path, "rb") as fp:
|
||||
self.historic_predictions = cloudpickle.load(fp)
|
||||
self.save_historic_predictions_to_disk()
|
||||
exists = True
|
||||
logger.warning('FreqAI successfully loaded the backup historical predictions file.')
|
||||
|
||||
else:
|
||||
logger.warning(
|
||||
f"Follower could not find historic predictions at {self.full_path} "
|
||||
"sending null values back to strategy"
|
||||
)
|
||||
logger.info("Could not find existing historic_predictions, starting from scratch")
|
||||
|
||||
return exists
|
||||
|
||||
def load_historic_predictions_from_folder(self):
|
||||
"""
|
||||
Try to build the historic_predictions dictionary from parquet
|
||||
files in the historic_predictions_folder
|
||||
"""
|
||||
for file_path in self.historic_predictions_folder.glob("*.parquet"):
|
||||
key = file_path.stem
|
||||
key.replace("_", "/")
|
||||
self.historic_predictions[key] = pd.read_parquet(file_path)
|
||||
|
||||
return
|
||||
|
||||
def save_historic_predictions_to_disk(self):
|
||||
"""
|
||||
Save historic predictions pickle to disk
|
||||
"""
|
||||
|
||||
self.historic_predictions_folder.mkdir(parents=True, exist_ok=True)
|
||||
for key, value in self.historic_predictions.items():
|
||||
key = key.replace("/", "_")
|
||||
# pytest.set_trace()
|
||||
filename = Path(self.historic_predictions_folder / f"{key}.parquet")
|
||||
value.to_parquet(filename)
|
||||
with open(self.historic_predictions_path, "wb") as fp:
|
||||
cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL)
|
||||
|
||||
# create a backup
|
||||
shutil.copytree(self.historic_predictions_folder,
|
||||
self.historic_predictions_bkp_folder, dirs_exist_ok=True)
|
||||
shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path)
|
||||
|
||||
def save_metric_tracker_to_disk(self):
|
||||
"""
|
||||
Save metric tracker of all pair metrics collected.
|
||||
"""
|
||||
with self.save_lock:
|
||||
with self.metric_tracker_path.open('w') as fp:
|
||||
with open(self.metric_tracker_path, 'w') as fp:
|
||||
rapidjson.dump(self.metric_tracker, fp, default=self.np_encoder,
|
||||
number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
def save_drawer_to_disk(self) -> None:
|
||||
def save_drawer_to_disk(self):
|
||||
"""
|
||||
Save data drawer full of all pair model metadata in present model folder.
|
||||
"""
|
||||
with self.save_lock:
|
||||
with self.pair_dictionary_path.open('w') as fp:
|
||||
with open(self.pair_dictionary_path, 'w') as fp:
|
||||
rapidjson.dump(self.pair_dict, fp, default=self.np_encoder,
|
||||
number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
@@ -248,7 +218,7 @@ class FreqaiDataDrawer:
|
||||
Save global metadata json to disk
|
||||
"""
|
||||
with self.save_lock:
|
||||
with self.global_metadata_path.open('w') as fp:
|
||||
with open(self.global_metadata_path, 'w') as fp:
|
||||
rapidjson.dump(metadata, fp, default=self.np_encoder,
|
||||
number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
@@ -454,7 +424,7 @@ class FreqaiDataDrawer:
|
||||
dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
|
||||
dk.data["label_list"] = dk.label_list
|
||||
|
||||
with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp:
|
||||
with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp:
|
||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
return
|
||||
@@ -487,7 +457,7 @@ class FreqaiDataDrawer:
|
||||
dk.data["training_features_list"] = dk.training_features_list
|
||||
dk.data["label_list"] = dk.label_list
|
||||
# store the metadata
|
||||
with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp:
|
||||
with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp:
|
||||
rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
# save the train data to file so we can check preds for area of applicability later
|
||||
@@ -501,7 +471,7 @@ class FreqaiDataDrawer:
|
||||
|
||||
if self.freqai_info["feature_parameters"].get("principal_component_analysis"):
|
||||
cloudpickle.dump(
|
||||
dk.pca, (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("wb")
|
||||
dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb")
|
||||
)
|
||||
|
||||
self.model_dictionary[coin] = model
|
||||
@@ -521,7 +491,7 @@ class FreqaiDataDrawer:
|
||||
Load only metadata into datakitchen to increase performance during
|
||||
presaved backtesting (prediction file loading).
|
||||
"""
|
||||
with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp:
|
||||
with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp:
|
||||
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
dk.training_features_list = dk.data["training_features_list"]
|
||||
dk.label_list = dk.data["label_list"]
|
||||
@@ -544,7 +514,7 @@ class FreqaiDataDrawer:
|
||||
dk.data = self.meta_data_dictionary[coin]["meta_data"]
|
||||
dk.data_dictionary["train_features"] = self.meta_data_dictionary[coin]["train_df"]
|
||||
else:
|
||||
with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp:
|
||||
with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp:
|
||||
dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
dk.data_dictionary["train_features"] = pd.read_pickle(
|
||||
@@ -582,7 +552,7 @@ class FreqaiDataDrawer:
|
||||
|
||||
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
||||
dk.pca = cloudpickle.load(
|
||||
(dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("rb")
|
||||
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
||||
)
|
||||
|
||||
return model
|
||||
@@ -600,12 +570,12 @@ class FreqaiDataDrawer:
|
||||
|
||||
for pair in dk.all_pairs:
|
||||
for tf in feat_params.get("include_timeframes"):
|
||||
hist_df = history_data[pair][tf]
|
||||
|
||||
# check if newest candle is already appended
|
||||
df_dp = strategy.dp.get_pair_dataframe(pair, tf)
|
||||
if len(df_dp.index) == 0:
|
||||
continue
|
||||
if str(hist_df.iloc[-1]["date"]) == str(
|
||||
if str(history_data[pair][tf].iloc[-1]["date"]) == str(
|
||||
df_dp.iloc[-1:]["date"].iloc[-1]
|
||||
):
|
||||
continue
|
||||
@@ -613,30 +583,21 @@ class FreqaiDataDrawer:
|
||||
try:
|
||||
index = (
|
||||
df_dp.loc[
|
||||
df_dp["date"] == hist_df.iloc[-1]["date"]
|
||||
df_dp["date"] == history_data[pair][tf].iloc[-1]["date"]
|
||||
].index[0]
|
||||
+ 1
|
||||
)
|
||||
except IndexError:
|
||||
if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]:
|
||||
raise OperationalException("In memory historical data is older than "
|
||||
f"oldest DataProvider candle for {pair} on "
|
||||
f"timeframe {tf}")
|
||||
else:
|
||||
index = -1
|
||||
logger.warning(
|
||||
f"No common dates in historical data and dataprovider for {pair}. "
|
||||
f"Appending latest dataprovider candle to historical data "
|
||||
"but please be aware that there is likely a gap in the historical "
|
||||
"data. \n"
|
||||
f"Historical data ends at {hist_df.iloc[-1]['date']} "
|
||||
f"while dataprovider starts at {df_dp['date'].iloc[0]} and"
|
||||
f"ends at {df_dp['date'].iloc[0]}."
|
||||
f"Unable to update pair history for {pair}. "
|
||||
"If this does not resolve itself after 1 additional candle, "
|
||||
"please report the error to #freqai discord channel"
|
||||
)
|
||||
return
|
||||
|
||||
history_data[pair][tf] = pd.concat(
|
||||
[
|
||||
hist_df,
|
||||
history_data[pair][tf],
|
||||
df_dp.iloc[index:],
|
||||
],
|
||||
ignore_index=True,
|
||||
@@ -705,7 +666,7 @@ class FreqaiDataDrawer:
|
||||
Returns timerange information based on historic predictions file
|
||||
:return: timerange calculated from saved live data
|
||||
"""
|
||||
if not self.historic_predictions_folder.exists():
|
||||
if not self.historic_predictions_path.is_file():
|
||||
raise OperationalException(
|
||||
'Historic predictions not found. Historic predictions data is required '
|
||||
'to run backtest with the freqai-backtest-live-models option '
|
||||
|
@@ -251,7 +251,7 @@ class FreqaiDataKitchen:
|
||||
(drop_index == 0) & (drop_index_labels == 0)
|
||||
]
|
||||
logger.info(
|
||||
f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points"
|
||||
f"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"{self.pair}: SVM tossed {len(y_pred) - kept_points.sum()}"
|
||||
f"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"{self.pair}: DI tossed {len(do_predict) - do_predict.sum()} predictions for "
|
||||
f"DI tossed {len(do_predict) - do_predict.sum()} predictions for "
|
||||
"being too far from training data."
|
||||
)
|
||||
|
||||
@@ -1315,21 +1315,11 @@ class FreqaiDataKitchen:
|
||||
dataframe: DataFrame = dataframe containing populated indicators
|
||||
"""
|
||||
|
||||
# check if the user is using the deprecated populate_any_indicators function
|
||||
# this is a hack to check if the user is using the populate_any_indicators function
|
||||
new_version = inspect.getsource(strategy.populate_any_indicators) == (
|
||||
inspect.getsource(IStrategy.populate_any_indicators))
|
||||
|
||||
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/"
|
||||
)
|
||||
|
||||
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", [])
|
||||
@@ -1373,6 +1363,85 @@ class FreqaiDataKitchen:
|
||||
|
||||
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", [])
|
||||
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
|
||||
)
|
||||
|
||||
# ensure corr pairs are always last
|
||||
for corr_pair in 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]
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def fit_labels(self) -> None:
|
||||
"""
|
||||
Fit the labels with a gaussian distribution
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
@@ -104,7 +105,8 @@ class IFreqaiModel(ABC):
|
||||
self.data_provider: Optional[DataProvider] = None
|
||||
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
|
||||
self.can_short = True # overridden in start() with strategy.can_short
|
||||
self.model: Any = None
|
||||
|
||||
self.warned_deprecated_populate_any_indicators = False
|
||||
|
||||
record_params(config, self.full_path)
|
||||
|
||||
@@ -136,6 +138,9 @@ 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"])
|
||||
@@ -339,14 +344,13 @@ class IFreqaiModel(ABC):
|
||||
except Exception as msg:
|
||||
logger.warning(
|
||||
f"Training {pair} raised exception {msg.__class__.__name__}. "
|
||||
f"Message: {msg}, skipping.", exc_info=True)
|
||||
self.model = None
|
||||
f"Message: {msg}, skipping.")
|
||||
|
||||
self.dd.pair_dict[pair]["trained_timestamp"] = int(
|
||||
tr_train.stopts)
|
||||
if self.plot_features and self.model is not None:
|
||||
if self.plot_features:
|
||||
plot_feature_importance(self.model, pair, dk, self.plot_features)
|
||||
if self.save_backtest_models and self.model is not None:
|
||||
if self.save_backtest_models:
|
||||
logger.info('Saving backtest model to disk.')
|
||||
self.dd.save_data(self.model, pair, dk)
|
||||
else:
|
||||
@@ -487,7 +491,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 "
|
||||
"feature_engineering_* functions"
|
||||
"populate_any_indicator() functions"
|
||||
)
|
||||
|
||||
def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None:
|
||||
@@ -599,7 +603,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 populating indicators
|
||||
for populate_any_indicators
|
||||
(larger than new_trained_timerange so that
|
||||
new_trained_timerange does not contain any NaNs)
|
||||
"""
|
||||
@@ -805,7 +809,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 `feature_engineering_*` functions.")
|
||||
"in `populate_any_indicators()`.")
|
||||
self.get_corr_dataframes = not bool(self.corr_dataframes)
|
||||
elif self.corr_dataframes:
|
||||
dataframe = dk.attach_corr_pair_columns(
|
||||
@@ -932,6 +936,26 @@ 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.
|
||||
|
||||
|
@@ -100,7 +100,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
|
||||
"""
|
||||
# first, penalize if the action is not valid
|
||||
if not self._is_valid(action):
|
||||
self.tensorboard_log("invalid", category="actions")
|
||||
self.tensorboard_log("is_valid")
|
||||
return -2
|
||||
|
||||
pnl = self.get_unrealized_profit()
|
||||
|
@@ -34,11 +34,6 @@ class ReinforcementLearner_multiproc(ReinforcementLearner):
|
||||
train_df = data_dictionary["train_features"]
|
||||
test_df = data_dictionary["test_features"]
|
||||
|
||||
if self.train_env:
|
||||
self.train_env.close()
|
||||
if self.eval_env:
|
||||
self.eval_env.close()
|
||||
|
||||
env_info = self.pack_env_dict(dk.pair)
|
||||
|
||||
env_id = "train_env"
|
||||
|
@@ -211,7 +211,7 @@ def record_params(config: Dict[str, Any], full_path: Path) -> None:
|
||||
"pairs": config.get('exchange', {}).get('pair_whitelist')
|
||||
}
|
||||
|
||||
with params_record_path.open("w") as handle:
|
||||
with open(params_record_path, "w") as handle:
|
||||
rapidjson.dump(
|
||||
run_params,
|
||||
handle,
|
||||
|
@@ -127,19 +127,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
for minutes in [0, 15, 30, 45]:
|
||||
t = str(time(time_slot, minutes, 2))
|
||||
self._schedule.every().day.at(t).do(update)
|
||||
self.last_process: Optional[datetime] = None
|
||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
self.strategy.ft_bot_start()
|
||||
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
||||
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
||||
|
||||
def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None:
|
||||
def notify_status(self, msg: str) -> 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': msg_type,
|
||||
'type': RPCMessageType.STATUS,
|
||||
'status': msg
|
||||
})
|
||||
|
||||
@@ -586,7 +586,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
|
||||
current_entry_rate,
|
||||
0.0)
|
||||
self.strategy.stoploss)
|
||||
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, supress_error=True)(
|
||||
default_retval=None)(
|
||||
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 min_exit_stake and remaining < min_exit_stake:
|
||||
if remaining < min_exit_stake:
|
||||
logger.info(f"Remaining amount of {remaining} would be smaller "
|
||||
f"than the minimum of {min_exit_stake}.")
|
||||
return
|
||||
@@ -700,8 +700,7 @@ 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_,
|
||||
pos_adjust)
|
||||
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_)
|
||||
|
||||
if not stake_amount:
|
||||
return False
|
||||
@@ -810,9 +809,6 @@ 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
|
||||
@@ -822,7 +818,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
trade.orders.append(order_obj)
|
||||
trade.recalc_trade_from_orders()
|
||||
Trade.session.add(trade)
|
||||
Trade.query.session.add(trade)
|
||||
Trade.commit()
|
||||
|
||||
# Updating wallets
|
||||
@@ -845,7 +841,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
|
||||
# First cancelling stoploss on exchange ...
|
||||
if trade.stoploss_order_id:
|
||||
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||
try:
|
||||
logger.info(f"Canceling stoploss on exchange for {trade}")
|
||||
co = self.exchange.cancel_stoploss_order_with_result(
|
||||
@@ -864,12 +860,7 @@ 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
|
||||
@@ -915,9 +906,7 @@ 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 if not pos_adjust else 0.0,
|
||||
leverage)
|
||||
pair, enter_limit_requested, self.strategy.stoploss, leverage)
|
||||
max_stake_amount = self.exchange.get_max_pair_stake_amount(
|
||||
pair, enter_limit_requested, leverage)
|
||||
|
||||
@@ -1024,16 +1013,12 @@ 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
|
||||
|
||||
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
|
||||
@@ -1137,7 +1122,8 @@ 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.emergency_exit(trade, stop_price)
|
||||
self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.EMERGENCY_EXIT))
|
||||
|
||||
except ExchangeError:
|
||||
trade.stoploss_order_id = None
|
||||
@@ -1289,22 +1275,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, reason)
|
||||
else:
|
||||
canceled = self.handle_cancel_exit(trade, order, reason)
|
||||
canceled = self.handle_cancel_exit(
|
||||
trade, order, reason)
|
||||
canceled_count = trade.get_exit_order_count()
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
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.')
|
||||
self.emergency_exit(trade, order['price'])
|
||||
|
||||
def emergency_exit(self, trade: Trade, price: float) -> None:
|
||||
try:
|
||||
self.execute_trade_exit(
|
||||
trade, price,
|
||||
trade, order['price'],
|
||||
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
|
||||
except DependencyException as exception:
|
||||
logger.warning(
|
||||
f'Unable to emergency exit trade {trade.pair}: {exception}')
|
||||
f'Unable to emergency sell trade {trade.pair}: {exception}')
|
||||
|
||||
def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
|
||||
"""
|
||||
@@ -1331,7 +1315,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.safe_price, entry_tag=trade.enter_tag,
|
||||
current_order_rate=order_obj.price, entry_tag=trade.enter_tag,
|
||||
side=trade.entry_side)
|
||||
|
||||
replacing = True
|
||||
@@ -1347,8 +1331,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
# place new order only if new price is supplied
|
||||
self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
|
||||
stake_amount=(order_obj.remaining * order_obj.price / trade.leverage),
|
||||
price=adjusted_entry_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
@@ -1362,8 +1345,6 @@ 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):
|
||||
@@ -1388,9 +1369,6 @@ 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:
|
||||
@@ -1477,32 +1455,34 @@ class FreqtradeBot(LoggingMixin):
|
||||
return False
|
||||
|
||||
try:
|
||||
order = self.exchange.cancel_order_with_result(order['id'], trade.pair,
|
||||
co = self.exchange.cancel_order_with_result(trade.open_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 order.get('status') in ('canceled', 'cancelled'):
|
||||
if co.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']
|
||||
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}.')
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
trade.open_order_id = None
|
||||
trade.close_rate = None
|
||||
trade.close_rate_requested = None
|
||||
|
||||
self._notify_exit_cancel(
|
||||
trade,
|
||||
@@ -1660,7 +1640,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.safe_close_rate
|
||||
order_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
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
|
||||
@@ -1715,7 +1695,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
raise DependencyException(
|
||||
f"Order_obj not found for {order_id}. This should not have happened.")
|
||||
|
||||
profit_rate: float = trade.safe_close_rate
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
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)
|
||||
@@ -1758,8 +1738,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
#
|
||||
|
||||
def update_trade_state(
|
||||
self, trade: Trade, order_id: Optional[str],
|
||||
action_order: Optional[Dict[str, Any]] = None,
|
||||
self, trade: Trade, order_id: 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
|
||||
|
@@ -6,7 +6,8 @@ import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union
|
||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, Union
|
||||
from typing.io import IO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import orjson
|
||||
@@ -80,7 +81,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool =
|
||||
else:
|
||||
if log:
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
with filename.open('w') as fp:
|
||||
with open(filename, 'w') as fp:
|
||||
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
logger.debug(f'done json to "{filename}"')
|
||||
@@ -97,12 +98,12 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None:
|
||||
|
||||
if log:
|
||||
logger.info(f'dumping joblib to "{filename}"')
|
||||
with filename.open('wb') as fp:
|
||||
with open(filename, 'wb') as fp:
|
||||
joblib.dump(data, fp)
|
||||
logger.debug(f'done joblib dump to "{filename}"')
|
||||
|
||||
|
||||
def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any:
|
||||
def json_load(datafile: IO) -> Any:
|
||||
"""
|
||||
load data with rapidjson
|
||||
Use this to have a consistent experience,
|
||||
@@ -111,7 +112,7 @@ def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any:
|
||||
return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE)
|
||||
|
||||
|
||||
def file_load_json(file: Path):
|
||||
def file_load_json(file):
|
||||
|
||||
if file.suffix != ".gz":
|
||||
gzipfile = file.with_suffix(file.suffix + '.gz')
|
||||
@@ -124,7 +125,7 @@ def file_load_json(file: Path):
|
||||
pairdata = json_load(datafile)
|
||||
elif file.is_file():
|
||||
logger.debug(f"Loading historical data from file {file}")
|
||||
with file.open() as datafile:
|
||||
with open(file) as datafile:
|
||||
pairdata = json_load(datafile)
|
||||
else:
|
||||
return None
|
||||
|
@@ -29,7 +29,7 @@ def get_strategy_run_id(strategy) -> str:
|
||||
# Include _ft_params_from_file - so changing parameter files cause cache eviction
|
||||
digest.update(rapidjson.dumps(
|
||||
strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8'))
|
||||
with Path(strategy.__file__).open('rb') as fp:
|
||||
with open(strategy.__file__, 'rb') as fp:
|
||||
digest.update(fp.read())
|
||||
return digest.hexdigest().lower()
|
||||
|
||||
|
@@ -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 feature_engineering_* functions.")
|
||||
"to have identical populate_any_indicators.")
|
||||
for strat in list(self.config['strategy_list']):
|
||||
stratconf = deepcopy(self.config)
|
||||
stratconf['strategy'] = strat
|
||||
@@ -440,8 +440,11 @@ 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 or 0.0) / leverage))
|
||||
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]
|
||||
|
||||
# Limit lower-end to candle low to avoid exits below the low.
|
||||
# This still remains "worst case" - but "worst realistic case".
|
||||
@@ -469,7 +472,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 or 0.0) - side_1 * 1)
|
||||
close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1)
|
||||
if is_short:
|
||||
is_new_roi = row[OPEN_IDX] < close_rate
|
||||
else:
|
||||
@@ -522,7 +525,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, supress_error=True)(
|
||||
default_retval=None)(
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
current_time=current_date, current_rate=current_rate,
|
||||
current_profit=current_profit, min_stake=min_stake,
|
||||
@@ -560,7 +563,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.ft_price, row):
|
||||
if self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
trade.recalc_trade_from_orders()
|
||||
self.wallets.update()
|
||||
@@ -661,7 +664,6 @@ class Backtesting:
|
||||
side=trade.exit_side,
|
||||
order_type=order_type,
|
||||
status="open",
|
||||
ft_price=close_rate,
|
||||
price=close_rate,
|
||||
average=close_rate,
|
||||
amount=amount,
|
||||
@@ -745,7 +747,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 if not pos_adjust else 0.0, leverage=leverage) or 0
|
||||
pair, propose_rate, -0.05, 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()
|
||||
@@ -885,7 +887,6 @@ 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,
|
||||
@@ -894,7 +895,7 @@ class Backtesting:
|
||||
cost=stake_amount + trade.fee_open,
|
||||
)
|
||||
trade.orders.append(order)
|
||||
if pos_adjust and self._get_order_filled(order.ft_price, row):
|
||||
if pos_adjust and self._get_order_filled(order.price, row):
|
||||
order.close_bt_order(current_time, trade)
|
||||
else:
|
||||
trade.open_order_id = str(self.order_id_counter)
|
||||
@@ -1007,15 +1008,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.ft_price)(
|
||||
default_retval=order.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.ft_price,
|
||||
proposed_rate=row[OPEN_IDX], current_order_rate=order.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.ft_price:
|
||||
if requested_rate == order.price:
|
||||
# assumption: there can't be multiple open entry orders at any given time
|
||||
return False
|
||||
else:
|
||||
@@ -1027,8 +1028,7 @@ class Backtesting:
|
||||
if requested_rate:
|
||||
self._enter_trade(pair=trade.pair, row=row, trade=trade,
|
||||
requested_rate=requested_rate,
|
||||
requested_stake=(
|
||||
order.safe_remaining * order.ft_price / trade.leverage),
|
||||
requested_stake=(order.remaining * order.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.ft_price, row):
|
||||
if order and self._get_order_filled(order.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.ft_price, row):
|
||||
if order and self._get_order_filled(order.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.ft_price, show_msg=False)
|
||||
trade.close(order.price, show_msg=False)
|
||||
|
||||
# logger.debug(f"{pair} - Backtesting exit {trade}")
|
||||
LocalTrade.close_bt_trade(trade)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import io
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
@@ -463,8 +464,8 @@ class HyperoptTools():
|
||||
return
|
||||
|
||||
try:
|
||||
Path(csv_file).open('w+').close()
|
||||
except OSError:
|
||||
io.open(csv_file, 'w+').close()
|
||||
except IOError:
|
||||
logger.error(f"Failed to create CSV file: {csv_file}")
|
||||
return
|
||||
|
||||
|
@@ -1,9 +1,7 @@
|
||||
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, scoped_session
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
|
||||
SessionType = scoped_session[Session]
|
||||
|
||||
|
||||
class ModelBase(DeclarativeBase):
|
||||
pass
|
||||
_DECL_BASE: Any = declarative_base()
|
||||
|
@@ -2,9 +2,6 @@
|
||||
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
|
||||
@@ -12,7 +9,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence.base import ModelBase
|
||||
from freqtrade.persistence.base import _DECL_BASE
|
||||
from freqtrade.persistence.migrations import check_migrate
|
||||
from freqtrade.persistence.pairlock import PairLock
|
||||
from freqtrade.persistence.trade_model import Order, Trade
|
||||
@@ -21,22 +18,6 @@ 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'
|
||||
|
||||
|
||||
@@ -48,7 +29,7 @@ def init_db(db_url: str) -> None:
|
||||
:param db_url: Database to use
|
||||
:return: None
|
||||
"""
|
||||
kwargs: Dict[str, Any] = {}
|
||||
kwargs = {}
|
||||
|
||||
if db_url == 'sqlite:///':
|
||||
raise OperationalException(
|
||||
@@ -71,12 +52,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.
|
||||
# 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
|
||||
# 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()
|
||||
|
||||
previous_tables = inspect(engine).get_table_names()
|
||||
ModelBase.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=ModelBase, previous_tables=previous_tables)
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
|
||||
|
@@ -1,34 +1,33 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, ClassVar, Dict, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import ScalarResult, String, or_, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer, String, or_
|
||||
from sqlalchemy.orm import Query
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
from freqtrade.persistence.base import ModelBase, SessionType
|
||||
from freqtrade.persistence.base import _DECL_BASE
|
||||
|
||||
|
||||
class PairLock(ModelBase):
|
||||
class PairLock(_DECL_BASE):
|
||||
"""
|
||||
Pair Locks database model.
|
||||
"""
|
||||
__tablename__ = 'pairlocks'
|
||||
session: ClassVar[SessionType]
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True)
|
||||
pair = Column(String(25), nullable=False, index=True)
|
||||
# lock direction - long, short or * (for both)
|
||||
side: Mapped[str] = mapped_column(String(25), nullable=False, default="*")
|
||||
reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
side = Column(String(25), nullable=False, default="*")
|
||||
reason = Column(String(255), nullable=True)
|
||||
# Time the pair was locked (start time)
|
||||
lock_time: Mapped[datetime] = mapped_column(nullable=False)
|
||||
lock_time = Column(DateTime(), nullable=False)
|
||||
# Time until the pair is locked (end time)
|
||||
lock_end_time: Mapped[datetime] = mapped_column(nullable=False, index=True)
|
||||
lock_end_time = Column(DateTime(), nullable=False, index=True)
|
||||
|
||||
active: Mapped[bool] = mapped_column(nullable=False, default=True, index=True)
|
||||
active = Column(Boolean, nullable=False, default=True, index=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
def __repr__(self):
|
||||
lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT)
|
||||
lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT)
|
||||
return (
|
||||
@@ -36,8 +35,7 @@ class PairLock(ModelBase):
|
||||
f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})')
|
||||
|
||||
@staticmethod
|
||||
def query_pair_locks(
|
||||
pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']:
|
||||
def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> Query:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
@@ -53,11 +51,9 @@ class PairLock(ModelBase):
|
||||
else:
|
||||
filters.append(PairLock.side == '*')
|
||||
|
||||
return PairLock.session.scalars(select(PairLock).filter(*filters))
|
||||
|
||||
@staticmethod
|
||||
def get_all_locks() -> ScalarResult['PairLock']:
|
||||
return PairLock.session.scalars(select(PairLock))
|
||||
return PairLock.query.filter(
|
||||
*filters
|
||||
)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from typing import List, Optional
|
||||
|
||||
from freqtrade.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence.models import PairLock
|
||||
@@ -53,15 +51,15 @@ class PairLocks():
|
||||
active=True
|
||||
)
|
||||
if PairLocks.use_db:
|
||||
PairLock.session.add(lock)
|
||||
PairLock.session.commit()
|
||||
PairLock.query.session.add(lock)
|
||||
PairLock.query.session.commit()
|
||||
else:
|
||||
PairLocks.locks.append(lock)
|
||||
return lock
|
||||
|
||||
@staticmethod
|
||||
def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None,
|
||||
side: str = '*') -> Sequence[PairLock]:
|
||||
def get_pair_locks(
|
||||
pair: Optional[str], now: Optional[datetime] = None, side: str = '*') -> List[PairLock]:
|
||||
"""
|
||||
Get all currently active locks for this pair
|
||||
:param pair: Pair to check for. Returns all current locks if pair is empty
|
||||
@@ -108,7 +106,7 @@ class PairLocks():
|
||||
for lock in locks:
|
||||
lock.active = False
|
||||
if PairLocks.use_db:
|
||||
PairLock.session.commit()
|
||||
PairLock.query.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def unlock_reason(reason: str, now: Optional[datetime] = None) -> None:
|
||||
@@ -128,15 +126,15 @@ class PairLocks():
|
||||
PairLock.active.is_(True),
|
||||
PairLock.reason == reason
|
||||
]
|
||||
locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all()
|
||||
locks = PairLock.query.filter(*filters)
|
||||
for lock in locks:
|
||||
logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.")
|
||||
lock.active = False
|
||||
PairLock.session.commit()
|
||||
PairLock.query.session.commit()
|
||||
else:
|
||||
# used in backtesting mode; don't show log messages for speed
|
||||
locksb = PairLocks.get_pair_locks(None)
|
||||
for lock in locksb:
|
||||
locks = PairLocks.get_pair_locks(None)
|
||||
for lock in locks:
|
||||
if lock.reason == reason:
|
||||
lock.active = False
|
||||
|
||||
@@ -167,11 +165,11 @@ class PairLocks():
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_all_locks() -> Sequence[PairLock]:
|
||||
def get_all_locks() -> List[PairLock]:
|
||||
"""
|
||||
Return all locks, also locks with expired end date
|
||||
"""
|
||||
if PairLocks.use_db:
|
||||
return PairLock.get_all_locks().all()
|
||||
return PairLock.query.all()
|
||||
else:
|
||||
return PairLocks.locks
|
||||
|
@@ -5,11 +5,11 @@ import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import isclose
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String,
|
||||
UniqueConstraint, desc, func, select)
|
||||
from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
||||
UniqueConstraint, desc, func)
|
||||
from sqlalchemy.orm import Query, lazyload, relationship
|
||||
|
||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, 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 ModelBase, SessionType
|
||||
from freqtrade.persistence.base import _DECL_BASE
|
||||
from freqtrade.util import FtPrecise
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Order(ModelBase):
|
||||
class Order(_DECL_BASE):
|
||||
"""
|
||||
Order database model
|
||||
Keeps a record of all orders placed on the exchange
|
||||
@@ -36,43 +36,41 @@ class Order(ModelBase):
|
||||
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: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True)
|
||||
id = Column(Integer, primary_key=True)
|
||||
ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
|
||||
|
||||
trade: Mapped[List["Trade"]] = relationship("Trade", back_populates="orders")
|
||||
trade = relationship("Trade", back_populates="orders")
|
||||
|
||||
# order_side can only be 'buy', 'sell' or 'stoploss'
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True)
|
||||
funding_fee = Column(Float(), nullable=True)
|
||||
|
||||
ft_fee_base = Column(Float(), nullable=True)
|
||||
|
||||
@property
|
||||
def order_date_utc(self) -> datetime:
|
||||
@@ -98,10 +96,6 @@ class Order(ModelBase):
|
||||
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 (
|
||||
@@ -119,9 +113,8 @@ class Order(ModelBase):
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
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})")
|
||||
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})')
|
||||
|
||||
def update_from_ccxt_object(self, order):
|
||||
"""
|
||||
@@ -158,7 +151,7 @@ class Order(ModelBase):
|
||||
self.order_update_date = datetime.now(timezone.utc)
|
||||
|
||||
def to_ccxt_object(self) -> Dict[str, Any]:
|
||||
order: Dict[str, Any] = {
|
||||
order = {
|
||||
'id': self.order_id,
|
||||
'symbol': self.ft_pair,
|
||||
'price': self.price,
|
||||
@@ -220,7 +213,7 @@ class Order(ModelBase):
|
||||
# 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 and self.price):
|
||||
if (self.ft_order_side == trade.entry_side):
|
||||
trade.open_rate = self.price
|
||||
trade.recalc_trade_from_orders()
|
||||
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
|
||||
@@ -262,12 +255,12 @@ class Order(ModelBase):
|
||||
return o
|
||||
|
||||
@staticmethod
|
||||
def get_open_orders() -> Sequence['Order']:
|
||||
def get_open_orders() -> List['Order']:
|
||||
"""
|
||||
Retrieve open orders from the database
|
||||
:return: List of open orders
|
||||
"""
|
||||
return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all()
|
||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||
|
||||
@staticmethod
|
||||
def order_by_id(order_id: str) -> Optional['Order']:
|
||||
@@ -275,7 +268,7 @@ class Order(ModelBase):
|
||||
Retrieve order based on order_id
|
||||
:return: Order or None
|
||||
"""
|
||||
return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first()
|
||||
return Order.query.filter(Order.order_id == order_id).first()
|
||||
|
||||
|
||||
class LocalTrade():
|
||||
@@ -300,15 +293,15 @@ class LocalTrade():
|
||||
|
||||
exchange: str = ''
|
||||
pair: str = ''
|
||||
base_currency: Optional[str] = ''
|
||||
stake_currency: Optional[str] = ''
|
||||
base_currency: str = ''
|
||||
stake_currency: str = ''
|
||||
is_open: bool = True
|
||||
fee_open: float = 0.0
|
||||
fee_open_cost: Optional[float] = None
|
||||
fee_open_currency: Optional[str] = ''
|
||||
fee_close: Optional[float] = 0.0
|
||||
fee_open_currency: str = ''
|
||||
fee_close: float = 0.0
|
||||
fee_close_cost: Optional[float] = None
|
||||
fee_close_currency: Optional[str] = ''
|
||||
fee_close_currency: str = ''
|
||||
open_rate: float = 0.0
|
||||
open_rate_requested: Optional[float] = None
|
||||
# open_trade_value - calculated via _calc_open_trade_value
|
||||
@@ -318,7 +311,7 @@ class LocalTrade():
|
||||
close_profit: Optional[float] = None
|
||||
close_profit_abs: Optional[float] = None
|
||||
stake_amount: float = 0.0
|
||||
max_stake_amount: Optional[float] = 0.0
|
||||
max_stake_amount: float = 0.0
|
||||
amount: float = 0.0
|
||||
amount_requested: Optional[float] = None
|
||||
open_date: datetime
|
||||
@@ -327,9 +320,9 @@ class LocalTrade():
|
||||
# absolute value of the stop loss
|
||||
stop_loss: float = 0.0
|
||||
# percentage value of the stop loss
|
||||
stop_loss_pct: Optional[float] = 0.0
|
||||
stop_loss_pct: float = 0.0
|
||||
# absolute value of the initial stop loss
|
||||
initial_stop_loss: Optional[float] = 0.0
|
||||
initial_stop_loss: float = 0.0
|
||||
# percentage value of the initial stop loss
|
||||
initial_stop_loss_pct: Optional[float] = None
|
||||
# stoploss order id which is on exchange
|
||||
@@ -337,12 +330,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: Optional[float] = None
|
||||
max_rate: float = 0.0
|
||||
# Lowest price reached
|
||||
min_rate: Optional[float] = None
|
||||
exit_reason: Optional[str] = ''
|
||||
exit_order_status: Optional[str] = ''
|
||||
strategy: Optional[str] = ''
|
||||
min_rate: float = 0.0
|
||||
exit_reason: str = ''
|
||||
exit_order_status: str = ''
|
||||
strategy: str = ''
|
||||
enter_tag: Optional[str] = None
|
||||
timeframe: Optional[int] = None
|
||||
|
||||
@@ -518,8 +511,6 @@ 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
|
||||
@@ -601,7 +592,7 @@ class LocalTrade():
|
||||
|
||||
self.stop_loss_pct = -1 * abs(percent)
|
||||
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: Optional[float],
|
||||
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
||||
initial: bool = False, refresh: bool = False) -> None:
|
||||
"""
|
||||
This adjusts the stop loss to it's most recently observed setting
|
||||
@@ -610,7 +601,7 @@ class LocalTrade():
|
||||
:param initial: Called to initiate stop_loss.
|
||||
Skips everything if self.stop_loss is already set.
|
||||
"""
|
||||
if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)):
|
||||
if 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
|
||||
@@ -649,7 +640,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 or 0.0):.8f}.")
|
||||
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
|
||||
|
||||
def update_trade(self, order: Order) -> None:
|
||||
"""
|
||||
@@ -801,10 +792,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: Optional[float]) -> FtPrecise:
|
||||
def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise:
|
||||
|
||||
close_trade = amount * FtPrecise(rate)
|
||||
fees = close_trade * FtPrecise(fee or 0.0)
|
||||
fees = close_trade * FtPrecise(fee)
|
||||
|
||||
if self.is_short:
|
||||
return close_trade + fees
|
||||
@@ -1068,14 +1059,10 @@ class LocalTrade():
|
||||
return len(self.select_filled_orders('sell'))
|
||||
|
||||
@property
|
||||
def sell_reason(self) -> Optional[str]:
|
||||
def sell_reason(self) -> 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,
|
||||
@@ -1087,11 +1074,6 @@ 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]
|
||||
"""
|
||||
|
||||
@@ -1142,7 +1124,7 @@ class LocalTrade():
|
||||
@staticmethod
|
||||
def get_open_trades() -> List[Any]:
|
||||
"""
|
||||
Retrieve open trades
|
||||
Query trades from persistence layer
|
||||
"""
|
||||
return Trade.get_trades_proxy(is_open=True)
|
||||
|
||||
@@ -1152,9 +1134,7 @@ class LocalTrade():
|
||||
get open trade count
|
||||
"""
|
||||
if Trade.use_db:
|
||||
return Trade.session.execute(
|
||||
select(func.count(Trade.id)).filter(Trade.is_open.is_(True))
|
||||
).scalar_one()
|
||||
return Trade.query.filter(Trade.is_open.is_(True)).count()
|
||||
else:
|
||||
return LocalTrade.bt_open_open_trade_count
|
||||
|
||||
@@ -1179,7 +1159,7 @@ class LocalTrade():
|
||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||
|
||||
|
||||
class Trade(ModelBase, LocalTrade):
|
||||
class Trade(_DECL_BASE, LocalTrade):
|
||||
"""
|
||||
Trade database model.
|
||||
Also handles updating and querying trades
|
||||
@@ -1187,97 +1167,79 @@ class Trade(ModelBase, LocalTrade):
|
||||
Note: Fields must be aligned with LocalTrade class
|
||||
"""
|
||||
__tablename__ = 'trades'
|
||||
session: ClassVar[SessionType]
|
||||
|
||||
use_db: bool = True
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True) # type: ignore
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
orders: Mapped[List[Order]] = relationship(
|
||||
"Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin",
|
||||
innerjoin=True) # type: ignore
|
||||
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan",
|
||||
lazy="selectin", innerjoin=True)
|
||||
|
||||
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
|
||||
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())
|
||||
# open_trade_value - calculated via _calc_open_trade_value
|
||||
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
|
||||
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))
|
||||
# absolute value of the stop loss
|
||||
stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore
|
||||
stop_loss = Column(Float(), nullable=True, default=0.0)
|
||||
# percentage value of the stop loss
|
||||
stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore
|
||||
stop_loss_pct = Column(Float(), nullable=True)
|
||||
# absolute value of the initial stop loss
|
||||
initial_stop_loss: Mapped[Optional[float]] = mapped_column(
|
||||
Float(), nullable=True, default=0.0) # type: ignore
|
||||
initial_stop_loss = Column(Float(), nullable=True, default=0.0)
|
||||
# percentage value of the initial stop loss
|
||||
initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(
|
||||
Float(), nullable=True) # type: ignore
|
||||
initial_stop_loss_pct = Column(Float(), nullable=True)
|
||||
# stoploss order id which is on exchange
|
||||
stoploss_order_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(255), nullable=True, index=True) # type: ignore
|
||||
stoploss_order_id = Column(String(255), nullable=True, index=True)
|
||||
# last update time of the stoploss order on exchange
|
||||
stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore
|
||||
stoploss_last_update = Column(DateTime(), nullable=True)
|
||||
# absolute value of the highest reached price
|
||||
max_rate: Mapped[Optional[float]] = mapped_column(
|
||||
Float(), nullable=True, default=0.0) # type: ignore
|
||||
max_rate = Column(Float(), nullable=True, default=0.0)
|
||||
# Lowest price reached
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
# Leverage trading properties
|
||||
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
|
||||
leverage = Column(Float(), nullable=True, default=1.0)
|
||||
is_short = Column(Boolean, nullable=False, default=False)
|
||||
liquidation_price = Column(Float(), nullable=True)
|
||||
|
||||
# Margin Trading Properties
|
||||
interest_rate: Mapped[float] = mapped_column(
|
||||
Float(), nullable=False, default=0.0) # type: ignore
|
||||
interest_rate = Column(Float(), nullable=False, default=0.0)
|
||||
|
||||
# Futures properties
|
||||
funding_fees: Mapped[Optional[float]] = mapped_column(
|
||||
Float(), nullable=True, default=None) # type: ignore
|
||||
funding_fees = Column(Float(), nullable=True, default=None)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -1287,18 +1249,18 @@ class Trade(ModelBase, LocalTrade):
|
||||
def delete(self) -> None:
|
||||
|
||||
for order in self.orders:
|
||||
Order.session.delete(order)
|
||||
Order.query.session.delete(order)
|
||||
|
||||
Trade.session.delete(self)
|
||||
Trade.query.session.delete(self)
|
||||
Trade.commit()
|
||||
|
||||
@staticmethod
|
||||
def commit():
|
||||
Trade.session.commit()
|
||||
Trade.query.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def rollback():
|
||||
Trade.session.rollback()
|
||||
Trade.query.session.rollback()
|
||||
|
||||
@staticmethod
|
||||
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
|
||||
@@ -1323,7 +1285,7 @@ class Trade(ModelBase, 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 cast(List[LocalTrade], Trade.get_trades(trade_filter).all())
|
||||
return Trade.get_trades(trade_filter).all()
|
||||
else:
|
||||
return LocalTrade.get_trades_proxy(
|
||||
pair=pair, is_open=is_open,
|
||||
@@ -1332,7 +1294,7 @@ class Trade(ModelBase, LocalTrade):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select:
|
||||
def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
|
||||
"""
|
||||
Helper function to query Trades using filters.
|
||||
NOTE: Not supported in Backtesting.
|
||||
@@ -1347,35 +1309,22 @@ class Trade(ModelBase, LocalTrade):
|
||||
if trade_filter is not None:
|
||||
if not isinstance(trade_filter, list):
|
||||
trade_filter = [trade_filter]
|
||||
this_query = select(Trade).filter(*trade_filter)
|
||||
this_query = Trade.query.filter(*trade_filter)
|
||||
else:
|
||||
this_query = select(Trade)
|
||||
this_query = Trade.query
|
||||
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 cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all())
|
||||
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
|
||||
|
||||
@staticmethod
|
||||
def get_open_trades_without_assigned_fees():
|
||||
@@ -1405,12 +1354,11 @@ class Trade(ModelBase, LocalTrade):
|
||||
Retrieves total realized profit
|
||||
"""
|
||||
if Trade.use_db:
|
||||
total_profit: float = Trade.session.execute(
|
||||
select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False))
|
||||
).scalar_one()
|
||||
total_profit = Trade.query.with_entities(
|
||||
func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar()
|
||||
else:
|
||||
total_profit = sum(t.close_profit_abs # type: ignore
|
||||
for t in LocalTrade.get_trades_proxy(is_open=False))
|
||||
total_profit = sum(
|
||||
t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False))
|
||||
return total_profit or 0
|
||||
|
||||
@staticmethod
|
||||
@@ -1420,9 +1368,8 @@ class Trade(ModelBase, LocalTrade):
|
||||
in stake currency
|
||||
"""
|
||||
if Trade.use_db:
|
||||
total_open_stake_amount = Trade.session.scalar(
|
||||
select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True))
|
||||
)
|
||||
total_open_stake_amount = Trade.query.with_entities(
|
||||
func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
|
||||
else:
|
||||
total_open_stake_amount = sum(
|
||||
t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True))
|
||||
@@ -1434,22 +1381,19 @@ class Trade(ModelBase, LocalTrade):
|
||||
Returns List of dicts containing all Trades, including profit and trade count
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
filters: List = [Trade.is_open.is_(False)]
|
||||
filters = [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.session.execute(
|
||||
select(
|
||||
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()
|
||||
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
return [
|
||||
{
|
||||
'pair': pair,
|
||||
@@ -1470,20 +1414,19 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
|
||||
filters: List = [Trade.is_open.is_(False)]
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
|
||||
enter_tag_perf = Trade.session.execute(
|
||||
select(
|
||||
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()
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.enter_tag) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1504,19 +1447,19 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
|
||||
filters: List = [Trade.is_open.is_(False)]
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
sell_tag_perf = Trade.session.execute(
|
||||
select(
|
||||
|
||||
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()
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.exit_reason) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -1537,21 +1480,21 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
|
||||
filters: List = [Trade.is_open.is_(False)]
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if (pair is not None):
|
||||
filters.append(Trade.pair == pair)
|
||||
mix_tag_perf = Trade.session.execute(
|
||||
select(
|
||||
|
||||
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()
|
||||
).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:
|
||||
@@ -1587,15 +1530,11 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
:returns: Tuple containing (pair, profit_sum)
|
||||
"""
|
||||
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()
|
||||
|
||||
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()
|
||||
return best_pair
|
||||
|
||||
@staticmethod
|
||||
@@ -1605,13 +1544,12 @@ class Trade(ModelBase, LocalTrade):
|
||||
NOTE: Not supported in Backtesting.
|
||||
:returns: Tuple containing (pair, profit_sum)
|
||||
"""
|
||||
trading_volume = Trade.session.execute(
|
||||
select(
|
||||
trading_volume = Order.query.with_entities(
|
||||
func.sum(Order.cost).label('volume')
|
||||
).filter(
|
||||
Order.order_filled_date >= start_date,
|
||||
Order.status == 'closed'
|
||||
)).scalar_one()
|
||||
).scalar()
|
||||
return trading_volume
|
||||
|
||||
@staticmethod
|
||||
|
@@ -157,7 +157,7 @@ class RemotePairList(IPairList):
|
||||
file_path = Path(filename)
|
||||
|
||||
if file_path.exists():
|
||||
with file_path.open() as json_file:
|
||||
with open(filename) as json_file:
|
||||
# Load the JSON data into a dictionary
|
||||
jsonparse = json.load(json_file)
|
||||
|
||||
|
@@ -5,7 +5,6 @@ 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
|
||||
|
||||
@@ -23,12 +22,6 @@ 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:
|
||||
"""
|
||||
|
@@ -228,33 +228,24 @@ class TradeSchema(BaseModel):
|
||||
fee_close: Optional[float]
|
||||
fee_close_cost: Optional[float]
|
||||
fee_close_currency: Optional[str]
|
||||
|
||||
open_date: str
|
||||
open_timestamp: int
|
||||
open_rate: float
|
||||
open_rate_requested: Optional[float]
|
||||
open_trade_value: float
|
||||
|
||||
close_date: Optional[str]
|
||||
close_timestamp: Optional[int]
|
||||
close_rate: Optional[float]
|
||||
close_rate_requested: Optional[float]
|
||||
|
||||
close_profit: Optional[float]
|
||||
close_profit_pct: Optional[float]
|
||||
close_profit_abs: Optional[float]
|
||||
|
||||
profit_ratio: Optional[float]
|
||||
profit_pct: Optional[float]
|
||||
profit_abs: Optional[float]
|
||||
profit_fiat: Optional[float]
|
||||
|
||||
realized_profit: float
|
||||
realized_profit_ratio: Optional[float]
|
||||
|
||||
exit_reason: Optional[str]
|
||||
exit_order_status: Optional[str]
|
||||
|
||||
stop_loss_abs: Optional[float]
|
||||
stop_loss_ratio: Optional[float]
|
||||
stop_loss_pct: Optional[float]
|
||||
@@ -264,7 +255,6 @@ class TradeSchema(BaseModel):
|
||||
initial_stop_loss_abs: Optional[float]
|
||||
initial_stop_loss_ratio: Optional[float]
|
||||
initial_stop_loss_pct: Optional[float]
|
||||
|
||||
min_rate: Optional[float]
|
||||
max_rate: Optional[float]
|
||||
open_order_id: Optional[str]
|
||||
@@ -283,11 +273,10 @@ class OpenTradeSchema(TradeSchema):
|
||||
stoploss_current_dist_ratio: Optional[float]
|
||||
stoploss_entry_dist: Optional[float]
|
||||
stoploss_entry_dist_ratio: Optional[float]
|
||||
current_profit: float
|
||||
current_profit_abs: float
|
||||
current_profit_pct: float
|
||||
current_rate: float
|
||||
total_profit_abs: float
|
||||
total_profit_fiat: Optional[float]
|
||||
total_profit_ratio: Optional[float]
|
||||
|
||||
open_order: Optional[str]
|
||||
|
||||
|
||||
@@ -311,7 +300,7 @@ class LockModel(BaseModel):
|
||||
lock_timestamp: int
|
||||
pair: str
|
||||
side: str
|
||||
reason: Optional[str]
|
||||
reason: str
|
||||
|
||||
|
||||
class Locks(BaseModel):
|
||||
@@ -467,5 +456,5 @@ class SysInfo(BaseModel):
|
||||
|
||||
|
||||
class Health(BaseModel):
|
||||
last_process: Optional[datetime]
|
||||
last_process_ts: Optional[int]
|
||||
last_process: datetime
|
||||
last_process_ts: int
|
||||
|
@@ -42,8 +42,7 @@ 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
|
||||
# 2.25: Add several profit values to /status endpoint
|
||||
API_VERSION = 2.25
|
||||
API_VERSION = 2.24
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -347,4 +346,4 @@ def sysinfo():
|
||||
|
||||
@router.get('/health', response_model=Health, tags=['info'])
|
||||
def health(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc.health()
|
||||
return rpc._health()
|
||||
|
@@ -1,11 +1,9 @@
|
||||
from typing import Any, AsyncIterator, Dict, Optional
|
||||
from uuid import uuid4
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
|
||||
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
|
||||
@@ -17,19 +15,12 @@ def get_rpc_optional() -> Optional[RPC]:
|
||||
return None
|
||||
|
||||
|
||||
async def get_rpc() -> Optional[AsyncIterator[RPC]]:
|
||||
|
||||
def get_rpc() -> Optional[Iterator[RPC]]:
|
||||
_rpc = get_rpc_optional()
|
||||
if _rpc:
|
||||
request_id = str(uuid4())
|
||||
ctx_token = _request_id_ctx_var.set(request_id)
|
||||
Trade.rollback()
|
||||
try:
|
||||
yield _rpc
|
||||
finally:
|
||||
Trade.session.remove()
|
||||
_request_id_ctx_var.reset(ctx_token)
|
||||
|
||||
Trade.rollback()
|
||||
else:
|
||||
raise RPCException('Bot is not in the correct state')
|
||||
|
||||
|
@@ -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, Sequence, Tuple, Union
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
import psutil
|
||||
@@ -13,15 +13,14 @@ 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
|
||||
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.data.metrics import calculate_max_drawdown
|
||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection,
|
||||
State, TradingMode)
|
||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
|
||||
TradingMode)
|
||||
from freqtrade.exceptions import ExchangeError, PricingError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.loggers import bufferHandler
|
||||
@@ -123,8 +122,7 @@ 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('order_types',
|
||||
{}).get('stoploss_on_exchange', False),
|
||||
'stoploss_on_exchange': config.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'),
|
||||
@@ -160,7 +158,7 @@ class RPC:
|
||||
"""
|
||||
# Fetch open trades
|
||||
if trade_ids:
|
||||
trades: Sequence[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
|
||||
trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
|
||||
else:
|
||||
trades = Trade.get_open_trades()
|
||||
|
||||
@@ -171,7 +169,6 @@ class RPC:
|
||||
for trade in trades:
|
||||
order: Optional[Order] = None
|
||||
current_profit_fiat: Optional[float] = None
|
||||
total_profit_fiat: Optional[float] = None
|
||||
if trade.open_order_id:
|
||||
order = trade.select_order_by_order_id(trade.open_order_id)
|
||||
# calculate profit and send message to user
|
||||
@@ -191,14 +188,8 @@ class RPC:
|
||||
else:
|
||||
# Closed trade ...
|
||||
current_rate = trade.close_rate
|
||||
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
|
||||
)
|
||||
current_profit = trade.close_profit
|
||||
current_profit_abs = trade.close_profit_abs
|
||||
|
||||
# Calculate fiat profit
|
||||
if not isnan(current_profit_abs) and self._fiat_converter:
|
||||
@@ -207,11 +198,6 @@ class RPC:
|
||||
self._freqtrade.config['stake_currency'],
|
||||
self._freqtrade.config['fiat_display_currency']
|
||||
)
|
||||
total_profit_fiat = self._fiat_converter.convert_amount(
|
||||
total_profit_abs,
|
||||
self._freqtrade.config['stake_currency'],
|
||||
self._freqtrade.config['fiat_display_currency']
|
||||
)
|
||||
|
||||
# Calculate guaranteed profit (in case of trailing stop)
|
||||
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
|
||||
@@ -224,14 +210,14 @@ class RPC:
|
||||
trade_dict.update(dict(
|
||||
close_profit=trade.close_profit if not trade.is_open else None,
|
||||
current_rate=current_rate,
|
||||
current_profit=current_profit, # Deprecated
|
||||
current_profit_pct=round(current_profit * 100, 2), # Deprecated
|
||||
current_profit_abs=current_profit_abs, # Deprecated
|
||||
profit_ratio=current_profit,
|
||||
profit_pct=round(current_profit * 100, 2),
|
||||
profit_abs=current_profit_abs,
|
||||
profit_fiat=current_profit_fiat,
|
||||
|
||||
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),
|
||||
@@ -341,13 +327,11 @@ class RPC:
|
||||
for day in range(0, timescale):
|
||||
profitday = start_date - time_offset(day)
|
||||
# Only query for necessary columns for performance reasons.
|
||||
trades = Trade.session.execute(
|
||||
select(Trade.close_profit_abs)
|
||||
.filter(Trade.is_open.is_(False),
|
||||
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()
|
||||
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)
|
||||
@@ -383,27 +367,21 @@ 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: Any = Trade.id if order_by_id else Trade.close_date.desc()
|
||||
order_by = Trade.id if order_by_id else Trade.close_date.desc()
|
||||
if limit:
|
||||
trades = Trade.session.scalars(
|
||||
Trade.get_trades_query([Trade.is_open.is_(False)])
|
||||
.order_by(order_by)
|
||||
.limit(limit)
|
||||
.offset(offset))
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||
order_by).limit(limit).offset(offset)
|
||||
else:
|
||||
trades = Trade.session.scalars(
|
||||
Trade.get_trades_query([Trade.is_open.is_(False)])
|
||||
.order_by(Trade.close_date.desc()))
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||
Trade.close_date.desc()).all()
|
||||
|
||||
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": total_trades,
|
||||
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
|
||||
}
|
||||
|
||||
def _rpc_stats(self) -> Dict[str, Any]:
|
||||
@@ -417,7 +395,7 @@ class RPC:
|
||||
return 'losses'
|
||||
else:
|
||||
return 'draws'
|
||||
trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
|
||||
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
|
||||
# Sell reason
|
||||
exit_reasons = {}
|
||||
for trade in trades:
|
||||
@@ -426,7 +404,7 @@ class RPC:
|
||||
exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
|
||||
|
||||
# Duration
|
||||
dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []}
|
||||
dur: Dict[str, List[int]] = {'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()
|
||||
@@ -445,8 +423,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: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query(
|
||||
trade_filter, include_orders=False).order_by(Trade.id)).all()
|
||||
trades: List[Trade] = Trade.get_trades(
|
||||
trade_filter, include_orders=False).order_by(Trade.id).all()
|
||||
|
||||
profit_all_coin = []
|
||||
profit_all_ratio = []
|
||||
@@ -465,11 +443,11 @@ class RPC:
|
||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||
|
||||
if not trade.is_open:
|
||||
profit_ratio = trade.close_profit or 0.0
|
||||
profit_abs = trade.close_profit_abs or 0.0
|
||||
profit_ratio = trade.close_profit
|
||||
profit_abs = trade.close_profit_abs
|
||||
profit_closed_coin.append(profit_abs)
|
||||
profit_closed_ratio.append(profit_ratio)
|
||||
if profit_ratio >= 0:
|
||||
if trade.close_profit >= 0:
|
||||
winning_trades += 1
|
||||
winning_profit += profit_abs
|
||||
else:
|
||||
@@ -522,7 +500,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 and trade.close_date])
|
||||
for trade in trades if not trade.is_open])
|
||||
max_drawdown_abs = 0.0
|
||||
max_drawdown = 0.0
|
||||
if len(trades_df) > 0:
|
||||
@@ -801,8 +779,7 @@ class RPC:
|
||||
# check if valid pair
|
||||
|
||||
# check if pair already has an open pair
|
||||
trade: Optional[Trade] = Trade.get_trades(
|
||||
[Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
trade: 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
|
||||
@@ -955,12 +932,12 @@ class RPC:
|
||||
def _rpc_delete_lock(self, lockid: Optional[int] = None,
|
||||
pair: Optional[str] = None) -> Dict[str, Any]:
|
||||
""" Delete specific lock(s) """
|
||||
locks: Sequence[PairLock] = []
|
||||
locks = []
|
||||
|
||||
if pair:
|
||||
locks = PairLocks.get_pair_locks(pair)
|
||||
if lockid:
|
||||
locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all()
|
||||
locks = PairLock.query.filter(PairLock.id == lockid).all()
|
||||
|
||||
for lock in locks:
|
||||
lock.active = False
|
||||
@@ -1221,23 +1198,10 @@ class RPC:
|
||||
"ram_pct": psutil.virtual_memory().percent
|
||||
}
|
||||
|
||||
def health(self) -> Dict[str, Optional[Union[str, int]]]:
|
||||
def _health(self) -> Dict[str, Union[str, int]]:
|
||||
last_p = self._freqtrade.last_process
|
||||
if last_p is None:
|
||||
return {
|
||||
"last_process": None,
|
||||
"last_process_loc": None,
|
||||
"last_process_ts": None,
|
||||
'last_process': str(last_p),
|
||||
'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT),
|
||||
'last_process_ts': int(last_p.timestamp()),
|
||||
}
|
||||
|
||||
return {
|
||||
"last_process": str(last_p),
|
||||
"last_process_loc": last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT),
|
||||
"last_process_ts": int(last_p.timestamp()),
|
||||
}
|
||||
|
||||
def _update_market_direction(self, direction: MarketDirection) -> None:
|
||||
self._freqtrade.strategy.market_direction = direction
|
||||
|
||||
def _get_market_direction(self) -> MarketDirection:
|
||||
return self._freqtrade.strategy.market_direction
|
||||
|
@@ -25,7 +25,7 @@ from telegram.utils.helpers import escape_markdown
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.constants import DUST_PER_COIN, Config
|
||||
from freqtrade.enums import MarketDirection, RPCMessageType, SignalDirection, TradingMode
|
||||
from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import chunks, plural, round_coin_value
|
||||
from freqtrade.persistence import Trade
|
||||
@@ -83,8 +83,6 @@ 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
|
||||
|
||||
@@ -131,8 +129,7 @@ class Telegram(RPCHandler):
|
||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
|
||||
r'/forcesell$', r'/forceexit$',
|
||||
r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir (long|short|even|none)$',
|
||||
r'/marketdir$'
|
||||
r'/edge$', r'/health$', r'/help$', r'/version$'
|
||||
]
|
||||
# Create keys for generation
|
||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||
@@ -200,7 +197,6 @@ class Telegram(RPCHandler):
|
||||
CommandHandler('health', self._health),
|
||||
CommandHandler('help', self._help),
|
||||
CommandHandler('version', self._version),
|
||||
CommandHandler('marketdir', self._changemarketdir)
|
||||
]
|
||||
callbacks = [
|
||||
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
|
||||
@@ -323,33 +319,31 @@ 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']}"
|
||||
exit_wording = f"Partially {exit_wording.lower()}"
|
||||
cp_extra = (
|
||||
f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} "
|
||||
else:
|
||||
cp_extra = ''
|
||||
cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \
|
||||
f"{msg['stake_currency']}{cp_extra}`)\n"
|
||||
)
|
||||
|
||||
message = (
|
||||
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
||||
f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"{'Exited' if is_fill else 'Exiting'} {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"
|
||||
@@ -368,7 +362,7 @@ class Telegram(RPCHandler):
|
||||
|
||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||
message += f"*Exit Rate:* `{msg['close_rate']:.8f}`"
|
||||
if is_sub_trade:
|
||||
if msg.get('sub_trade'):
|
||||
if self._rpc._fiat_converter:
|
||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
@@ -416,9 +410,6 @@ 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']}"
|
||||
@@ -478,51 +469,44 @@ class Telegram(RPCHandler):
|
||||
lines_detail: List[str] = []
|
||||
if len(filled_orders) > 0:
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
order_nr = 0
|
||||
for order in filled_orders:
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
lines: List[str] = []
|
||||
if order['is_open'] is True:
|
||||
continue
|
||||
order_nr += 1
|
||||
wording = 'Entry' if order['ft_is_entry'] else 'Exit'
|
||||
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["filled"] or order["amount"]
|
||||
cur_entry_average = order["safe_price"]
|
||||
lines.append(" ")
|
||||
if order_nr == 1:
|
||||
lines.append(f"*{wording} #{order_nr}:*")
|
||||
if x == 0:
|
||||
lines.append(f"*{wording} #{x+1}:*")
|
||||
lines.append(
|
||||
f"*Amount:* {cur_entry_amount} "
|
||||
f"({round_coin_value(order['cost'], quote_currency)})"
|
||||
)
|
||||
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average Price:* {cur_entry_average}")
|
||||
else:
|
||||
sum_stake = 0
|
||||
sum_amount = 0
|
||||
for y in range(order_nr):
|
||||
loc_order = filled_orders[y]
|
||||
if loc_order['is_open'] is True:
|
||||
# Skip open orders (e.g. stop orders)
|
||||
continue
|
||||
amount = loc_order["filled"] or loc_order["amount"]
|
||||
sum_stake += amount * loc_order["safe_price"]
|
||||
sum_amount += amount
|
||||
prev_avg_price = sum_stake / sum_amount
|
||||
sumA = 0
|
||||
sumB = 0
|
||||
for y in range(x):
|
||||
amount = filled_orders[y]["filled"] or filled_orders[y]["amount"]
|
||||
sumA += amount * filled_orders[y]["safe_price"]
|
||||
sumB += amount
|
||||
prev_avg_price = sumA / sumB
|
||||
# TODO: This calculation ignores fees.
|
||||
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
|
||||
minus_on_entry = 0
|
||||
if prev_avg_price:
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
|
||||
|
||||
lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg Profit")
|
||||
lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit")
|
||||
if is_open:
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
lines.append(f"*Amount:* {cur_entry_amount} "
|
||||
f"({round_coin_value(order['cost'], quote_currency)})")
|
||||
lines.append(
|
||||
f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average {wording} Price:* {cur_entry_average} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry Rate)")
|
||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||
lines.append(f"*Order filled:* {order['order_filled_date']}")
|
||||
|
||||
# TODO: is this really useful?
|
||||
@@ -534,7 +518,6 @@ class Telegram(RPCHandler):
|
||||
# lines.append(
|
||||
# f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})")
|
||||
lines_detail.append("\n".join(lines))
|
||||
|
||||
return lines_detail
|
||||
|
||||
@authorized_only
|
||||
@@ -570,54 +553,35 @@ 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(
|
||||
r['total_profit_abs'], r['quote_currency'])
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
(" `(since {open_date_hum})`" if r['is_open'] else ""),
|
||||
"*Current Pair:* {pair}",
|
||||
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 "",
|
||||
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
||||
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
||||
"*Amount:* `{amount} ({stake_amount} {quote_currency})`",
|
||||
"*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.extend([
|
||||
"*Number of Entries:* `{num_entries}" + max_buy_str + "`",
|
||||
"*Number of Exits:* `{num_exits}`"
|
||||
])
|
||||
lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str)
|
||||
|
||||
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 "",
|
||||
" \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})`",
|
||||
"*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "",
|
||||
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||
+ "`{profit_ratio:.2%}`",
|
||||
])
|
||||
|
||||
if r['is_open']:
|
||||
if r.get('realized_profit'):
|
||||
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(" ")
|
||||
lines.append("*Realized Profit:* `{realized_profit:.8f}`")
|
||||
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||
and r['initial_stop_loss_ratio'] is not None):
|
||||
# Adding initial stoploss only if it is different from stoploss
|
||||
@@ -1076,14 +1040,10 @@ class Telegram(RPCHandler):
|
||||
query.answer()
|
||||
query.edit_message_text(text="Force exit canceled.")
|
||||
return
|
||||
trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
|
||||
trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
|
||||
query.answer()
|
||||
if trade:
|
||||
query.edit_message_text(
|
||||
text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
|
||||
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':
|
||||
@@ -1342,7 +1302,7 @@ class Telegram(RPCHandler):
|
||||
message = tabulate({k: [v] for k, v in counts.items()},
|
||||
headers=['current', 'max', 'total stake'],
|
||||
tablefmt='simple')
|
||||
message = f"<pre>{message}</pre>"
|
||||
message = "<pre>{}</pre>".format(message)
|
||||
logger.debug(message)
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_count",
|
||||
@@ -1534,9 +1494,6 @@ class Telegram(RPCHandler):
|
||||
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||
"*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n"
|
||||
"*/marketdir [long | short | even | none]:* `Updates the user managed variable "
|
||||
"that represents the current market direction. If no direction is provided `"
|
||||
"`the currently set market direction will be output.` \n"
|
||||
|
||||
"_Statistics_\n"
|
||||
"------------\n"
|
||||
@@ -1570,7 +1527,7 @@ class Telegram(RPCHandler):
|
||||
Handler for /health
|
||||
Shows the last process timestamp
|
||||
"""
|
||||
health = self._rpc.health()
|
||||
health = self._rpc._health()
|
||||
message = f"Last process: `{health['last_process_loc']}`"
|
||||
self._send_msg(message)
|
||||
|
||||
@@ -1644,7 +1601,7 @@ class Telegram(RPCHandler):
|
||||
])
|
||||
else:
|
||||
reply_markup = InlineKeyboardMarkup([[]])
|
||||
msg += f"\nUpdated: {datetime.now().ctime()}"
|
||||
msg += "\nUpdated: {}".format(datetime.now().ctime())
|
||||
if not query.message:
|
||||
return
|
||||
chat_id = query.message.chat_id
|
||||
@@ -1720,39 +1677,3 @@ class Telegram(RPCHandler):
|
||||
'TelegramError: %s! Giving up on that message.',
|
||||
telegram_err.message
|
||||
)
|
||||
|
||||
@authorized_only
|
||||
def _changemarketdir(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /marketdir.
|
||||
Updates the bot's market_direction
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
if context.args and len(context.args) == 1:
|
||||
new_market_dir_arg = context.args[0]
|
||||
old_market_dir = self._rpc._get_market_direction()
|
||||
new_market_dir = None
|
||||
if new_market_dir_arg == "long":
|
||||
new_market_dir = MarketDirection.LONG
|
||||
elif new_market_dir_arg == "short":
|
||||
new_market_dir = MarketDirection.SHORT
|
||||
elif new_market_dir_arg == "even":
|
||||
new_market_dir = MarketDirection.EVEN
|
||||
elif new_market_dir_arg == "none":
|
||||
new_market_dir = MarketDirection.NONE
|
||||
|
||||
if new_market_dir is not None:
|
||||
self._rpc._update_market_direction(new_market_dir)
|
||||
self._send_msg("Successfully updated market direction"
|
||||
f" from *{old_market_dir}* to *{new_market_dir}*.")
|
||||
else:
|
||||
raise RPCException("Invalid market direction provided. \n"
|
||||
"Valid market directions: *long, short, even, none*")
|
||||
elif context.args is not None and len(context.args) == 0:
|
||||
old_market_dir = self._rpc._get_market_direction()
|
||||
self._send_msg(f"Currently set market direction: *{old_market_dir}*")
|
||||
else:
|
||||
raise RPCException("Invalid usage of command /marketdir. \n"
|
||||
"Usage: */marketdir [short | long | even | none]*")
|
||||
|
@@ -58,7 +58,6 @@ 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:
|
||||
@@ -113,7 +112,7 @@ class Webhook(RPCHandler):
|
||||
response = post(self._url, data=payload['data'],
|
||||
headers={'Content-Type': 'text/plain'})
|
||||
else:
|
||||
raise NotImplementedError(f'Unknown format: {self._format}')
|
||||
raise NotImplementedError('Unknown format: {}'.format(self._format))
|
||||
|
||||
# Throw a RequestException if the post was not successful
|
||||
response.raise_for_status()
|
||||
|
@@ -12,8 +12,8 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode,
|
||||
SignalDirection, SignalTagType, SignalType, TradingMode)
|
||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
|
||||
SignalTagType, SignalType, TradingMode)
|
||||
from freqtrade.exceptions import OperationalException, StrategyError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
|
||||
from freqtrade.misc import remove_entry_exit_signals
|
||||
@@ -122,9 +122,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# Definition of plot_config. See plotting documentation for more details.
|
||||
plot_config: Dict = {}
|
||||
|
||||
# A self set parameter that represents the market direction. filled from configuration
|
||||
market_direction: MarketDirection = MarketDirection.NONE
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.config = config
|
||||
# Dict to determine if analysis is necessary
|
||||
|
@@ -86,41 +86,37 @@ 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,
|
||||
leverage: float = 1.0
|
||||
is_short: bool = False
|
||||
) -> float:
|
||||
"""
|
||||
Given the current profit, and a desired stop loss value relative to the trade entry price,
|
||||
|
||||
Given the current profit, and a desired stop loss value relative to the open 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 the open price,
|
||||
adjusted for leverage
|
||||
:param open_relative_stop: Desired stop loss percentage relative to open price
|
||||
: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
|
||||
_current_profit = current_profit / leverage
|
||||
if (_current_profit == -1 and not is_short) or (is_short and _current_profit == 1):
|
||||
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 / leverage) / (1 - _current_profit))
|
||||
stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit))
|
||||
else:
|
||||
stoploss = 1 - ((1 + open_relative_stop / leverage) / (1 + _current_profit))
|
||||
stoploss = 1 - ((1 + open_relative_stop) / (1 + current_profit))
|
||||
|
||||
# negative stoploss values indicate the requested stop price is higher/lower
|
||||
# (long/short) than the current price
|
||||
return max(stoploss * leverage, 0.0)
|
||||
return max(stoploss, 0.0)
|
||||
|
||||
|
||||
def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float:
|
||||
|
@@ -1,255 +0,0 @@
|
||||
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
|
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
|
||||
from packaging import version
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.enums.tradingmode import TradingMode
|
||||
@@ -45,7 +44,7 @@ def _migrate_binance_futures_db(config: Config):
|
||||
# Should symbol be migrated too?
|
||||
# order.symbol = new_pair
|
||||
Trade.commit()
|
||||
pls = PairLock.session.scalars(select(PairLock).filter(PairLock.pair.notlike('%:%'))).all()
|
||||
pls = PairLock.query.filter(PairLock.pair.notlike('%:%'))
|
||||
for pl in pls:
|
||||
pl.pair = f"{pl.pair}:{config['stake_currency']}"
|
||||
# print(pls)
|
||||
|
8
freqtrade/vendor/qtpylib/indicators.py
vendored
8
freqtrade/vendor/qtpylib/indicators.py
vendored
@@ -1,3 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# QTPyLib: Quantitative Trading Python Library
|
||||
# https://github.com/ranaroussi/qtpylib
|
||||
#
|
||||
@@ -16,6 +18,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -24,6 +27,11 @@ 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)
|
||||
|
||||
|
@@ -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 RPCMessageType, State
|
||||
from freqtrade.enums import State
|
||||
from freqtrade.exceptions import OperationalException, TemporaryError
|
||||
from freqtrade.exchange import timeframe_to_next_date
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
@@ -185,10 +185,7 @@ 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}```\n {hint}',
|
||||
msg_type=RPCMessageType.EXCEPTION
|
||||
)
|
||||
self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}')
|
||||
|
||||
logger.exception('OperationalException. Stopping trader ...')
|
||||
self.freqtrade.state = State.STOPPED
|
||||
|
@@ -1,7 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools >= 46.4.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
exclude = '''
|
||||
@@ -39,9 +35,6 @@ warn_unused_ignores = true
|
||||
exclude = [
|
||||
'^build_helpers\.py$'
|
||||
]
|
||||
plugins = [
|
||||
"sqlalchemy.ext.mypy.plugin"
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests.*"
|
||||
@@ -52,6 +45,10 @@ 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 = [
|
||||
@@ -59,28 +56,3 @@ exclude = [
|
||||
"build_helpers/*.py",
|
||||
]
|
||||
ignore = ["freqtrade/vendor/**"]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
extend-exclude = [".env"]
|
||||
target-version = "py38"
|
||||
extend-select = [
|
||||
"C90", # mccabe
|
||||
# "N", # pep8-naming
|
||||
"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
|
||||
]
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
max-complexity = 12
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"tests/*" = ["S"]
|
||||
|
@@ -7,11 +7,12 @@
|
||||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==3.3.1
|
||||
ruff==0.0.257
|
||||
mypy==1.1.1
|
||||
pre-commit==3.2.0
|
||||
pytest==7.2.2
|
||||
pytest-asyncio==0.21.0
|
||||
flake8==6.0.0
|
||||
flake8-tidy-imports==4.8.0
|
||||
mypy==1.0.1
|
||||
pre-commit==3.0.4
|
||||
pytest==7.2.1
|
||||
pytest-asyncio==0.20.3
|
||||
pytest-cov==4.0.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-random-order==1.1.0
|
||||
@@ -22,11 +23,11 @@ time-machine==2.9.0
|
||||
httpx==0.23.3
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.2.10
|
||||
nbconvert==7.2.9
|
||||
|
||||
# mypy types
|
||||
types-cachetools==5.3.0.4
|
||||
types-cachetools==5.3.0.0
|
||||
types-filelock==3.2.7
|
||||
types-requests==2.28.11.15
|
||||
types-tabulate==0.9.0.1
|
||||
types-python-dateutil==2.8.19.10
|
||||
types-requests==2.28.11.13
|
||||
types-tabulate==0.9.0.0
|
||||
types-python-dateutil==2.8.19.6
|
||||
|
@@ -2,9 +2,9 @@
|
||||
-r requirements-freqai.txt
|
||||
|
||||
# Required for freqai-rl
|
||||
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'
|
||||
torch==1.13.1
|
||||
stable-baselines3==1.7.0
|
||||
sb3-contrib==1.7.0
|
||||
# Gym is forced to this version by stable-baselines3.
|
||||
setuptools==65.5.1 # Should be removed when gym is fixed.
|
||||
gym==0.21; python_version < '3.11'
|
||||
gym==0.21
|
||||
|
@@ -5,7 +5,7 @@
|
||||
# Required for freqai
|
||||
scikit-learn==1.1.3
|
||||
joblib==1.2.0
|
||||
catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11'
|
||||
catboost==1.1.1; platform_machine != 'aarch64'
|
||||
lightgbm==3.3.5
|
||||
xgboost==1.7.4
|
||||
xgboost==1.7.3
|
||||
tensorboard==2.12.0
|
||||
|
@@ -5,5 +5,5 @@
|
||||
scipy==1.10.1
|
||||
scikit-learn==1.1.3
|
||||
scikit-optimize==0.9.0
|
||||
filelock==3.10.0
|
||||
filelock==3.9.0
|
||||
progressbar2==4.2.0
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.13.1
|
||||
plotly==5.13.0
|
||||
|
@@ -2,15 +2,15 @@ numpy==1.24.2
|
||||
pandas==1.5.3
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==3.0.23
|
||||
cryptography==39.0.2
|
||||
ccxt==2.8.17
|
||||
cryptography==39.0.1
|
||||
aiohttp==3.8.4
|
||||
SQLAlchemy==2.0.7
|
||||
SQLAlchemy==1.4.46
|
||||
python-telegram-bot==13.15
|
||||
arrow==1.2.3
|
||||
cachetools==4.2.2
|
||||
requests==2.28.2
|
||||
urllib3==1.26.15
|
||||
urllib3==1.26.14
|
||||
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.10
|
||||
python-rapidjson==1.9
|
||||
# Properly format api responses
|
||||
orjson==3.8.7
|
||||
orjson==3.8.6
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.95.0
|
||||
pydantic==1.10.6
|
||||
uvicorn==0.21.1
|
||||
fastapi==0.92.0
|
||||
pydantic==1.10.4
|
||||
uvicorn==0.20.0
|
||||
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.38
|
||||
prompt-toolkit==3.0.36
|
||||
# Extensions to datetime library
|
||||
python-dateutil==2.8.2
|
||||
|
||||
@@ -55,5 +55,3 @@ schedule==1.1.0
|
||||
#WS Messages
|
||||
websockets==10.4
|
||||
janus==1.0.0
|
||||
|
||||
ast-comments==1.0.1
|
||||
|
@@ -340,13 +340,11 @@ class FtRestClient():
|
||||
:param limit: Limit result to the last n candles.
|
||||
:return: json object
|
||||
"""
|
||||
params = {
|
||||
return self._get("pair_candles", params={
|
||||
"pair": pair,
|
||||
"timeframe": timeframe,
|
||||
}
|
||||
if limit:
|
||||
params['limit'] = limit
|
||||
return self._get("pair_candles", params=params)
|
||||
"limit": limit,
|
||||
})
|
||||
|
||||
def pair_history(self, pair, timeframe, strategy, timerange=None):
|
||||
"""Return historic, analyzed dataframe
|
||||
|
@@ -17,7 +17,6 @@ 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
|
||||
|
2
setup.py
2
setup.py
@@ -32,6 +32,8 @@ hdf5 = [
|
||||
|
||||
develop = [
|
||||
'coveralls',
|
||||
'flake8',
|
||||
'flake8-tidy-imports',
|
||||
'mypy',
|
||||
'pytest',
|
||||
'pytest-asyncio',
|
||||
|
@@ -14,8 +14,7 @@ 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_strategy_update, start_test_pairlist, start_trading,
|
||||
start_webserver)
|
||||
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)
|
||||
@@ -25,7 +24,7 @@ from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.persistence.models import init_db
|
||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_args, log_has,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has,
|
||||
log_has_re, patch_exchange, patched_configuration_load_config_file)
|
||||
from tests.conftest_trades import MOCK_TRADE_COUNT
|
||||
|
||||
@@ -455,7 +454,7 @@ def test_list_markets(mocker, markets_static, capsys):
|
||||
assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE)
|
||||
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(side_effect=ValueError))
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError))
|
||||
# Test --one-column
|
||||
args = [
|
||||
"list-markets",
|
||||
@@ -644,7 +643,9 @@ def test_download_data_keyboardInterrupt(mocker, markets):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(side_effect=KeyboardInterrupt))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
@@ -663,7 +664,9 @@ def test_download_data_timerange(mocker, markets):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
@@ -712,7 +715,9 @@ def test_download_data_no_markets(mocker, caplog):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker, id='binance')
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
@@ -728,7 +733,9 @@ def test_download_data_no_exchange(mocker, caplog):
|
||||
mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
]
|
||||
@@ -744,7 +751,9 @@ def test_download_data_no_pairs(mocker):
|
||||
mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange",
|
||||
@@ -762,7 +771,9 @@ def test_download_data_all_pairs(mocker, markets):
|
||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange",
|
||||
@@ -799,7 +810,9 @@ def test_download_data_trades(mocker, caplog):
|
||||
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
||||
MagicMock(return_value=[]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "kraken",
|
||||
@@ -830,7 +843,9 @@ def test_download_data_trades(mocker, caplog):
|
||||
|
||||
def test_download_data_data_invalid(mocker):
|
||||
patch_exchange(mocker, id="kraken")
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "kraken",
|
||||
@@ -847,7 +862,9 @@ def test_start_convert_trades(mocker, caplog):
|
||||
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
||||
MagicMock(return_value=[]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"trades-to-ohlcv",
|
||||
"--exchange", "kraken",
|
||||
@@ -954,7 +971,7 @@ def test_start_list_freqAI_models(capsys):
|
||||
|
||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||
patch_exchange(mocker, mock_markets=True)
|
||||
mocker.patch.multiple(EXMS,
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers,
|
||||
)
|
||||
@@ -1547,37 +1564,3 @@ 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
|
||||
|
@@ -40,7 +40,6 @@ np.seterr(all='raise')
|
||||
|
||||
CURRENT_TEST_STRATEGY = 'StrategyTestV3'
|
||||
TRADE_SIDES = ('long', 'short')
|
||||
EXMS = 'freqtrade.exchange.exchange.Exchange'
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -146,21 +145,22 @@ def patch_exchange(
|
||||
mock_markets=True,
|
||||
mock_supported_modes=True
|
||||
) -> None:
|
||||
mocker.patch(f'{EXMS}._load_async_markets', return_value={})
|
||||
mocker.patch(f'{EXMS}.validate_config', MagicMock())
|
||||
mocker.patch(f'{EXMS}.validate_timeframes', MagicMock())
|
||||
mocker.patch(f'{EXMS}.id', PropertyMock(return_value=id))
|
||||
mocker.patch(f'{EXMS}.name', PropertyMock(return_value=id.title()))
|
||||
mocker.patch(f'{EXMS}.precisionMode', PropertyMock(return_value=2))
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||
|
||||
if mock_markets:
|
||||
if isinstance(mock_markets, bool):
|
||||
mock_markets = get_markets()
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=mock_markets))
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||
PropertyMock(return_value=mock_markets))
|
||||
|
||||
if mock_supported_modes:
|
||||
mocker.patch(
|
||||
f'freqtrade.exchange.{id}.{id.capitalize()}._supported_trading_mode_margin_pairs',
|
||||
f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_margin_pairs',
|
||||
PropertyMock(return_value=[
|
||||
(TradingMode.MARGIN, MarginMode.CROSS),
|
||||
(TradingMode.MARGIN, MarginMode.ISOLATED),
|
||||
@@ -170,10 +170,10 @@ def patch_exchange(
|
||||
)
|
||||
|
||||
if api_mock:
|
||||
mocker.patch(f'{EXMS}._init_ccxt', return_value=api_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||
else:
|
||||
mocker.patch(f'{EXMS}._init_ccxt', MagicMock())
|
||||
mocker.patch(f'{EXMS}.timeframes', PropertyMock(
|
||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(
|
||||
return_value=['5m', '15m', '1h', '1d']))
|
||||
|
||||
|
||||
@@ -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.session.add(trade)
|
||||
Trade.query.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.session.rollback()
|
||||
Trade.query.session.rollback()
|
||||
|
||||
def add_trade(trade):
|
||||
if use_db:
|
||||
Trade.session.add(trade)
|
||||
Trade.query.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.session.flush()
|
||||
Trade.query.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.session.add(trade)
|
||||
Trade.query.session.add(trade)
|
||||
else:
|
||||
LocalTrade.add_bt_trade(trade)
|
||||
|
||||
|
@@ -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("filename" + "nofile")
|
||||
load_backtest_data(str("filename") + "nofile")
|
||||
|
||||
with pytest.raises(ValueError, match=r"Unknown dataformat."):
|
||||
load_backtest_data(testdatadir / "backtest_results" / LAST_BT_RESULT_FN)
|
||||
|
@@ -8,7 +8,7 @@ from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import CandleType, RunMode
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||
from tests.conftest import EXMS, generate_test_data, get_patched_exchange
|
||||
from tests.conftest import generate_test_data, get_patched_exchange
|
||||
|
||||
|
||||
@pytest.mark.parametrize('candle_type', [
|
||||
@@ -223,7 +223,7 @@ def test_emit_df(mocker, default_conf, ohlcv_history):
|
||||
|
||||
def test_refresh(mocker, default_conf):
|
||||
refresh_mock = MagicMock()
|
||||
mocker.patch(f"{EXMS}.refresh_latest_ohlcv", refresh_mock)
|
||||
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
timeframe = default_conf["timeframe"]
|
||||
@@ -281,7 +281,7 @@ def test_market(mocker, default_conf, markets):
|
||||
|
||||
def test_ticker(mocker, default_conf, tickers):
|
||||
ticker_mock = MagicMock(return_value=tickers()['ETH/BTC'])
|
||||
mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock)
|
||||
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
res = dp.ticker('ETH/BTC')
|
||||
@@ -290,7 +290,7 @@ def test_ticker(mocker, default_conf, tickers):
|
||||
assert res['symbol'] == 'ETH/BTC'
|
||||
|
||||
ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found'))
|
||||
mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock)
|
||||
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
res = dp.ticker('UNITTEST/BTC')
|
||||
@@ -301,7 +301,7 @@ def test_current_whitelist(mocker, default_conf, tickers):
|
||||
# patch default conf to volumepairlist
|
||||
default_conf['pairlists'][0] = {'method': 'VolumePairList', "number_assets": 5}
|
||||
|
||||
mocker.patch.multiple(EXMS,
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
@@ -26,7 +26,7 @@ from freqtrade.enums import CandleType
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import file_dump_json
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_patched_exchange, log_has, log_has_re,
|
||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re,
|
||||
patch_exchange)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_load_data_7min_timeframe(caplog, testdatadir) -> None:
|
||||
|
||||
|
||||
def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None:
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
file = testdatadir / 'UNITTEST_BTC-1m.json'
|
||||
load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'])
|
||||
assert file.is_file()
|
||||
@@ -77,7 +77,7 @@ def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) ->
|
||||
|
||||
|
||||
def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None:
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
|
||||
file = testdatadir / 'futures/UNITTEST_USDT_USDT-1h-mark.json'
|
||||
load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark')
|
||||
assert file.is_file()
|
||||
@@ -109,7 +109,7 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog,
|
||||
Test load_pair_history() with 1 min timeframe
|
||||
"""
|
||||
tmpdir1 = Path(tmpdir)
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file = tmpdir1 / 'MEME_BTC-1m.json'
|
||||
|
||||
@@ -191,7 +191,7 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
|
||||
|
||||
test_data = None
|
||||
test_filename = testdatadir.joinpath('UNITTEST_BTC-1m.json')
|
||||
with test_filename.open("rt") as file:
|
||||
with open(test_filename, "rt") as file:
|
||||
test_data = json.load(file)
|
||||
|
||||
test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC',
|
||||
@@ -277,7 +277,7 @@ def test_download_pair_history(
|
||||
subdir,
|
||||
file_tail
|
||||
) -> None:
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
tmpdir1 = Path(tmpdir)
|
||||
file1_1 = tmpdir1 / f'{subdir}MEME_BTC-1m{file_tail}.json'
|
||||
@@ -328,7 +328,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
||||
json_dump_mock = mocker.patch(
|
||||
'freqtrade.data.history.jsondatahandler.JsonDataHandler.ohlcv_store',
|
||||
return_value=None)
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=tick)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
_download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC",
|
||||
timeframe='1m', candle_type='spot')
|
||||
@@ -340,7 +340,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
||||
|
||||
|
||||
def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None:
|
||||
mocker.patch(f'{EXMS}.get_historic_ohlcv',
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
|
||||
side_effect=Exception('File Error'))
|
||||
tmpdir1 = Path(tmpdir)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
@@ -409,7 +409,7 @@ def test_init_with_refresh(default_conf, mocker) -> None:
|
||||
|
||||
|
||||
def test_file_dump_json_tofile(testdatadir) -> None:
|
||||
file = testdatadir / f'test_{uuid.uuid4()}.json'
|
||||
file = testdatadir / 'test_{id}.json'.format(id=str(uuid.uuid4()))
|
||||
data = {'bar': 'foo'}
|
||||
|
||||
# check the file we will create does not exist
|
||||
@@ -506,7 +506,9 @@ def test_refresh_backtest_ohlcv_data(
|
||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount):
|
||||
dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_pair_history',
|
||||
MagicMock())
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "unlink", MagicMock())
|
||||
|
||||
@@ -529,7 +531,9 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
MagicMock())
|
||||
|
||||
ex = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
timerange = TimeRange.parse_timerange("20190101-20190102")
|
||||
unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"],
|
||||
timeframes=["1m", "5m"],
|
||||
@@ -547,7 +551,9 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir):
|
||||
dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_trades_history',
|
||||
MagicMock())
|
||||
mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "unlink", MagicMock())
|
||||
|
||||
@@ -571,7 +577,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
||||
tmpdir) -> None:
|
||||
tmpdir1 = Path(tmpdir)
|
||||
ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history))
|
||||
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||
ght_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file1 = tmpdir1 / 'ETH_BTC-trades.json.gz'
|
||||
data_handler = get_datahandler(tmpdir1, data_format='jsongz')
|
||||
@@ -597,7 +604,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
||||
|
||||
file1.unlink()
|
||||
|
||||
mocker.patch(f'{EXMS}.get_historic_trades', MagicMock(side_effect=ValueError))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||
MagicMock(side_effect=ValueError))
|
||||
|
||||
assert not _download_trades_history(data_handler=data_handler, exchange=exchange,
|
||||
pair='ETH/BTC')
|
||||
@@ -607,7 +615,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
||||
copyfile(testdatadir / file2.name, file2)
|
||||
|
||||
ght_mock.reset_mock()
|
||||
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_trades',
|
||||
ght_mock)
|
||||
# Since before first start date
|
||||
since_time = int(trades_history[0][0] // 1000) - 500
|
||||
timerange = TimeRange('date', None, since_time, 0)
|
||||
|
@@ -14,7 +14,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.edge import Edge, PairInfo
|
||||
from freqtrade.enums import ExitType
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from tests.conftest import EXMS, get_patched_freqtradebot, log_has
|
||||
from tests.conftest import get_patched_freqtradebot, log_has
|
||||
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
||||
_get_frame_time_from_offset)
|
||||
|
||||
@@ -261,7 +261,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m',
|
||||
|
||||
def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
@@ -273,7 +273,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
|
||||
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={}))
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
@@ -286,7 +286,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
|
||||
def test_edge_process_no_trades(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch(f'{EXMS}.get_fee', return_value=0.001)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', )
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
|
||||
# Return empty
|
||||
@@ -303,7 +303,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog):
|
||||
mocker.patch('freqtrade.freqtradebot.validate_config_consistency')
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.001)
|
||||
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
|
||||
mocker.patch('freqtrade.edge.edge_positioning.refresh_data')
|
||||
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
|
||||
# Return empty
|
||||
@@ -319,7 +319,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog):
|
||||
|
||||
def test_edge_init_error(mocker, edge_conf,):
|
||||
edge_conf['stake_amount'] = 0.5
|
||||
mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'):
|
||||
get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
|
@@ -7,23 +7,10 @@ import pytest
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re
|
||||
from tests.conftest import 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"),
|
||||
@@ -47,12 +34,12 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte
|
||||
default_conf['dry_run'] = False
|
||||
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||
default_conf['trading_mode'] = trademode
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
@@ -126,12 +113,12 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop_loss_limit'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.create_stoploss(
|
||||
pair='ETH/BTC',
|
||||
amount=1,
|
||||
@@ -555,6 +542,7 @@ def test__set_leverage_binance(mocker, default_conf):
|
||||
"set_leverage",
|
||||
pair="XRP/USDT",
|
||||
leverage=5.0,
|
||||
trading_mode=TradingMode.FUTURES
|
||||
)
|
||||
|
||||
|
||||
@@ -612,7 +600,7 @@ def test_get_maintenance_ratio_and_amt_binance(
|
||||
mm_ratio,
|
||||
amt,
|
||||
):
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
exchange._leverage_tiers = leverage_tiers
|
||||
(result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from tests.conftest import EXMS, get_patched_exchange
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_get_trades_for_order(default_conf, mocker):
|
||||
@@ -9,7 +9,7 @@ def test_get_trades_for_order(default_conf, mocker):
|
||||
order_id = 'ABCD-ABCD'
|
||||
since = datetime(2018, 5, 5, 0, 0, 0)
|
||||
default_conf["dry_run"] = False
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||
api_mock = MagicMock()
|
||||
|
||||
api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user