Merge branch 'develop' into pr/Axel-CH/8779
This commit is contained in:
@@ -10,7 +10,7 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 15
|
||||||
target-branch: develop
|
target-branch: develop
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ jobs:
|
|||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.8
|
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@@ -469,7 +469,7 @@ jobs:
|
|||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.8
|
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ repos:
|
|||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: "v1.3.0"
|
rev: "v1.5.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
@@ -18,7 +18,7 @@ repos:
|
|||||||
- types-requests==2.31.0.2
|
- types-requests==2.31.0.2
|
||||||
- types-tabulate==0.9.0.3
|
- types-tabulate==0.9.0.3
|
||||||
- types-python-dateutil==2.8.19.14
|
- types-python-dateutil==2.8.19.14
|
||||||
- SQLAlchemy==2.0.19
|
- SQLAlchemy==2.0.20
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
|||||||
+9
-3
@@ -1,8 +1,14 @@
|
|||||||
# .readthedocs.yml
|
# .readthedocs.yml
|
||||||
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: latest
|
os: "ubuntu-22.04"
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: 3.8
|
install:
|
||||||
setup_py_install: false
|
- requirements: docs/requirements-docs.txt
|
||||||
|
|
||||||
|
mkdocs:
|
||||||
|
configuration: mkdocs.yml
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,8 +8,9 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib \
|
cd ta-lib \
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
&& 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 \
|
&& echo "Downloading gcc config.guess and config.sub" \
|
||||||
&& curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \
|
||||||
|
&& curl -s 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make
|
&& make
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ python -m pip install --upgrade pip wheel
|
|||||||
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
|
||||||
|
|
||||||
|
|
||||||
pip install --find-links=build_helpers\ TA-Lib
|
pip install --find-links=build_helpers\ --prefer-binary TA-Lib
|
||||||
|
|
||||||
pip install -r requirements-dev.txt
|
pip install -r requirements-dev.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|||||||
@@ -89,7 +89,6 @@
|
|||||||
],
|
],
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "binance",
|
"name": "binance",
|
||||||
"sandbox": false,
|
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"password": "",
|
"password": "",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
+15
-1
@@ -7,7 +7,7 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
|||||||
* **Strategy**: Your trading strategy, telling the bot what to do.
|
* **Strategy**: Your trading strategy, telling the bot what to do.
|
||||||
* **Trade**: Open position.
|
* **Trade**: Open position.
|
||||||
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
|
* **Open Order**: Order which is currently placed on the exchange, and is not yet complete.
|
||||||
* **Pair**: Tradable pair, usually in the format of Base/Quote (e.g. XRP/USDT).
|
* **Pair**: Tradable pair, usually in the format of Base/Quote (e.g. `XRP/USDT` for spot, `XRP/USDT:USDT` for futures).
|
||||||
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
|
* **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
|
||||||
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
* **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
|
||||||
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
* **Limit order**: Limit orders which execute at the defined limit price or better.
|
||||||
@@ -20,6 +20,20 @@ This page provides you some basic concepts on how Freqtrade works and operates.
|
|||||||
|
|
||||||
All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt / Dry-run modes, the exchange default fee is used (lowest tier on the exchange). For live operations, fees are used as applied by the exchange (this includes BNB rebates etc.).
|
All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt / Dry-run modes, the exchange default fee is used (lowest tier on the exchange). For live operations, fees are used as applied by the exchange (this includes BNB rebates etc.).
|
||||||
|
|
||||||
|
## Pair naming
|
||||||
|
|
||||||
|
Freqtrade follows the [ccxt naming convention](https://docs.ccxt.com/#/README?id=consistency-of-base-and-quote-currencies) for currencies.
|
||||||
|
Using the wrong naming convention in the wrong market will usually result in the bot not recognizing the pair, usually resulting in errors like "this pair is not available".
|
||||||
|
|
||||||
|
### Spot pair naming
|
||||||
|
|
||||||
|
For spot pairs, naming will be `base/quote` (e.g. `ETH/USDT`).
|
||||||
|
|
||||||
|
### Futures pair naming
|
||||||
|
|
||||||
|
For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
|
|
||||||
## Bot execution logic
|
## Bot execution logic
|
||||||
|
|
||||||
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
This page explains the different parameters of the bot and how to run it.
|
This page explains the different parameters of the bot and how to run it.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
|
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .venv/bin/activate`) before running freqtrade commands.
|
||||||
|
|
||||||
!!! Warning "Up-to-date clock"
|
!!! Warning "Up-to-date clock"
|
||||||
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
| `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
|
||||||
| | **Exchange**
|
| | **Exchange**
|
||||||
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
|
||||||
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
|
|
||||||
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ For this to work, first activate your virtual environment and run the following
|
|||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# Activate virtual environment
|
# Activate virtual environment
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
|
|
||||||
pip install ipykernel
|
pip install ipykernel
|
||||||
ipython kernel install --user --name=freqtrade
|
ipython kernel install --user --name=freqtrade
|
||||||
|
|||||||
+14
-1
@@ -77,7 +77,7 @@ def test_method_to_test(caplog):
|
|||||||
|
|
||||||
### Debug configuration
|
### Debug configuration
|
||||||
|
|
||||||
To debug freqtrade, we recommend VSCode with the following launch configuration (located in `.vscode/launch.json`).
|
To debug freqtrade, we recommend VSCode (with the Python extension) with the following launch configuration (located in `.vscode/launch.json`).
|
||||||
Details will obviously vary between setups - but this should work to get you started.
|
Details will obviously vary between setups - but this should work to get you started.
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
@@ -102,6 +102,19 @@ This method can also be used to debug a strategy, by setting the breakpoints wit
|
|||||||
|
|
||||||
A similar setup can also be taken for Pycharm - using `freqtrade` as module name, and setting the command line arguments as "parameters".
|
A similar setup can also be taken for Pycharm - using `freqtrade` as module name, and setting the command line arguments as "parameters".
|
||||||
|
|
||||||
|
??? Tip "Correct venv usage"
|
||||||
|
When using a virtual environment (which you should), make sure that your Editor is using the correct virtual environment to avoid problems or "unknown import" errors.
|
||||||
|
|
||||||
|
#### Vscode
|
||||||
|
|
||||||
|
You can select the correct environment in VSCode with the command "Python: Select Interpreter" - which will show you environments the extension detected.
|
||||||
|
If your environment has not been detected, you can also pick a path manually.
|
||||||
|
|
||||||
|
#### Pycharm
|
||||||
|
|
||||||
|
In pycharm, you can select the appropriate Environment in the "Run/Debug Configurations" window.
|
||||||
|

|
||||||
|
|
||||||
!!! Note "Startup directory"
|
!!! Note "Startup directory"
|
||||||
This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository).
|
This assumes that you have the repository checked out, and the editor is started at the repository root level (so setup.py is at the top level of your repository).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr
|
|||||||
This could be caused by the following reasons:
|
This could be caused by the following reasons:
|
||||||
|
|
||||||
* The virtual environment is not active.
|
* The virtual environment is not active.
|
||||||
* Run `source .env/bin/activate` to activate the virtual environment.
|
* Run `source .venv/bin/activate` to activate the virtual environment.
|
||||||
* The installation did not complete successfully.
|
* The installation did not complete successfully.
|
||||||
* Please check the [Installation documentation](installation.md).
|
* Please check the [Installation documentation](installation.md).
|
||||||
|
|
||||||
|
|||||||
@@ -100,12 +100,12 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
|
|||||||
|
|
||||||
#### trainer_kwargs
|
#### trainer_kwargs
|
||||||
|
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|------------|-------------|
|
|--------------|-------------|
|
||||||
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary**
|
||||||
| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs. <br> **Datatype:** int. <br> Default: `100`.
|
| `n_epochs` | The `n_epochs` parameter is a crucial setting in the PyTorch training loop that determines the number of times the entire training dataset will be used to update the model's parameters. An epoch represents one full pass through the entire training dataset. Overrides `n_steps`. Either `n_epochs` or `n_steps` must be set. <br><br> **Datatype:** int. optional. <br> Default: `10`.
|
||||||
| `batch_size` | The size of the batches to use during training.. <br> **Datatype:** int. <br> Default: `64`.
|
| `n_steps` | An alternative way of setting `n_epochs` - the number of training iterations to run. Iteration here refer to the number of times we call `optimizer.step()`. Ignored if `n_epochs` is set. A simplified version of the function: <br><br> n_epochs = n_steps / (n_obs / batch_size) <br><br> The motivation here is that `n_steps` is easier to optimize and keep stable across different n_obs - the number of data points. <br> <br> **Datatype:** int. optional. <br> Default: `None`.
|
||||||
| `max_n_eval_batches` | The maximum number batches to use for evaluation.. <br> **Datatype:** int, optional. <br> Default: `None`.
|
| `batch_size` | The size of the batches to use during training. <br><br> **Datatype:** int. <br> Default: `64`.
|
||||||
|
|
||||||
|
|
||||||
### Additional parameters
|
### Additional parameters
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ With the current framework, we aim to expose the training environment via the co
|
|||||||
|
|
||||||
We envision the majority of users focusing their effort on creative design of the `calculate_reward()` function [details here](#creating-a-custom-reward-function), while leaving the rest of the environment untouched. Other users may not touch the environment at all, and they will only play with the configuration settings and the powerful feature engineering that already exists in FreqAI. Meanwhile, we enable advanced users to create their own model classes entirely.
|
We envision the majority of users focusing their effort on creative design of the `calculate_reward()` function [details here](#creating-a-custom-reward-function), while leaving the rest of the environment untouched. Other users may not touch the environment at all, and they will only play with the configuration settings and the powerful feature engineering that already exists in FreqAI. Meanwhile, we enable advanced users to create their own model classes entirely.
|
||||||
|
|
||||||
The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.env` which means that it is necessary to write an entirely new environment in order to switch to a different library.
|
The framework is built on stable_baselines3 (torch) and OpenAI gym for the base environment class. But generally speaking, the model class is well isolated. Thus, the addition of competing libraries can be easily integrated into the existing framework. For the environment, it is inheriting from `gym.Env` which means that it is necessary to write an entirely new environment in order to switch to a different library.
|
||||||
|
|
||||||
### Important considerations
|
### Important considerations
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class MyCoolRLModel(ReinforcementLearner):
|
|||||||
"""
|
"""
|
||||||
class MyRLEnv(Base5ActionRLEnv):
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
"""
|
"""
|
||||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
User made custom environment. This class inherits from BaseEnvironment and gym.Env.
|
||||||
Users can override any functions from those parent classes. Here is an example
|
Users can override any functions from those parent classes. Here is an example
|
||||||
of a user customized `calculate_reward()` function.
|
of a user customized `calculate_reward()` function.
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
|
|||||||
```python
|
```python
|
||||||
class MyRLEnv(Base5ActionRLEnv):
|
class MyRLEnv(Base5ActionRLEnv):
|
||||||
"""
|
"""
|
||||||
User made custom environment. This class inherits from BaseEnvironment and gym.env.
|
User made custom environment. This class inherits from BaseEnvironment and gym.Env.
|
||||||
Users can override any functions from those parent classes. Here is an example
|
Users can override any functions from those parent classes. Here is an example
|
||||||
of a user customized `calculate_reward()` function.
|
of a user customized `calculate_reward()` function.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ The docker-image includes hyperopt dependencies, no further action needed.
|
|||||||
### Easy installation script (setup.sh) / Manual installation
|
### Easy installation script (setup.sh) / Manual installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements-hyperopt.txt
|
pip install -r requirements-hyperopt.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -143,11 +143,11 @@ If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install
|
|||||||
|
|
||||||
### Activate your virtual environment
|
### Activate your virtual environment
|
||||||
|
|
||||||
Each time you open a new terminal, you must run `source .env/bin/activate` to activate your virtual environment.
|
Each time you open a new terminal, you must run `source .venv/bin/activate` to activate your virtual environment.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# then activate your .env
|
# activate virtual environment
|
||||||
source ./.env/bin/activate
|
source ./.venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Congratulations
|
### Congratulations
|
||||||
@@ -172,7 +172,7 @@ With this option, the script will install the bot and most dependencies:
|
|||||||
You will need to have git and python3.8+ installed beforehand for this to work.
|
You will need to have git and python3.8+ installed beforehand for this to work.
|
||||||
|
|
||||||
* Mandatory software as: `ta-lib`
|
* Mandatory software as: `ta-lib`
|
||||||
* Setup your virtualenv under `.env/`
|
* Setup your virtualenv under `.venv/`
|
||||||
|
|
||||||
This option is a combination of installation tasks and `--reset`
|
This option is a combination of installation tasks and `--reset`
|
||||||
|
|
||||||
@@ -225,11 +225,11 @@ rm -rf ./ta-lib*
|
|||||||
You will run freqtrade in separated `virtual environment`
|
You will run freqtrade in separated `virtual environment`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# create virtualenv in directory /freqtrade/.env
|
# create virtualenv in directory /freqtrade/.venv
|
||||||
python3 -m venv .env
|
python3 -m venv .venv
|
||||||
|
|
||||||
# run virtualenv
|
# run virtualenv
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install python dependencies
|
#### Install python dependencies
|
||||||
@@ -383,7 +383,7 @@ You've made it this far, so you have successfully installed freqtrade.
|
|||||||
freqtrade create-userdir --userdir user_data
|
freqtrade create-userdir --userdir user_data
|
||||||
|
|
||||||
# Step 2 - Create a new configuration file
|
# Step 2 - Create a new configuration file
|
||||||
freqtrade new-config --config config.json
|
freqtrade new-config --config user_data/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
You are ready to run, read [Bot Configuration](configuration.md), remember to start with `dry_run: True` and verify that everything is working.
|
You are ready to run, read [Bot Configuration](configuration.md), remember to start with `dry_run: True` and verify that everything is working.
|
||||||
@@ -393,7 +393,7 @@ To learn how to setup your configuration, please refer to the [Bot Configuration
|
|||||||
### Start the Bot
|
### Start the Bot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade trade --config config.json --strategy SampleStrategy
|
freqtrade trade --config user_data/config.json --strategy SampleStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
@@ -411,8 +411,8 @@ If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in
|
|||||||
# if:
|
# if:
|
||||||
bash: freqtrade: command not found
|
bash: freqtrade: command not found
|
||||||
|
|
||||||
# then activate your .env
|
# then activate your virtual environment
|
||||||
source ./.env/bin/activate
|
source ./.venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### MacOS installation error
|
### MacOS installation error
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
|||||||
|
|
||||||
##### Pair namings
|
##### Pair namings
|
||||||
|
|
||||||
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future).
|
Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/#/README?id=perpetual-swap-perpetual-future).
|
||||||
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
### Margin mode
|
### Margin mode
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ It also supports the lookahead-analysis of freqai strategies.
|
|||||||
|
|
||||||
- `--cache` is forced to "none".
|
- `--cache` is forced to "none".
|
||||||
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
- `--max-open-trades` is forced to be at least equal to the number of pairs.
|
||||||
- `--dry-run-wallet` is forced to be basically infinite.
|
- `--dry-run-wallet` is forced to be basically infinite (1 billion).
|
||||||
|
- `--stake-amount` is forced to be a static 10000 (10k).
|
||||||
|
|
||||||
|
Those are set to avoid users accidentally generating false positives.
|
||||||
|
|
||||||
## Lookahead-analysis command reference
|
## Lookahead-analysis command reference
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.4.4
|
markdown==3.4.4
|
||||||
mkdocs==1.5.2
|
mkdocs==1.5.2
|
||||||
mkdocs-material==9.1.21
|
mkdocs-material==9.2.1
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.1
|
pymdown-extensions==10.1
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
# Sandbox API testing
|
|
||||||
|
|
||||||
Some exchanges provide sandboxes or testbeds for risk-free testing, while running the bot against a real exchange.
|
|
||||||
With some configuration, freqtrade (in combination with ccxt) provides access to these.
|
|
||||||
|
|
||||||
This document is an overview to configure Freqtrade to be used with sandboxes.
|
|
||||||
This can be useful to developers and trader alike.
|
|
||||||
|
|
||||||
!!! Warning
|
|
||||||
Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all.
|
|
||||||
Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading.
|
|
||||||
|
|
||||||
## Exchanges known to have a sandbox / testnet
|
|
||||||
|
|
||||||
* [binance](https://testnet.binance.vision/)
|
|
||||||
* [coinbasepro](https://public.sandbox.pro.coinbase.com)
|
|
||||||
* [gemini](https://exchange.sandbox.gemini.com/)
|
|
||||||
* [huobipro](https://www.testnet.huobi.pro/)
|
|
||||||
* [kucoin](https://sandbox.kucoin.com/)
|
|
||||||
* [phemex](https://testnet.phemex.com/)
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
We did not test correct functioning of all of the above testnets. Please report your experiences with each sandbox.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configure a Sandbox account
|
|
||||||
|
|
||||||
When testing your API connectivity, make sure to use the appropriate sandbox / testnet URL.
|
|
||||||
|
|
||||||
In general, you should follow these steps to enable an exchange's sandbox:
|
|
||||||
|
|
||||||
* Figure out if an exchange has a sandbox (most likely by using google or the exchange's support documents)
|
|
||||||
* Create a sandbox account (often the sandbox-account requires separate registration)
|
|
||||||
* [Add some test assets to account](#add-test-funds)
|
|
||||||
* Create API keys
|
|
||||||
|
|
||||||
### Add test funds
|
|
||||||
|
|
||||||
Usually, sandbox exchanges allow depositing funds directly via web-interface.
|
|
||||||
You should make sure to have a realistic amount of funds available to your test-account, so results are representable of your real account funds.
|
|
||||||
|
|
||||||
!!! Warning
|
|
||||||
Test exchanges will **NEVER** require your real credit card or banking details!
|
|
||||||
|
|
||||||
## Configure freqtrade to use a exchange's sandbox
|
|
||||||
|
|
||||||
### Sandbox URLs
|
|
||||||
|
|
||||||
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
|
|
||||||
These include `['test']` and `['api']`.
|
|
||||||
|
|
||||||
* `[Test]` if available will point to an Exchanges sandbox.
|
|
||||||
* `[Api]` normally used, and resolves to live API target on the exchange.
|
|
||||||
|
|
||||||
To make use of sandbox / test add "sandbox": true, to your config.json
|
|
||||||
|
|
||||||
```json
|
|
||||||
"exchange": {
|
|
||||||
"name": "coinbasepro",
|
|
||||||
"sandbox": true,
|
|
||||||
"key": "5wowfxemogxeowo;heiohgmd",
|
|
||||||
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
|
||||||
"password": "1bkjfkhfhfu6sr",
|
|
||||||
"outdated_offset": 5
|
|
||||||
"pair_whitelist": [
|
|
||||||
"BTC/USD"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"datadir": "user_data/data/coinbasepro_sandbox"
|
|
||||||
```
|
|
||||||
|
|
||||||
Also the following information:
|
|
||||||
|
|
||||||
* api-key (created for the sandbox webpage)
|
|
||||||
* api-secret (noted earlier)
|
|
||||||
* password (the passphrase - noted earlier)
|
|
||||||
|
|
||||||
!!! Tip "Different data directory"
|
|
||||||
We also recommend to set `datadir` to something identifying downloaded data as sandbox data, to avoid having sandbox data mixed with data from the real exchange.
|
|
||||||
This can be done by adding the `"datadir"` key to the configuration.
|
|
||||||
Now, whenever you use this configuration, your data directory will be set to this directory.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## You should now be ready to test your sandbox
|
|
||||||
|
|
||||||
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. Also make sure to select a pair which shows at least some decent value (which very often is BTC/<somestablecoin>).
|
|
||||||
|
|
||||||
## Common problems with sandbox exchanges
|
|
||||||
|
|
||||||
Sandbox exchange instances often have very low volume, which can cause some problems which usually are not seen on a real exchange instance.
|
|
||||||
|
|
||||||
### Old Candles problem
|
|
||||||
|
|
||||||
Since Sandboxes often have low volume, candles can be quite old and show no volume.
|
|
||||||
To disable the error "Outdated history for pair ...", best increase the parameter `"outdated_offset"` to a number that seems realistic for the sandbox you're using.
|
|
||||||
|
|
||||||
### Unfilled orders
|
|
||||||
|
|
||||||
Sandboxes often have very low volumes - which means that many trades can go unfilled, or can go unfilled for a very long time.
|
|
||||||
|
|
||||||
To mitigate this, you can try to match the first order on the opposite orderbook side using the following configuration:
|
|
||||||
|
|
||||||
``` jsonc
|
|
||||||
"order_types": {
|
|
||||||
"entry": "limit",
|
|
||||||
"exit": "limit"
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
"entry_pricing": {
|
|
||||||
"price_side": "other",
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
"exit_pricing":{
|
|
||||||
"price_side": "other",
|
|
||||||
// ...
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
The configuration is similar to the suggested configuration for market orders - however by using limit-orders you can avoid moving the price too much, and you can set the worst price you might get.
|
|
||||||
@@ -31,8 +31,8 @@ Other versions must be downloaded from the above link.
|
|||||||
|
|
||||||
``` powershell
|
``` powershell
|
||||||
cd \path\freqtrade
|
cd \path\freqtrade
|
||||||
python -m venv .env
|
python -m venv .venv
|
||||||
.env\Scripts\activate.ps1
|
.venv\Scripts\activate.ps1
|
||||||
# optionally install ta-lib from wheel
|
# optionally install ta-lib from wheel
|
||||||
# Eventually adjust the below filename to match the downloaded wheel
|
# Eventually adjust the below filename to match the downloaded wheel
|
||||||
pip install --find-links build_helpers\ TA-Lib -U
|
pip install --find-links build_helpers\ TA-Lib -U
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from freqtrade.configuration.directory_operations import chown_user_directory
|
|||||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
|
from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges
|
||||||
from freqtrade.misc import render_template
|
from freqtrade.util import render_template
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -105,7 +105,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"type": "select",
|
"type": "select",
|
||||||
"name": "exchange_name",
|
"name": "exchange_name",
|
||||||
"message": "Select exchange",
|
"message": "Select exchange",
|
||||||
"choices": lambda x: [
|
"choices": [
|
||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
"dataformat_trades": Arg(
|
"dataformat_trades": Arg(
|
||||||
'--data-format-trades',
|
'--data-format-trades',
|
||||||
help='Storage format for downloaded trades data. (default: `feather`).',
|
help='Storage format for downloaded trades data. (default: `feather`).',
|
||||||
choices=constants.AVAILABLE_DATAHANDLERS_TRADES,
|
choices=constants.AVAILABLE_DATAHANDLERS,
|
||||||
),
|
),
|
||||||
"show_timerange": Arg(
|
"show_timerange": Arg(
|
||||||
'--show-timerange',
|
'--show-timerange',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from freqtrade.configuration.directory_operations import copy_sample_files, crea
|
|||||||
from freqtrade.constants import USERPATH_STRATEGIES
|
from freqtrade.constants import USERPATH_STRATEGIES
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import render_template, render_template_with_fallback
|
from freqtrade.util import render_template, render_template_with_fallback
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -35,6 +35,10 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
|||||||
Deploy new strategy from template to strategy_path
|
Deploy new strategy from template to strategy_path
|
||||||
"""
|
"""
|
||||||
fallback = 'full'
|
fallback = 'full'
|
||||||
|
attributes = render_template_with_fallback(
|
||||||
|
templatefile=f"strategy_subtemplates/strategy_attributes_{subtemplate}.j2",
|
||||||
|
templatefallbackfile=f"strategy_subtemplates/strategy_attributes_{fallback}.j2",
|
||||||
|
)
|
||||||
indicators = render_template_with_fallback(
|
indicators = render_template_with_fallback(
|
||||||
templatefile=f"strategy_subtemplates/indicators_{subtemplate}.j2",
|
templatefile=f"strategy_subtemplates/indicators_{subtemplate}.j2",
|
||||||
templatefallbackfile=f"strategy_subtemplates/indicators_{fallback}.j2",
|
templatefallbackfile=f"strategy_subtemplates/indicators_{fallback}.j2",
|
||||||
@@ -58,6 +62,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
|
|||||||
|
|
||||||
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
strategy_text = render_template(templatefile='base_strategy.py.j2',
|
||||||
arguments={"strategy": strategy_name,
|
arguments={"strategy": strategy_name,
|
||||||
|
"attributes": attributes,
|
||||||
"indicators": indicators,
|
"indicators": indicators,
|
||||||
"buy_trend": buy_trend,
|
"buy_trend": buy_trend,
|
||||||
"sell_trend": sell_trend,
|
"sell_trend": sell_trend,
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ def start_webserver(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Main entry point for webserver mode
|
Main entry point for webserver mode
|
||||||
"""
|
"""
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.rpc.api_server import ApiServer
|
from freqtrade.rpc.api_server import ApiServer
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
config = Configuration(args, RunMode.WEBSERVER).get_config()
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.WEBSERVER)
|
||||||
ApiServer(config, standalone=True)
|
ApiServer(config, standalone=True)
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ def validate_config_schema(conf: Dict[str, Any], preliminary: bool = False) -> D
|
|||||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
|
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED_FINAL
|
||||||
|
elif conf.get('runmode', RunMode.OTHER) == RunMode.WEBSERVER:
|
||||||
|
conf_schema['required'] = constants.SCHEMA_MINIMAL_WEBSERVER
|
||||||
else:
|
else:
|
||||||
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
|
|||||||
key = env_var.replace(prefix, '')
|
key = env_var.replace(prefix, '')
|
||||||
for k in reversed(key.split('__')):
|
for k in reversed(key.split('__')):
|
||||||
val = {k.lower(): get_var_typed(val)
|
val = {k.lower(): get_var_typed(val)
|
||||||
if type(val) != dict and k not in no_convert else val}
|
if not isinstance(val, dict) and k not in no_convert else val}
|
||||||
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
relevant_vars = deep_merge_dicts(val, relevant_vars)
|
||||||
return relevant_vars
|
return relevant_vars
|
||||||
|
|
||||||
|
|||||||
+14
-4
@@ -38,8 +38,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', '
|
|||||||
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
|
||||||
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
|
||||||
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
|
||||||
AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather']
|
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5', 'feather', 'parquet']
|
||||||
AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet']
|
|
||||||
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
BACKTEST_BREAKDOWNS = ['day', 'week', 'month']
|
||||||
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month']
|
||||||
BACKTEST_CACHE_DEFAULT = 'day'
|
BACKTEST_CACHE_DEFAULT = 'day'
|
||||||
@@ -50,6 +49,15 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|||||||
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
||||||
# it has wide consequences for stored trades files
|
# it has wide consequences for stored trades files
|
||||||
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
||||||
|
TRADES_DTYPES = {
|
||||||
|
'timestamp': 'int64',
|
||||||
|
'id': 'str',
|
||||||
|
'type': 'str',
|
||||||
|
'side': 'str',
|
||||||
|
'price': 'float64',
|
||||||
|
'amount': 'float64',
|
||||||
|
'cost': 'float64',
|
||||||
|
}
|
||||||
TRADING_MODES = ['spot', 'margin', 'futures']
|
TRADING_MODES = ['spot', 'margin', 'futures']
|
||||||
MARGIN_MODES = ['cross', 'isolated', '']
|
MARGIN_MODES = ['cross', 'isolated', '']
|
||||||
|
|
||||||
@@ -450,7 +458,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'dataformat_trades': {
|
'dataformat_trades': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': AVAILABLE_DATAHANDLERS_TRADES,
|
'enum': AVAILABLE_DATAHANDLERS,
|
||||||
'default': 'feather'
|
'default': 'feather'
|
||||||
},
|
},
|
||||||
'position_adjustment_enable': {'type': 'boolean'},
|
'position_adjustment_enable': {'type': 'boolean'},
|
||||||
@@ -461,7 +469,6 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'name': {'type': 'string'},
|
'name': {'type': 'string'},
|
||||||
'sandbox': {'type': 'boolean', 'default': False},
|
|
||||||
'key': {'type': 'string', 'default': ''},
|
'key': {'type': 'string', 'default': ''},
|
||||||
'secret': {'type': 'string', 'default': ''},
|
'secret': {'type': 'string', 'default': ''},
|
||||||
'password': {'type': 'string', 'default': ''},
|
'password': {'type': 'string', 'default': ''},
|
||||||
@@ -668,6 +675,9 @@ SCHEMA_MINIMAL_REQUIRED = [
|
|||||||
'dataformat_ohlcv',
|
'dataformat_ohlcv',
|
||||||
'dataformat_trades',
|
'dataformat_trades',
|
||||||
]
|
]
|
||||||
|
SCHEMA_MINIMAL_WEBSERVER = SCHEMA_MINIMAL_REQUIRED + [
|
||||||
|
'api_server',
|
||||||
|
]
|
||||||
|
|
||||||
CANCEL_REASON = {
|
CANCEL_REASON = {
|
||||||
"TIMEOUT": "cancelled due to timeout",
|
"TIMEOUT": "cancelled due to timeout",
|
||||||
|
|||||||
+36
-16
@@ -1,16 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Functions to convert data from one format to another
|
Functions to convert data from one format to another
|
||||||
"""
|
"""
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
from operator import itemgetter
|
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, Config, TradeList
|
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES,
|
||||||
|
Config, TradeList)
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
|
|
||||||
|
|
||||||
@@ -195,15 +194,14 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
|||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
def trades_remove_duplicates(trades: List[List]) -> List[List]:
|
def trades_df_remove_duplicates(trades: pd.DataFrame) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Removes duplicates from the trades list.
|
Removes duplicates from the trades DataFrame.
|
||||||
Uses itertools.groupby to avoid converting to pandas.
|
Uses pandas.DataFrame.drop_duplicates to remove duplicates based on the 'timestamp' column.
|
||||||
Tests show it as being pretty efficient on lists of 4M Lists.
|
:param trades: DataFrame with the columns constants.DEFAULT_TRADES_COLUMNS
|
||||||
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
:return: DataFrame with duplicates removed based on the 'timestamp' column
|
||||||
:return: same format as above, but with duplicates removed
|
|
||||||
"""
|
"""
|
||||||
return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))]
|
return trades.drop_duplicates(subset=['timestamp', 'id'])
|
||||||
|
|
||||||
|
|
||||||
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
||||||
@@ -215,7 +213,32 @@ def trades_dict_to_list(trades: List[Dict]) -> TradeList:
|
|||||||
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
|
return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades]
|
||||||
|
|
||||||
|
|
||||||
def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame:
|
def trades_convert_types(trades: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Convert Trades dtypes and add 'date' column
|
||||||
|
"""
|
||||||
|
trades = trades.astype(TRADES_DTYPES)
|
||||||
|
trades['date'] = to_datetime(trades['timestamp'], unit='ms', utc=True)
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
|
def trades_list_to_df(trades: TradeList, convert: bool = True):
|
||||||
|
"""
|
||||||
|
convert trades list to dataframe
|
||||||
|
:param trades: List of Lists with constants.DEFAULT_TRADES_COLUMNS as columns
|
||||||
|
"""
|
||||||
|
if not trades:
|
||||||
|
df = DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
else:
|
||||||
|
df = DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
|
if convert:
|
||||||
|
df = trades_convert_types(df)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def trades_to_ohlcv(trades: DataFrame, timeframe: str) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Converts trades list to OHLCV list
|
Converts trades list to OHLCV list
|
||||||
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
:param trades: List of trades, as returned by ccxt.fetch_trades.
|
||||||
@@ -225,12 +248,9 @@ def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame:
|
|||||||
"""
|
"""
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
timeframe_minutes = timeframe_to_minutes(timeframe)
|
timeframe_minutes = timeframe_to_minutes(timeframe)
|
||||||
if not trades:
|
if trades.empty:
|
||||||
raise ValueError('Trade-list empty.')
|
raise ValueError('Trade-list empty.')
|
||||||
df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS)
|
df = trades.set_index('date', drop=True)
|
||||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms',
|
|
||||||
utc=True,)
|
|
||||||
df = df.set_index('timestamp')
|
|
||||||
|
|
||||||
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc()
|
||||||
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWith
|
|||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
from freqtrade.enums import CandleType, RPCMessageType, RunMode
|
||||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||||
from freqtrade.exchange import Exchange, timeframe_to_seconds
|
from freqtrade.exchange import Exchange, timeframe_to_prev_date, timeframe_to_seconds
|
||||||
from freqtrade.exchange.types import OrderBook
|
from freqtrade.exchange.types import OrderBook
|
||||||
from freqtrade.misc import append_candles_to_dataframe
|
from freqtrade.misc import append_candles_to_dataframe
|
||||||
from freqtrade.rpc import RPCManager
|
from freqtrade.rpc import RPCManager
|
||||||
@@ -46,6 +46,8 @@ class DataProvider:
|
|||||||
self.__rpc = rpc
|
self.__rpc = rpc
|
||||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||||
self.__slice_index: Optional[int] = None
|
self.__slice_index: Optional[int] = None
|
||||||
|
self.__slice_date: Optional[datetime] = None
|
||||||
|
|
||||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
self.__producer_pairs_df: Dict[str,
|
self.__producer_pairs_df: Dict[str,
|
||||||
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
Dict[PairWithTimeframe, Tuple[DataFrame, datetime]]] = {}
|
||||||
@@ -64,10 +66,19 @@ class DataProvider:
|
|||||||
def _set_dataframe_max_index(self, limit_index: int):
|
def _set_dataframe_max_index(self, limit_index: int):
|
||||||
"""
|
"""
|
||||||
Limit analyzed dataframe to max specified index.
|
Limit analyzed dataframe to max specified index.
|
||||||
|
Only relevant in backtesting.
|
||||||
:param limit_index: dataframe index.
|
:param limit_index: dataframe index.
|
||||||
"""
|
"""
|
||||||
self.__slice_index = limit_index
|
self.__slice_index = limit_index
|
||||||
|
|
||||||
|
def _set_dataframe_max_date(self, limit_date: datetime):
|
||||||
|
"""
|
||||||
|
Limit infomrative dataframe to max specified index.
|
||||||
|
Only relevant in backtesting.
|
||||||
|
:param limit_date: "current date"
|
||||||
|
"""
|
||||||
|
self.__slice_date = limit_date
|
||||||
|
|
||||||
def _set_cached_df(
|
def _set_cached_df(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@@ -284,7 +295,7 @@ class DataProvider:
|
|||||||
def historic_ohlcv(
|
def historic_ohlcv(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
timeframe: Optional[str] = None,
|
timeframe: str,
|
||||||
candle_type: str = ''
|
candle_type: str = ''
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@@ -307,7 +318,7 @@ class DataProvider:
|
|||||||
timerange.subtract_start(tf_seconds * startup_candles)
|
timerange.subtract_start(tf_seconds * startup_candles)
|
||||||
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
self.__cached_pairs_backtesting[saved_pair] = load_pair_history(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
timeframe=timeframe or self._config['timeframe'],
|
timeframe=timeframe,
|
||||||
datadir=self._config['datadir'],
|
datadir=self._config['datadir'],
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self._config['dataformat_ohlcv'],
|
data_format=self._config['dataformat_ohlcv'],
|
||||||
@@ -354,7 +365,13 @@ class DataProvider:
|
|||||||
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
else:
|
else:
|
||||||
# Get historical OHLCV data (cached on disk).
|
# Get historical OHLCV data (cached on disk).
|
||||||
|
timeframe = timeframe or self._config['timeframe']
|
||||||
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||||
|
# Cut date to timeframe-specific date.
|
||||||
|
# This is necessary to prevent lookahead bias in callbacks through informative pairs.
|
||||||
|
if self.__slice_date:
|
||||||
|
cutoff_date = timeframe_to_prev_date(timeframe, self.__slice_date)
|
||||||
|
data = data.loc[data['date'] < cutoff_date]
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_feather, to_datetime
|
from pandas import DataFrame, read_feather, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -82,43 +82,41 @@ class FeatherDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Store trades data (list of Dicts) to file
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
self.create_dir_if_needed(filename)
|
self.create_dir_if_needed(filename)
|
||||||
|
data.reset_index(drop=True).to_feather(filename, compression_level=9, compression='lz4')
|
||||||
|
|
||||||
tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS)
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
tradesdata.to_feather(filename, compression_level=9, compression='lz4')
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
# TODO: respect timerange ...
|
# TODO: respect timerange ...
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: Dataframe containing trades
|
||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return []
|
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
tradesdata = read_feather(filename)
|
tradesdata = read_feather(filename)
|
||||||
|
|
||||||
return tradesdata.values.tolist()
|
return tradesdata
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -100,42 +100,42 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def _trades_store(self, pair: str, data: pd.DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Store trades data (list of Dicts) to file
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
|
|
||||||
pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf(
|
data.to_hdf(
|
||||||
self._pair_trades_filename(self._datadir, pair), key,
|
self._pair_trades_filename(self._datadir, pair), key,
|
||||||
mode='a', complevel=9, complib='blosc',
|
mode='a', complevel=9, complib='blosc',
|
||||||
format='table', data_columns=['timestamp']
|
format='table', data_columns=['timestamp']
|
||||||
)
|
)
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: pd.DataFrame):
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from h5 file.
|
Load a pair from h5 file.
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: Dataframe containing trades
|
||||||
"""
|
"""
|
||||||
key = self._pair_trades_key(pair)
|
key = self._pair_trades_key(pair)
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
|
||||||
if not filename.exists():
|
if not filename.exists():
|
||||||
return []
|
return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
where = []
|
where = []
|
||||||
if timerange:
|
if timerange:
|
||||||
if timerange.starttype == 'date':
|
if timerange.starttype == 'date':
|
||||||
@@ -145,7 +145,7 @@ class HDF5DataHandler(IDataHandler):
|
|||||||
|
|
||||||
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where)
|
||||||
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None})
|
||||||
return trades.values.tolist()
|
return trades
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ from freqtrade.configuration import TimeRange
|
|||||||
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMNS,
|
||||||
DL_DATA_TIMEFRAMES, Config)
|
DL_DATA_TIMEFRAMES, Config)
|
||||||
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
|
||||||
trades_remove_duplicates, trades_to_ohlcv)
|
trades_df_remove_duplicates, trades_list_to_df,
|
||||||
|
trades_to_ohlcv)
|
||||||
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
from freqtrade.util import format_ms_time
|
from freqtrade.util import dt_ts, format_ms_time
|
||||||
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
from freqtrade.util.binance_mig import migrate_binance_futures_data
|
||||||
|
from freqtrade.util.datetime_helpers import dt_now
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -349,24 +351,27 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
|
|
||||||
if trades and since < trades[0][0]:
|
if not trades.empty and since > 0 and since < trades.iloc[0]['timestamp']:
|
||||||
# since is before the first trade
|
# since is before the first trade
|
||||||
logger.info(f"Start earlier than available data. Redownloading trades for {pair}...")
|
logger.info(f"Start ({trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}) earlier than "
|
||||||
trades = []
|
f"available data. Redownloading trades for {pair}...")
|
||||||
|
trades = trades_list_to_df([])
|
||||||
|
|
||||||
if not since:
|
from_id = trades.iloc[-1]['id'] if not trades.empty else None
|
||||||
since = int((datetime.now() - timedelta(days=new_pairs_days)).timestamp()) * 1000
|
if not trades.empty and since < trades.iloc[-1]['timestamp']:
|
||||||
|
|
||||||
from_id = trades[-1][1] if trades else None
|
|
||||||
if trades and since < trades[-1][0]:
|
|
||||||
# Reset since to the last available point
|
# Reset since to the last available point
|
||||||
# - 5 seconds (to ensure we're getting all trades)
|
# - 5 seconds (to ensure we're getting all trades)
|
||||||
since = trades[-1][0] - (5 * 1000)
|
since = trades.iloc[-1]['timestamp'] - (5 * 1000)
|
||||||
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
|
logger.info(f"Using last trade date -5s - Downloading trades for {pair} "
|
||||||
f"since: {format_ms_time(since)}.")
|
f"since: {format_ms_time(since)}.")
|
||||||
|
|
||||||
logger.debug(f"Current Start: {format_ms_time(trades[0][0]) if trades else 'None'}")
|
if not since:
|
||||||
logger.debug(f"Current End: {format_ms_time(trades[-1][0]) if trades else 'None'}")
|
since = dt_ts(dt_now() - timedelta(days=new_pairs_days))
|
||||||
|
|
||||||
|
logger.debug("Current Start: %s", 'None' if trades.empty else
|
||||||
|
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||||
|
logger.debug("Current End: %s", 'None' if trades.empty else
|
||||||
|
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||||
logger.info(f"Current Amount of trades: {len(trades)}")
|
logger.info(f"Current Amount of trades: {len(trades)}")
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
# Default since_ms to 30 days if nothing is given
|
||||||
@@ -375,13 +380,16 @@ def _download_trades_history(exchange: Exchange,
|
|||||||
until=until,
|
until=until,
|
||||||
from_id=from_id,
|
from_id=from_id,
|
||||||
)
|
)
|
||||||
trades.extend(new_trades[1])
|
new_trades_df = trades_list_to_df(new_trades[1])
|
||||||
|
trades = concat([trades, new_trades_df], axis=0)
|
||||||
# Remove duplicates to make sure we're not storing data we don't need
|
# Remove duplicates to make sure we're not storing data we don't need
|
||||||
trades = trades_remove_duplicates(trades)
|
trades = trades_df_remove_duplicates(trades)
|
||||||
data_handler.trades_store(pair, data=trades)
|
data_handler.trades_store(pair, data=trades)
|
||||||
|
|
||||||
logger.debug(f"New Start: {format_ms_time(trades[0][0])}")
|
logger.debug("New Start: %s", 'None' if trades.empty else
|
||||||
logger.debug(f"New End: {format_ms_time(trades[-1][0])}")
|
f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||||
|
logger.debug("New End: %s", 'None' if trades.empty else
|
||||||
|
f"{trades.iloc[-1]['date']:{DATETIME_PRINT_FORMAT}}")
|
||||||
logger.info(f"New Amount of trades: {len(trades)}")
|
logger.info(f"New Amount of trades: {len(trades)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, TradeList
|
from freqtrade.constants import DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes
|
||||||
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
|
from freqtrade.data.converter import (clean_ohlcv_dataframe, trades_convert_types,
|
||||||
|
trades_df_remove_duplicates, trim_dataframe)
|
||||||
from freqtrade.enums import CandleType, TradingMode
|
from freqtrade.enums import CandleType, TradingMode
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
|
|
||||||
@@ -170,32 +171,42 @@ class IDataHandler(ABC):
|
|||||||
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Store trades data (list of Dicts) to file
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: Dataframe containing trades
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def trades_store(self, pair: str, data: DataFrame) -> None:
|
||||||
|
"""
|
||||||
|
Store trades data (list of Dicts) to file
|
||||||
|
:param pair: Pair - used for filename
|
||||||
|
:param data: Dataframe containing trades
|
||||||
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
|
"""
|
||||||
|
# Filter on expected columns (will remove the actual date column).
|
||||||
|
self._trades_store(pair, data[DEFAULT_TRADES_COLUMNS])
|
||||||
|
|
||||||
def trades_purge(self, pair: str) -> bool:
|
def trades_purge(self, pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Remove data for this pair
|
Remove data for this pair
|
||||||
@@ -208,7 +219,7 @@ class IDataHandler(ABC):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
Removes duplicates in the process.
|
Removes duplicates in the process.
|
||||||
@@ -216,7 +227,10 @@ class IDataHandler(ABC):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
trades = trades_df_remove_duplicates(self._trades_load(pair, timerange=timerange))
|
||||||
|
|
||||||
|
trades = trades_convert_types(trades)
|
||||||
|
return trades
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_dir_if_needed(cls, datadir: Path):
|
def create_dir_if_needed(cls, datadir: Path):
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from pandas import DataFrame, read_json, to_datetime
|
|||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
|
||||||
from freqtrade.data.converter import trades_dict_to_list
|
from freqtrade.data.converter import trades_dict_to_list, trades_list_to_df
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -94,45 +94,46 @@ class JsonDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Store trades data (list of Dicts) to file
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
misc.file_dump_json(filename, data, is_zip=self._use_zip)
|
trades = data.values.tolist()
|
||||||
|
misc.file_dump_json(filename, trades, is_zip=self._use_zip)
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
|
def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Load a pair from file, either .json.gz or .json
|
Load a pair from file, either .json.gz or .json
|
||||||
# TODO: respect timerange ...
|
# TODO: respect timerange ...
|
||||||
:param pair: Load trades for this pair
|
:param pair: Load trades for this pair
|
||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: Dataframe containing trades
|
||||||
"""
|
"""
|
||||||
filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
tradesdata = misc.file_load_json(filename)
|
tradesdata = misc.file_load_json(filename)
|
||||||
|
|
||||||
if not tradesdata:
|
if not tradesdata:
|
||||||
return []
|
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
if isinstance(tradesdata[0], dict):
|
if isinstance(tradesdata[0], dict):
|
||||||
# Convert trades dict to list
|
# Convert trades dict to list
|
||||||
logger.info("Old trades format detected - converting")
|
logger.info("Old trades format detected - converting")
|
||||||
tradesdata = trades_dict_to_list(tradesdata)
|
tradesdata = trades_dict_to_list(tradesdata)
|
||||||
pass
|
pass
|
||||||
return tradesdata
|
return trades_list_to_df(tradesdata, convert=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pandas import DataFrame, read_parquet, to_datetime
|
from pandas import DataFrame, read_parquet, to_datetime
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
from .idatahandler import IDataHandler
|
from .idatahandler import IDataHandler
|
||||||
@@ -81,25 +81,22 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def trades_store(self, pair: str, data: TradeList) -> None:
|
def _trades_store(self, pair: str, data: DataFrame) -> None:
|
||||||
"""
|
"""
|
||||||
Store trades data (list of Dicts) to file
|
Store trades data (list of Dicts) to file
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
|
self.create_dir_if_needed(filename)
|
||||||
|
data.reset_index(drop=True).to_parquet(filename)
|
||||||
|
|
||||||
raise NotImplementedError()
|
def trades_append(self, pair: str, data: DataFrame):
|
||||||
# array = pa.array(data)
|
|
||||||
# array
|
|
||||||
# feather.write_feather(data, filename)
|
|
||||||
|
|
||||||
def trades_append(self, pair: str, data: TradeList):
|
|
||||||
"""
|
"""
|
||||||
Append data to existing files
|
Append data to existing files
|
||||||
:param pair: Pair - used for filename
|
:param pair: Pair - used for filename
|
||||||
:param data: List of Lists containing trade data,
|
:param data: Dataframe containing trades
|
||||||
column sequence as in DEFAULT_TRADES_COLUMNS
|
column sequence as in DEFAULT_TRADES_COLUMNS
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -112,14 +109,13 @@ class ParquetDataHandler(IDataHandler):
|
|||||||
:param timerange: Timerange to load trades for - currently not implemented
|
:param timerange: Timerange to load trades for - currently not implemented
|
||||||
:return: List of trades
|
:return: List of trades
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
filename = self._pair_trades_filename(self._datadir, pair)
|
||||||
# filename = self._pair_trades_filename(self._datadir, pair)
|
if not filename.exists():
|
||||||
# tradesdata = misc.file_load_json(filename)
|
return DataFrame(columns=DEFAULT_TRADES_COLUMNS)
|
||||||
|
|
||||||
# if not tradesdata:
|
tradesdata = read_parquet(filename)
|
||||||
# return []
|
|
||||||
|
|
||||||
# return tradesdata
|
return tradesdata
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_extension(cls):
|
def _get_file_extension(cls):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ Cryptocurrency Exchanges support
|
|||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
import signal
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import floor
|
from math import floor
|
||||||
@@ -263,8 +264,6 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
|
||||||
|
|
||||||
self.set_sandbox(api, exchange_config, name)
|
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -465,16 +464,6 @@ class Exchange:
|
|||||||
return amount_to_contract_precision(amount, self.get_precision_amount(pair),
|
return amount_to_contract_precision(amount, self.get_precision_amount(pair),
|
||||||
self.precisionMode, contract_size)
|
self.precisionMode, contract_size)
|
||||||
|
|
||||||
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
|
||||||
if exchange_config.get('sandbox'):
|
|
||||||
if api.urls.get('test'):
|
|
||||||
api.urls['api'] = api.urls['test']
|
|
||||||
logger.info("Enabled Sandbox API on %s", name)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json")
|
|
||||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
|
||||||
|
|
||||||
def _load_async_markets(self, reload: bool = False) -> None:
|
def _load_async_markets(self, reload: bool = False) -> None:
|
||||||
try:
|
try:
|
||||||
if self._api_async:
|
if self._api_async:
|
||||||
@@ -580,7 +569,7 @@ class Exchange:
|
|||||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||||
if pair in self.markets and self.markets[pair].get('active'):
|
if pair in self.markets and self.markets[pair].get('active'):
|
||||||
return pair
|
return pair
|
||||||
raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||||
|
|
||||||
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -1876,7 +1865,7 @@ class Exchange:
|
|||||||
tick = self.fetch_ticker(comb)
|
tick = self.fetch_ticker(comb)
|
||||||
|
|
||||||
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
|
||||||
except ExchangeError:
|
except (ValueError, ExchangeError):
|
||||||
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
|
||||||
if not fee_to_quote_rate:
|
if not fee_to_quote_rate:
|
||||||
return None
|
return None
|
||||||
@@ -2265,20 +2254,24 @@ class Exchange:
|
|||||||
from_id = t[-1][1]
|
from_id = t[-1][1]
|
||||||
trades.extend(t[:-1])
|
trades.extend(t[:-1])
|
||||||
while True:
|
while True:
|
||||||
t = await self._async_fetch_trades(pair,
|
try:
|
||||||
params={self._trades_pagination_arg: from_id})
|
t = await self._async_fetch_trades(pair,
|
||||||
if t:
|
params={self._trades_pagination_arg: from_id})
|
||||||
# Skip last id since its the key for the next call
|
if t:
|
||||||
trades.extend(t[:-1])
|
# Skip last id since its the key for the next call
|
||||||
if from_id == t[-1][1] or t[-1][0] > until:
|
trades.extend(t[:-1])
|
||||||
logger.debug(f"Stopping because from_id did not change. "
|
if from_id == t[-1][1] or t[-1][0] > until:
|
||||||
f"Reached {t[-1][0]} > {until}")
|
logger.debug(f"Stopping because from_id did not change. "
|
||||||
# Reached the end of the defined-download period - add last trade as well.
|
f"Reached {t[-1][0]} > {until}")
|
||||||
trades.extend(t[-1:])
|
# Reached the end of the defined-download period - add last trade as well.
|
||||||
break
|
trades.extend(t[-1:])
|
||||||
|
break
|
||||||
|
|
||||||
from_id = t[-1][1]
|
from_id = t[-1][1]
|
||||||
else:
|
else:
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
break
|
break
|
||||||
|
|
||||||
return (pair, trades)
|
return (pair, trades)
|
||||||
@@ -2298,16 +2291,20 @@ class Exchange:
|
|||||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||||
while True:
|
while True:
|
||||||
t = await self._async_fetch_trades(pair, since=since)
|
try:
|
||||||
if t:
|
t = await self._async_fetch_trades(pair, since=since)
|
||||||
since = t[-1][0]
|
if t:
|
||||||
trades.extend(t)
|
since = t[-1][0]
|
||||||
# Reached the end of the defined-download period
|
trades.extend(t)
|
||||||
if until and t[-1][0] > until:
|
# Reached the end of the defined-download period
|
||||||
logger.debug(
|
if until and t[-1][0] > until:
|
||||||
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
logger.debug(
|
||||||
|
f"Stopping because until was reached. {t[-1][0]} > {until}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
break
|
break
|
||||||
else:
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Async operation Interrupted, breaking trades DL loop.")
|
||||||
break
|
break
|
||||||
|
|
||||||
return (pair, trades)
|
return (pair, trades)
|
||||||
@@ -2356,9 +2353,16 @@ class Exchange:
|
|||||||
raise OperationalException("This exchange does not support downloading Trades.")
|
raise OperationalException("This exchange does not support downloading Trades.")
|
||||||
|
|
||||||
with self._loop_lock:
|
with self._loop_lock:
|
||||||
return self.loop.run_until_complete(
|
task = asyncio.ensure_future(self._async_get_trade_history(
|
||||||
self._async_get_trade_history(pair=pair, since=since,
|
pair=pair, since=since, until=until, from_id=from_id))
|
||||||
until=until, from_id=from_id))
|
|
||||||
|
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||||
|
try:
|
||||||
|
self.loop.add_signal_handler(sig, task.cancel)
|
||||||
|
except NotImplementedError:
|
||||||
|
# Not all platforms implement signals (e.g. windows)
|
||||||
|
pass
|
||||||
|
return self.loop.run_until_complete(task)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ class FreqaiDataDrawer:
|
|||||||
num_keep = self.freqai_info["purge_old_models"]
|
num_keep = self.freqai_info["purge_old_models"]
|
||||||
if not num_keep:
|
if not num_keep:
|
||||||
return
|
return
|
||||||
elif type(num_keep) == bool:
|
elif isinstance(num_keep, bool):
|
||||||
num_keep = 2
|
num_keep = 2
|
||||||
|
|
||||||
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ class PyTorchMLPClassifier(BasePyTorchClassifier):
|
|||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 5000,
|
"n_steps": 5000,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": null,
|
"n_epochs": null,
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 512,
|
"hidden_dim": 512,
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class PyTorchMLPRegressor(BasePyTorchRegressor):
|
|||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 5000,
|
"n_steps": 5000,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": null,
|
"n_epochs": null,
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 512,
|
"hidden_dim": 512,
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor):
|
|||||||
"model_training_parameters" : {
|
"model_training_parameters" : {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 5000,
|
"n_steps": 5000,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": null
|
"n_epochs": null
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 512,
|
"hidden_dim": 512,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import torch
|
import torch
|
||||||
@@ -12,14 +11,14 @@ class PyTorchDataConvertor(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
def convert_x(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||||
"""
|
"""
|
||||||
:param df: "*_features" dataframe.
|
:param df: "*_features" dataframe.
|
||||||
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
def convert_y(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||||
"""
|
"""
|
||||||
:param df: "*_labels" dataframe.
|
:param df: "*_labels" dataframe.
|
||||||
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
:param device: The device to use for training (e.g. 'cpu', 'cuda').
|
||||||
@@ -33,8 +32,8 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
target_tensor_type: Optional[torch.dtype] = None,
|
target_tensor_type: torch.dtype = torch.float32,
|
||||||
squeeze_target_tensor: bool = False
|
squeeze_target_tensor: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param target_tensor_type: type of target tensor, for classification use
|
:param target_tensor_type: type of target tensor, for classification use
|
||||||
@@ -45,23 +44,14 @@ class DefaultPyTorchDataConvertor(PyTorchDataConvertor):
|
|||||||
self._target_tensor_type = target_tensor_type
|
self._target_tensor_type = target_tensor_type
|
||||||
self._squeeze_target_tensor = squeeze_target_tensor
|
self._squeeze_target_tensor = squeeze_target_tensor
|
||||||
|
|
||||||
def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
def convert_x(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||||
x = torch.from_numpy(df.values).float()
|
numpy_arrays = df.values
|
||||||
if device:
|
x = torch.tensor(numpy_arrays, device=device, dtype=torch.float32)
|
||||||
x = x.to(device)
|
|
||||||
|
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor:
|
def convert_y(self, df: pd.DataFrame, device: str) -> torch.Tensor:
|
||||||
y = torch.from_numpy(df.values)
|
numpy_arrays = df.values
|
||||||
|
y = torch.tensor(numpy_arrays, device=device, dtype=self._target_tensor_type)
|
||||||
if self._target_tensor_type:
|
|
||||||
y = y.to(self._target_tensor_type)
|
|
||||||
|
|
||||||
if self._squeeze_target_tensor:
|
if self._squeeze_target_tensor:
|
||||||
y = y.squeeze()
|
y = y.squeeze()
|
||||||
|
|
||||||
if device:
|
|
||||||
y = y.to(device)
|
|
||||||
|
|
||||||
return y
|
return y
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import math
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@@ -40,23 +39,27 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
state_dict and model_meta_data saved by self.save() method.
|
state_dict and model_meta_data saved by self.save() method.
|
||||||
:param model_meta_data: Additional metadata about the model (optional).
|
:param model_meta_data: Additional metadata about the model (optional).
|
||||||
:param data_convertor: convertor from pd.DataFrame to torch.tensor.
|
:param data_convertor: convertor from pd.DataFrame to torch.tensor.
|
||||||
:param max_iters: The number of training iterations to run.
|
:param n_steps: used to calculate n_epochs. The number of training iterations to run.
|
||||||
iteration here refers to the number of times we call
|
iteration here refers to the number of times optimizer.step() is called.
|
||||||
self.optimizer.step(). used to calculate n_epochs.
|
ignored if n_epochs is set.
|
||||||
|
:param n_epochs: The maximum number batches to use for evaluation.
|
||||||
:param batch_size: The size of the batches to use during training.
|
:param batch_size: The size of the batches to use during training.
|
||||||
:param max_n_eval_batches: The maximum number batches to use for evaluation.
|
|
||||||
"""
|
"""
|
||||||
self.model = model
|
self.model = model
|
||||||
self.optimizer = optimizer
|
self.optimizer = optimizer
|
||||||
self.criterion = criterion
|
self.criterion = criterion
|
||||||
self.model_meta_data = model_meta_data
|
self.model_meta_data = model_meta_data
|
||||||
self.device = device
|
self.device = device
|
||||||
self.max_iters: int = kwargs.get("max_iters", 100)
|
self.n_epochs: Optional[int] = kwargs.get("n_epochs", 10)
|
||||||
|
self.n_steps: Optional[int] = kwargs.get("n_steps", None)
|
||||||
|
if self.n_steps is None and not self.n_epochs:
|
||||||
|
raise Exception("Either `n_steps` or `n_epochs` should be set.")
|
||||||
|
|
||||||
self.batch_size: int = kwargs.get("batch_size", 64)
|
self.batch_size: int = kwargs.get("batch_size", 64)
|
||||||
self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None)
|
|
||||||
self.data_convertor = data_convertor
|
self.data_convertor = data_convertor
|
||||||
self.window_size: int = window_size
|
self.window_size: int = window_size
|
||||||
self.tb_logger = tb_logger
|
self.tb_logger = tb_logger
|
||||||
|
self.test_batch_counter = 0
|
||||||
|
|
||||||
def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]):
|
def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]):
|
||||||
"""
|
"""
|
||||||
@@ -72,55 +75,46 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
backpropagation.
|
backpropagation.
|
||||||
- Updates the model's parameters using an optimizer.
|
- Updates the model's parameters using an optimizer.
|
||||||
"""
|
"""
|
||||||
data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits)
|
|
||||||
epochs = self.calc_n_epochs(
|
|
||||||
n_obs=len(data_dictionary["train_features"]),
|
|
||||||
batch_size=self.batch_size,
|
|
||||||
n_iters=self.max_iters
|
|
||||||
)
|
|
||||||
self.model.train()
|
self.model.train()
|
||||||
for epoch in range(1, epochs + 1):
|
|
||||||
for i, batch_data in enumerate(data_loaders_dictionary["train"]):
|
|
||||||
|
|
||||||
|
data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits)
|
||||||
|
n_obs = len(data_dictionary["train_features"])
|
||||||
|
n_epochs = self.n_epochs or self.calc_n_epochs(n_obs=n_obs)
|
||||||
|
batch_counter = 0
|
||||||
|
for _ in range(n_epochs):
|
||||||
|
for _, batch_data in enumerate(data_loaders_dictionary["train"]):
|
||||||
xb, yb = batch_data
|
xb, yb = batch_data
|
||||||
xb.to(self.device)
|
xb = xb.to(self.device)
|
||||||
yb.to(self.device)
|
yb = yb.to(self.device)
|
||||||
yb_pred = self.model(xb)
|
yb_pred = self.model(xb)
|
||||||
loss = self.criterion(yb_pred, yb)
|
loss = self.criterion(yb_pred, yb)
|
||||||
|
|
||||||
self.optimizer.zero_grad(set_to_none=True)
|
self.optimizer.zero_grad(set_to_none=True)
|
||||||
loss.backward()
|
loss.backward()
|
||||||
self.optimizer.step()
|
self.optimizer.step()
|
||||||
self.tb_logger.log_scalar("train_loss", loss.item(), i)
|
self.tb_logger.log_scalar("train_loss", loss.item(), batch_counter)
|
||||||
|
batch_counter += 1
|
||||||
|
|
||||||
# evaluation
|
# evaluation
|
||||||
if "test" in splits:
|
if "test" in splits:
|
||||||
self.estimate_loss(
|
self.estimate_loss(data_loaders_dictionary, "test")
|
||||||
data_loaders_dictionary,
|
|
||||||
self.max_n_eval_batches,
|
|
||||||
"test"
|
|
||||||
)
|
|
||||||
|
|
||||||
@torch.no_grad()
|
@torch.no_grad()
|
||||||
def estimate_loss(
|
def estimate_loss(
|
||||||
self,
|
self,
|
||||||
data_loader_dictionary: Dict[str, DataLoader],
|
data_loader_dictionary: Dict[str, DataLoader],
|
||||||
max_n_eval_batches: Optional[int],
|
|
||||||
split: str,
|
split: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
n_batches = 0
|
for _, batch_data in enumerate(data_loader_dictionary[split]):
|
||||||
for i, batch_data in enumerate(data_loader_dictionary[split]):
|
|
||||||
if max_n_eval_batches and i > max_n_eval_batches:
|
|
||||||
n_batches += 1
|
|
||||||
break
|
|
||||||
xb, yb = batch_data
|
xb, yb = batch_data
|
||||||
xb.to(self.device)
|
xb = xb.to(self.device)
|
||||||
yb.to(self.device)
|
yb = yb.to(self.device)
|
||||||
|
|
||||||
yb_pred = self.model(xb)
|
yb_pred = self.model(xb)
|
||||||
loss = self.criterion(yb_pred, yb)
|
loss = self.criterion(yb_pred, yb)
|
||||||
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), i)
|
self.tb_logger.log_scalar(f"{split}_loss", loss.item(), self.test_batch_counter)
|
||||||
|
self.test_batch_counter += 1
|
||||||
|
|
||||||
self.model.train()
|
self.model.train()
|
||||||
|
|
||||||
@@ -148,31 +142,30 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
|||||||
|
|
||||||
return data_loader_dictionary
|
return data_loader_dictionary
|
||||||
|
|
||||||
@staticmethod
|
def calc_n_epochs(self, n_obs: int) -> int:
|
||||||
def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int:
|
|
||||||
"""
|
"""
|
||||||
Calculates the number of epochs required to reach the maximum number
|
Calculates the number of epochs required to reach the maximum number
|
||||||
of iterations specified in the model training parameters.
|
of iterations specified in the model training parameters.
|
||||||
|
|
||||||
the motivation here is that `max_iters` is easier to optimize and keep stable,
|
the motivation here is that `n_steps` is easier to optimize and keep stable,
|
||||||
across different n_obs - the number of data points.
|
across different n_obs - the number of data points.
|
||||||
"""
|
"""
|
||||||
|
assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set."
|
||||||
|
n_batches = n_obs // self.batch_size
|
||||||
|
n_epochs = min(self.n_steps // n_batches, 1)
|
||||||
|
if n_epochs <= 10:
|
||||||
|
logger.warning(
|
||||||
|
f"Setting low n_epochs: {n_epochs}. "
|
||||||
|
f"Please consider increasing `n_steps` hyper-parameter."
|
||||||
|
)
|
||||||
|
|
||||||
n_batches = math.ceil(n_obs // batch_size)
|
return n_epochs
|
||||||
epochs = math.ceil(n_iters // n_batches)
|
|
||||||
if epochs <= 10:
|
|
||||||
logger.warning("User set `max_iters` in such a way that the trainer will only perform "
|
|
||||||
f" {epochs} epochs. Please consider increasing this value accordingly")
|
|
||||||
if epochs <= 1:
|
|
||||||
logger.warning("Epochs set to 1. Please review your `max_iters` value")
|
|
||||||
epochs = 1
|
|
||||||
return epochs
|
|
||||||
|
|
||||||
def save(self, path: Path):
|
def save(self, path: Path):
|
||||||
"""
|
"""
|
||||||
- Saving any nn.Module state_dict
|
- Saving any nn.Module state_dict
|
||||||
- Saving model_meta_data, this dict should contain any additional data that the
|
- Saving model_meta_data, this dict should contain any additional data that the
|
||||||
user needs to store. e.g class_names for classification models.
|
user needs to store. e.g. class_names for classification models.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
torch.save({
|
torch.save({
|
||||||
|
|||||||
@@ -192,30 +192,6 @@ def plural(num: float, singular: str, plural: Optional[str] = None) -> str:
|
|||||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||||
|
|
||||||
|
|
||||||
def render_template(templatefile: str, arguments: dict = {}) -> str:
|
|
||||||
|
|
||||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
||||||
|
|
||||||
env = Environment(
|
|
||||||
loader=PackageLoader('freqtrade', 'templates'),
|
|
||||||
autoescape=select_autoescape(['html', 'xml'])
|
|
||||||
)
|
|
||||||
template = env.get_template(templatefile)
|
|
||||||
return template.render(**arguments)
|
|
||||||
|
|
||||||
|
|
||||||
def render_template_with_fallback(templatefile: str, templatefallbackfile: str,
|
|
||||||
arguments: dict = {}) -> str:
|
|
||||||
"""
|
|
||||||
Use templatefile if possible, otherwise fall back to templatefallbackfile
|
|
||||||
"""
|
|
||||||
from jinja2.exceptions import TemplateNotFound
|
|
||||||
try:
|
|
||||||
return render_template(templatefile, arguments)
|
|
||||||
except TemplateNotFound:
|
|
||||||
return render_template(templatefallbackfile, arguments)
|
|
||||||
|
|
||||||
|
|
||||||
def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]:
|
def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]:
|
||||||
"""
|
"""
|
||||||
Split lst into chunks of the size n.
|
Split lst into chunks of the size n.
|
||||||
|
|||||||
@@ -369,13 +369,14 @@ class Backtesting:
|
|||||||
# Cleanup from prior runs
|
# Cleanup from prior runs
|
||||||
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
|
pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore')
|
||||||
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})
|
df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})
|
||||||
# Trim startup period from analyzed dataframe
|
|
||||||
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
|
||||||
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
|
||||||
# Update dataprovider cache
|
# Update dataprovider cache
|
||||||
self.dataprovider._set_cached_df(
|
self.dataprovider._set_cached_df(
|
||||||
pair, self.timeframe, df_analyzed, self.config['candle_type_def'])
|
pair, self.timeframe, df_analyzed, self.config['candle_type_def'])
|
||||||
|
|
||||||
|
# Trim startup period from analyzed dataframe
|
||||||
|
df_analyzed = processed[pair] = pair_data = trim_dataframe(
|
||||||
|
df_analyzed, self.timerange, startup_candles=self.required_startup)
|
||||||
|
|
||||||
# Create a copy of the dataframe before shifting, that way the entry signal/tag
|
# Create a copy of the dataframe before shifting, that way the entry signal/tag
|
||||||
# remains on the correct candle for callbacks.
|
# remains on the correct candle for callbacks.
|
||||||
df_analyzed = df_analyzed.copy()
|
df_analyzed = df_analyzed.copy()
|
||||||
@@ -567,8 +568,7 @@ class Backtesting:
|
|||||||
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
|
pos_trade = self._get_exit_for_signal(trade, row, exit_, amount)
|
||||||
if pos_trade is not None:
|
if pos_trade is not None:
|
||||||
order = pos_trade.orders[-1]
|
order = pos_trade.orders[-1]
|
||||||
if self._get_order_filled(order.ft_price, row):
|
if self._try_close_open_order(order, trade, current_date, row):
|
||||||
order.close_bt_order(current_date, trade)
|
|
||||||
trade.recalc_trade_from_orders()
|
trade.recalc_trade_from_orders()
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
return pos_trade
|
return pos_trade
|
||||||
@@ -579,6 +579,19 @@ class Backtesting:
|
|||||||
""" Rate is within candle, therefore filled"""
|
""" Rate is within candle, therefore filled"""
|
||||||
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||||
|
|
||||||
|
def _try_close_open_order(
|
||||||
|
self, order: Optional[Order], trade: LocalTrade, current_date: datetime,
|
||||||
|
row: Tuple) -> bool:
|
||||||
|
"""
|
||||||
|
Check if an order is open and if it should've filled.
|
||||||
|
:return: True if the order filled.
|
||||||
|
"""
|
||||||
|
if order and self._get_order_filled(order.ft_price, row):
|
||||||
|
order.close_bt_order(current_date, trade)
|
||||||
|
trade.open_order_id = None
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _get_exit_for_signal(
|
def _get_exit_for_signal(
|
||||||
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
|
||||||
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
amount: Optional[float] = None) -> Optional[LocalTrade]:
|
||||||
@@ -903,9 +916,7 @@ class Backtesting:
|
|||||||
)
|
)
|
||||||
order._trade_bt = trade
|
order._trade_bt = trade
|
||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
if pos_adjust and self._get_order_filled(order.ft_price, row):
|
if not self._try_close_open_order(order, trade, current_time, row):
|
||||||
order.close_bt_order(current_time, trade)
|
|
||||||
else:
|
|
||||||
trade.open_order_id = str(self.order_id_counter)
|
trade.open_order_id = str(self.order_id_counter)
|
||||||
trade.recalc_trade_from_orders()
|
trade.recalc_trade_from_orders()
|
||||||
|
|
||||||
@@ -1121,23 +1132,18 @@ class Backtesting:
|
|||||||
for trade in list(LocalTrade.bt_trades_open_pp[pair]):
|
for trade in list(LocalTrade.bt_trades_open_pp[pair]):
|
||||||
# 3. Process entry orders.
|
# 3. Process entry orders.
|
||||||
order = trade.select_order(trade.entry_side, is_open=True)
|
order = trade.select_order(trade.entry_side, is_open=True)
|
||||||
if order and self._get_order_filled(order.ft_price, row):
|
if self._try_close_open_order(order, trade, current_time, row):
|
||||||
order.close_bt_order(current_time, trade)
|
|
||||||
trade.open_order_id = None
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# 4. Create exit orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
self._check_trade_exit(trade, row) # Place exit order if necessary
|
self._check_trade_exit(trade, row) # Place exit order if necessary
|
||||||
|
|
||||||
# 5. Process exit orders.
|
# 5. Process exit orders.
|
||||||
order = trade.select_order(trade.exit_side, is_open=True)
|
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._try_close_open_order(order, trade, current_time, row):
|
||||||
order.close_bt_order(current_time, trade)
|
|
||||||
trade.open_order_id = None
|
|
||||||
sub_trade = order.safe_amount_after_fee != trade.amount
|
sub_trade = order.safe_amount_after_fee != trade.amount
|
||||||
if sub_trade:
|
if sub_trade:
|
||||||
order.close_bt_order(current_time, trade)
|
|
||||||
trade.recalc_trade_from_orders()
|
trade.recalc_trade_from_orders()
|
||||||
else:
|
else:
|
||||||
trade.close_date = current_time
|
trade.close_date = current_time
|
||||||
@@ -1191,7 +1197,8 @@ class Backtesting:
|
|||||||
|
|
||||||
row_index += 1
|
row_index += 1
|
||||||
indexes[pair] = row_index
|
indexes[pair] = row_index
|
||||||
self.dataprovider._set_dataframe_max_index(row_index)
|
self.dataprovider._set_dataframe_max_index(self.required_startup + row_index)
|
||||||
|
self.dataprovider._set_dataframe_max_date(current_time)
|
||||||
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
|
||||||
|
|
||||||
@@ -1224,12 +1231,14 @@ class Backtesting:
|
|||||||
is_first = True
|
is_first = True
|
||||||
current_time_det = current_time
|
current_time_det = current_time
|
||||||
for det_row in detail_data[HEADERS].values.tolist():
|
for det_row in detail_data[HEADERS].values.tolist():
|
||||||
|
self.dataprovider._set_dataframe_max_date(current_time_det)
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
det_row, pair, current_time_det, end_date,
|
det_row, pair, current_time_det, end_date,
|
||||||
open_trade_count_start, trade_dir, is_first)
|
open_trade_count_start, trade_dir, is_first)
|
||||||
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
current_time_det += timedelta(minutes=self.timeframe_detail_min)
|
||||||
is_first = False
|
is_first = False
|
||||||
else:
|
else:
|
||||||
|
self.dataprovider._set_dataframe_max_date(current_time)
|
||||||
open_trade_count_start = self.backtest_loop(
|
open_trade_count_start = self.backtest_loop(
|
||||||
row, pair, current_time, end_date,
|
row, pair, current_time, end_date,
|
||||||
open_trade_count_start, trade_dir)
|
open_trade_count_start, trade_dir)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class LookaheadAnalysis:
|
|||||||
self.entry_varHolders: List[VarHolder] = []
|
self.entry_varHolders: List[VarHolder] = []
|
||||||
self.exit_varHolders: List[VarHolder] = []
|
self.exit_varHolders: List[VarHolder] = []
|
||||||
self.exchange: Optional[Any] = None
|
self.exchange: Optional[Any] = None
|
||||||
|
self._fee = None
|
||||||
|
|
||||||
# pull variables the scope of the lookahead_analysis-instance
|
# pull variables the scope of the lookahead_analysis-instance
|
||||||
self.local_config = deepcopy(config)
|
self.local_config = deepcopy(config)
|
||||||
@@ -145,8 +146,13 @@ class LookaheadAnalysis:
|
|||||||
str(self.dt_to_timestamp(varholder.to_dt)))
|
str(self.dt_to_timestamp(varholder.to_dt)))
|
||||||
prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load
|
prepare_data_config['exchange']['pair_whitelist'] = pairs_to_load
|
||||||
|
|
||||||
|
if self._fee is not None:
|
||||||
|
# Don't re-calculate fee per pair, as fee might differ per pair.
|
||||||
|
prepare_data_config['fee'] = self._fee
|
||||||
|
|
||||||
backtesting = Backtesting(prepare_data_config, self.exchange)
|
backtesting = Backtesting(prepare_data_config, self.exchange)
|
||||||
self.exchange = backtesting.exchange
|
self.exchange = backtesting.exchange
|
||||||
|
self._fee = backtesting.fee
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
|
||||||
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
varholder.data, varholder.timerange = backtesting.load_bt_data()
|
||||||
@@ -198,7 +204,7 @@ class LookaheadAnalysis:
|
|||||||
self.prepare_data(exit_varHolder, [result_row['pair']])
|
self.prepare_data(exit_varHolder, [result_row['pair']])
|
||||||
|
|
||||||
# now we analyze a full trade of full_varholder and look for analyze its bias
|
# now we analyze a full trade of full_varholder and look for analyze its bias
|
||||||
def analyze_row(self, idx, result_row):
|
def analyze_row(self, idx: int, result_row):
|
||||||
# if force-sold, ignore this signal since here it will unconditionally exit.
|
# if force-sold, ignore this signal since here it will unconditionally exit.
|
||||||
if result_row.close_date == self.dt_to_timestamp(self.full_varHolder.to_dt):
|
if result_row.close_date == self.dt_to_timestamp(self.full_varHolder.to_dt):
|
||||||
return
|
return
|
||||||
@@ -209,12 +215,16 @@ class LookaheadAnalysis:
|
|||||||
# fill entry_varHolder and exit_varHolder
|
# fill entry_varHolder and exit_varHolder
|
||||||
self.fill_entry_and_exit_varHolders(result_row)
|
self.fill_entry_and_exit_varHolders(result_row)
|
||||||
|
|
||||||
|
# this will trigger a logger-message
|
||||||
|
buy_or_sell_biased: bool = False
|
||||||
|
|
||||||
# register if buy signal is broken
|
# register if buy signal is broken
|
||||||
if not self.report_signal(
|
if not self.report_signal(
|
||||||
self.entry_varHolders[idx].result,
|
self.entry_varHolders[idx].result,
|
||||||
"open_date",
|
"open_date",
|
||||||
self.entry_varHolders[idx].compared_dt):
|
self.entry_varHolders[idx].compared_dt):
|
||||||
self.current_analysis.false_entry_signals += 1
|
self.current_analysis.false_entry_signals += 1
|
||||||
|
buy_or_sell_biased = True
|
||||||
|
|
||||||
# register if buy or sell signal is broken
|
# register if buy or sell signal is broken
|
||||||
if not self.report_signal(
|
if not self.report_signal(
|
||||||
@@ -222,6 +232,13 @@ class LookaheadAnalysis:
|
|||||||
"close_date",
|
"close_date",
|
||||||
self.exit_varHolders[idx].compared_dt):
|
self.exit_varHolders[idx].compared_dt):
|
||||||
self.current_analysis.false_exit_signals += 1
|
self.current_analysis.false_exit_signals += 1
|
||||||
|
buy_or_sell_biased = True
|
||||||
|
|
||||||
|
if buy_or_sell_biased:
|
||||||
|
logger.info(f"found lookahead-bias in trade "
|
||||||
|
f"pair: {result_row['pair']}, "
|
||||||
|
f"timerange:{result_row['open_date']} - {result_row['close_date']}, "
|
||||||
|
f"idx: {idx}")
|
||||||
|
|
||||||
# check if the indicators themselves contain biased data
|
# check if the indicators themselves contain biased data
|
||||||
self.analyze_indicators(self.full_varHolder, self.entry_varHolders[idx], result_row['pair'])
|
self.analyze_indicators(self.full_varHolder, self.entry_varHolders[idx], result_row['pair'])
|
||||||
@@ -251,9 +268,33 @@ class LookaheadAnalysis:
|
|||||||
# starting from the same datetime to avoid miss-reports of bias
|
# starting from the same datetime to avoid miss-reports of bias
|
||||||
for idx, result_row in self.full_varHolder.result['results'].iterrows():
|
for idx, result_row in self.full_varHolder.result['results'].iterrows():
|
||||||
if self.current_analysis.total_signals == self.targeted_trade_amount:
|
if self.current_analysis.total_signals == self.targeted_trade_amount:
|
||||||
|
logger.info(f"Found targeted trade amount = {self.targeted_trade_amount} signals.")
|
||||||
break
|
break
|
||||||
|
if found_signals < self.minimum_trade_amount:
|
||||||
|
logger.info(f"only found {found_signals} "
|
||||||
|
f"which is smaller than "
|
||||||
|
f"minimum trade amount = {self.minimum_trade_amount}. "
|
||||||
|
f"Exiting this lookahead-analysis")
|
||||||
|
return None
|
||||||
|
if "force_exit" in result_row['exit_reason']:
|
||||||
|
logger.info("found force-exit in pair: {result_row['pair']}, "
|
||||||
|
f"timerange:{result_row['open_date']}-{result_row['close_date']}, "
|
||||||
|
f"idx: {idx}, skipping this one to avoid a false-positive.")
|
||||||
|
|
||||||
|
# just to keep the IDs of both full, entry and exit varholders the same
|
||||||
|
# to achieve a better debugging experience
|
||||||
|
self.entry_varHolders.append(VarHolder())
|
||||||
|
self.exit_varHolders.append(VarHolder())
|
||||||
|
continue
|
||||||
|
|
||||||
self.analyze_row(idx, result_row)
|
self.analyze_row(idx, result_row)
|
||||||
|
|
||||||
|
if len(self.entry_varHolders) < self.minimum_trade_amount:
|
||||||
|
logger.info(f"only found {found_signals} after skipping forced exits "
|
||||||
|
f"which is smaller than "
|
||||||
|
f"minimum trade amount = {self.minimum_trade_amount}. "
|
||||||
|
f"Exiting this lookahead-analysis")
|
||||||
|
|
||||||
# Restore verbosity, so it's not too quiet for the next strategy
|
# Restore verbosity, so it's not too quiet for the next strategy
|
||||||
restore_verbosity_for_bias_tester()
|
restore_verbosity_for_bias_tester()
|
||||||
# check and report signals
|
# check and report signals
|
||||||
|
|||||||
@@ -137,6 +137,19 @@ class LookaheadAnalysisSubFunctions:
|
|||||||
'just to avoid false positives')
|
'just to avoid false positives')
|
||||||
config['dry_run_wallet'] = min_dry_run_wallet
|
config['dry_run_wallet'] = min_dry_run_wallet
|
||||||
|
|
||||||
|
if 'timerange' not in config:
|
||||||
|
# setting a timerange is enforced here
|
||||||
|
raise OperationalException(
|
||||||
|
"Please set a timerange. "
|
||||||
|
"Usually a few months are enough depending on your needs and strategy."
|
||||||
|
)
|
||||||
|
# fix stake_amount to 10k.
|
||||||
|
# in a combination with a wallet size of 1 billion it should always be able to trade
|
||||||
|
# no matter if they use custom_stake_amount as a small percentage of wallet size
|
||||||
|
# or fixate custom_stake_amount to a certain value.
|
||||||
|
logger.info('fixing stake_amount to 10k')
|
||||||
|
config['stake_amount'] = 10000
|
||||||
|
|
||||||
# enforce cache to be 'none', shift it to 'none' if not already
|
# enforce cache to be 'none', shift it to 'none' if not already
|
||||||
# (since the default value is 'day')
|
# (since the default value is 'day')
|
||||||
if config.get('backtest_cache') is None:
|
if config.get('backtest_cache') is None:
|
||||||
|
|||||||
@@ -648,11 +648,9 @@ class LocalTrade:
|
|||||||
"""
|
"""
|
||||||
Method used internally to set self.stop_loss.
|
Method used internally to set self.stop_loss.
|
||||||
"""
|
"""
|
||||||
stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode,
|
|
||||||
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
|
||||||
if not self.stop_loss:
|
if not self.stop_loss:
|
||||||
self.initial_stop_loss = stop_loss_norm
|
self.initial_stop_loss = stop_loss
|
||||||
self.stop_loss = stop_loss_norm
|
self.stop_loss = stop_loss
|
||||||
|
|
||||||
self.stop_loss_pct = -1 * abs(percent)
|
self.stop_loss_pct = -1 * abs(percent)
|
||||||
|
|
||||||
@@ -676,26 +674,27 @@ class LocalTrade:
|
|||||||
else:
|
else:
|
||||||
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
|
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
|
||||||
|
|
||||||
|
stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode,
|
||||||
|
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
||||||
# no stop loss assigned yet
|
# no stop loss assigned yet
|
||||||
if self.initial_stop_loss_pct is None or refresh:
|
if self.initial_stop_loss_pct is None or refresh:
|
||||||
self.__set_stop_loss(new_loss, stoploss)
|
self.__set_stop_loss(stop_loss_norm, stoploss)
|
||||||
self.initial_stop_loss = price_to_precision(
|
self.initial_stop_loss = price_to_precision(
|
||||||
new_loss, self.price_precision, self.precision_mode,
|
stop_loss_norm, self.price_precision, self.precision_mode,
|
||||||
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP)
|
||||||
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
||||||
|
|
||||||
# evaluate if the stop loss needs to be updated
|
# evaluate if the stop loss needs to be updated
|
||||||
else:
|
else:
|
||||||
|
higher_stop = stop_loss_norm > self.stop_loss
|
||||||
higher_stop = new_loss > self.stop_loss
|
lower_stop = stop_loss_norm < self.stop_loss
|
||||||
lower_stop = new_loss < self.stop_loss
|
|
||||||
|
|
||||||
# stop losses only walk up, never down!,
|
# stop losses only walk up, never down!,
|
||||||
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
||||||
# ? decreasing the minimum stoploss
|
# ? decreasing the minimum stoploss
|
||||||
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
||||||
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
||||||
self.__set_stop_loss(new_loss, stoploss)
|
self.__set_stop_loss(stop_loss_norm, stoploss)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
||||||
|
|
||||||
@@ -769,10 +768,8 @@ class LocalTrade:
|
|||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
self.recalc_trade_from_orders(is_closing=True)
|
self.recalc_trade_from_orders(is_closing=True)
|
||||||
if show_msg:
|
if show_msg:
|
||||||
logger.info(
|
logger.info(f"Marking {self} as closed as the trade is fulfilled "
|
||||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
"and found no open orders for it.")
|
||||||
self
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
|
def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float],
|
||||||
side: str) -> None:
|
side: str) -> None:
|
||||||
@@ -1220,12 +1217,13 @@ class LocalTrade:
|
|||||||
return LocalTrade.bt_open_open_trade_count
|
return LocalTrade.bt_open_open_trade_count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def stoploss_reinitialization(desired_stoploss):
|
def stoploss_reinitialization(desired_stoploss: float):
|
||||||
"""
|
"""
|
||||||
Adjust initial Stoploss to desired stoploss for all open trades.
|
Adjust initial Stoploss to desired stoploss for all open trades.
|
||||||
"""
|
"""
|
||||||
|
trade: Trade
|
||||||
for trade in Trade.get_open_trades():
|
for trade in Trade.get_open_trades():
|
||||||
logger.info("Found open trade: %s", trade)
|
logger.info(f"Found open trade: {trade}")
|
||||||
|
|
||||||
# skip case if trailing-stop changed the stoploss already.
|
# skip case if trailing-stop changed the stoploss already.
|
||||||
if (trade.stop_loss == trade.initial_stop_loss
|
if (trade.stop_loss == trade.initial_stop_loss
|
||||||
@@ -1234,7 +1232,7 @@ class LocalTrade:
|
|||||||
|
|
||||||
logger.info(f"Stoploss for {trade} needs adjustment...")
|
logger.info(f"Stoploss for {trade} needs adjustment...")
|
||||||
# Force reset of stoploss
|
# Force reset of stoploss
|
||||||
trade.stop_loss = None
|
trade.stop_loss = 0.0
|
||||||
trade.initial_stop_loss_pct = None
|
trade.initial_stop_loss_pct = None
|
||||||
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
|
||||||
logger.info(f"New stoploss: {trade.stop_loss}.")
|
logger.info(f"New stoploss: {trade.stop_loss}.")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict, RootModel, SerializeAsAny
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
||||||
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode
|
||||||
@@ -9,9 +9,9 @@ from freqtrade.types import ValidExchangesType
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeModePayloadMixin(BaseModel):
|
class ExchangeModePayloadMixin(BaseModel):
|
||||||
trading_mode: Optional[TradingMode]
|
trading_mode: Optional[TradingMode] = None
|
||||||
margin_mode: Optional[MarginMode]
|
margin_mode: Optional[MarginMode] = None
|
||||||
exchange: Optional[str]
|
exchange: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Ping(BaseModel):
|
class Ping(BaseModel):
|
||||||
@@ -43,11 +43,11 @@ class BackgroundTaskStatus(BaseModel):
|
|||||||
job_category: str
|
job_category: str
|
||||||
status: str
|
status: str
|
||||||
running: bool
|
running: bool
|
||||||
progress: Optional[float]
|
progress: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
class BackgroundTaskResult(BaseModel):
|
class BackgroundTaskResult(BaseModel):
|
||||||
error: Optional[str]
|
error: Optional[str] = None
|
||||||
status: str
|
status: str
|
||||||
|
|
||||||
|
|
||||||
@@ -60,9 +60,9 @@ class Balance(BaseModel):
|
|||||||
free: float
|
free: float
|
||||||
balance: float
|
balance: float
|
||||||
used: float
|
used: float
|
||||||
bot_owned: Optional[float]
|
bot_owned: Optional[float] = None
|
||||||
est_stake: float
|
est_stake: float
|
||||||
est_stake_bot: Optional[float]
|
est_stake_bot: Optional[float] = None
|
||||||
stake: str
|
stake: str
|
||||||
# Starting with 2.x
|
# Starting with 2.x
|
||||||
side: str
|
side: str
|
||||||
@@ -141,7 +141,7 @@ class Profit(BaseModel):
|
|||||||
expectancy_ratio: float
|
expectancy_ratio: float
|
||||||
max_drawdown: float
|
max_drawdown: float
|
||||||
max_drawdown_abs: float
|
max_drawdown_abs: float
|
||||||
trading_volume: Optional[float]
|
trading_volume: Optional[float] = None
|
||||||
bot_start_timestamp: int
|
bot_start_timestamp: int
|
||||||
bot_start_date: str
|
bot_start_date: str
|
||||||
|
|
||||||
@@ -173,50 +173,50 @@ class Daily(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class UnfilledTimeout(BaseModel):
|
class UnfilledTimeout(BaseModel):
|
||||||
entry: Optional[int]
|
entry: Optional[int] = None
|
||||||
exit: Optional[int]
|
exit: Optional[int] = None
|
||||||
unit: Optional[str]
|
unit: Optional[str] = None
|
||||||
exit_timeout_count: Optional[int]
|
exit_timeout_count: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class OrderTypes(BaseModel):
|
class OrderTypes(BaseModel):
|
||||||
entry: OrderTypeValues
|
entry: OrderTypeValues
|
||||||
exit: OrderTypeValues
|
exit: OrderTypeValues
|
||||||
emergency_exit: Optional[OrderTypeValues]
|
emergency_exit: Optional[OrderTypeValues] = None
|
||||||
force_exit: Optional[OrderTypeValues]
|
force_exit: Optional[OrderTypeValues] = None
|
||||||
force_entry: Optional[OrderTypeValues]
|
force_entry: Optional[OrderTypeValues] = None
|
||||||
stoploss: OrderTypeValues
|
stoploss: OrderTypeValues
|
||||||
stoploss_on_exchange: bool
|
stoploss_on_exchange: bool
|
||||||
stoploss_on_exchange_interval: Optional[int]
|
stoploss_on_exchange_interval: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ShowConfig(BaseModel):
|
class ShowConfig(BaseModel):
|
||||||
version: str
|
version: str
|
||||||
strategy_version: Optional[str]
|
strategy_version: Optional[str] = None
|
||||||
api_version: float
|
api_version: float
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
trading_mode: str
|
trading_mode: str
|
||||||
short_allowed: bool
|
short_allowed: bool
|
||||||
stake_currency: str
|
stake_currency: str
|
||||||
stake_amount: str
|
stake_amount: str
|
||||||
available_capital: Optional[float]
|
available_capital: Optional[float] = None
|
||||||
stake_currency_decimals: int
|
stake_currency_decimals: int
|
||||||
max_open_trades: IntOrInf
|
max_open_trades: IntOrInf
|
||||||
minimal_roi: Dict[str, Any]
|
minimal_roi: Dict[str, Any]
|
||||||
stoploss: Optional[float]
|
stoploss: Optional[float] = None
|
||||||
stoploss_on_exchange: bool
|
stoploss_on_exchange: bool
|
||||||
trailing_stop: Optional[bool]
|
trailing_stop: Optional[bool] = None
|
||||||
trailing_stop_positive: Optional[float]
|
trailing_stop_positive: Optional[float] = None
|
||||||
trailing_stop_positive_offset: Optional[float]
|
trailing_stop_positive_offset: Optional[float] = None
|
||||||
trailing_only_offset_is_reached: Optional[bool]
|
trailing_only_offset_is_reached: Optional[bool] = None
|
||||||
unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode
|
unfilledtimeout: Optional[UnfilledTimeout] = None # Empty in webserver mode
|
||||||
order_types: Optional[OrderTypes]
|
order_types: Optional[OrderTypes] = None
|
||||||
use_custom_stoploss: Optional[bool]
|
use_custom_stoploss: Optional[bool] = None
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str] = None
|
||||||
timeframe_ms: int
|
timeframe_ms: int
|
||||||
timeframe_min: int
|
timeframe_min: int
|
||||||
exchange: str
|
exchange: str
|
||||||
strategy: Optional[str]
|
strategy: Optional[str] = None
|
||||||
force_entry_enable: bool
|
force_entry_enable: bool
|
||||||
exit_pricing: Dict[str, Any]
|
exit_pricing: Dict[str, Any]
|
||||||
entry_pricing: Dict[str, Any]
|
entry_pricing: Dict[str, Any]
|
||||||
@@ -231,17 +231,17 @@ class OrderSchema(BaseModel):
|
|||||||
pair: str
|
pair: str
|
||||||
order_id: str
|
order_id: str
|
||||||
status: str
|
status: str
|
||||||
remaining: Optional[float]
|
remaining: Optional[float] = None
|
||||||
amount: float
|
amount: float
|
||||||
safe_price: float
|
safe_price: float
|
||||||
cost: float
|
cost: float
|
||||||
filled: Optional[float]
|
filled: Optional[float] = None
|
||||||
ft_order_side: str
|
ft_order_side: str
|
||||||
order_type: str
|
order_type: str
|
||||||
is_open: bool
|
is_open: bool
|
||||||
order_timestamp: Optional[int]
|
order_timestamp: Optional[int] = None
|
||||||
order_filled_timestamp: Optional[int]
|
order_filled_timestamp: Optional[int] = None
|
||||||
ft_fee_base: Optional[float]
|
ft_fee_base: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
class TradeSchema(BaseModel):
|
class TradeSchema(BaseModel):
|
||||||
@@ -255,81 +255,81 @@ class TradeSchema(BaseModel):
|
|||||||
amount: float
|
amount: float
|
||||||
amount_requested: float
|
amount_requested: float
|
||||||
stake_amount: float
|
stake_amount: float
|
||||||
max_stake_amount: Optional[float]
|
max_stake_amount: Optional[float] = None
|
||||||
strategy: str
|
strategy: str
|
||||||
enter_tag: Optional[str]
|
enter_tag: Optional[str] = None
|
||||||
timeframe: int
|
timeframe: int
|
||||||
fee_open: Optional[float]
|
fee_open: Optional[float] = None
|
||||||
fee_open_cost: Optional[float]
|
fee_open_cost: Optional[float] = None
|
||||||
fee_open_currency: Optional[str]
|
fee_open_currency: Optional[str] = None
|
||||||
fee_close: Optional[float]
|
fee_close: Optional[float] = None
|
||||||
fee_close_cost: Optional[float]
|
fee_close_cost: Optional[float] = None
|
||||||
fee_close_currency: Optional[str]
|
fee_close_currency: Optional[str] = None
|
||||||
|
|
||||||
open_date: str
|
open_date: str
|
||||||
open_timestamp: int
|
open_timestamp: int
|
||||||
open_rate: float
|
open_rate: float
|
||||||
open_rate_requested: Optional[float]
|
open_rate_requested: Optional[float] = None
|
||||||
open_trade_value: float
|
open_trade_value: float
|
||||||
|
|
||||||
close_date: Optional[str]
|
close_date: Optional[str] = None
|
||||||
close_timestamp: Optional[int]
|
close_timestamp: Optional[int] = None
|
||||||
close_rate: Optional[float]
|
close_rate: Optional[float] = None
|
||||||
close_rate_requested: Optional[float]
|
close_rate_requested: Optional[float] = None
|
||||||
|
|
||||||
close_profit: Optional[float]
|
close_profit: Optional[float] = None
|
||||||
close_profit_pct: Optional[float]
|
close_profit_pct: Optional[float] = None
|
||||||
close_profit_abs: Optional[float]
|
close_profit_abs: Optional[float] = None
|
||||||
|
|
||||||
profit_ratio: Optional[float]
|
profit_ratio: Optional[float] = None
|
||||||
profit_pct: Optional[float]
|
profit_pct: Optional[float] = None
|
||||||
profit_abs: Optional[float]
|
profit_abs: Optional[float] = None
|
||||||
profit_fiat: Optional[float]
|
profit_fiat: Optional[float] = None
|
||||||
|
|
||||||
realized_profit: float
|
realized_profit: float
|
||||||
realized_profit_ratio: Optional[float]
|
realized_profit_ratio: Optional[float] = None
|
||||||
|
|
||||||
exit_reason: Optional[str]
|
exit_reason: Optional[str] = None
|
||||||
exit_order_status: Optional[str]
|
exit_order_status: Optional[str] = None
|
||||||
|
|
||||||
stop_loss_abs: Optional[float]
|
stop_loss_abs: Optional[float] = None
|
||||||
stop_loss_ratio: Optional[float]
|
stop_loss_ratio: Optional[float] = None
|
||||||
stop_loss_pct: Optional[float]
|
stop_loss_pct: Optional[float] = None
|
||||||
stoploss_order_id: Optional[str]
|
stoploss_order_id: Optional[str] = None
|
||||||
stoploss_last_update: Optional[str]
|
stoploss_last_update: Optional[str] = None
|
||||||
stoploss_last_update_timestamp: Optional[int]
|
stoploss_last_update_timestamp: Optional[int] = None
|
||||||
initial_stop_loss_abs: Optional[float]
|
initial_stop_loss_abs: Optional[float] = None
|
||||||
initial_stop_loss_ratio: Optional[float]
|
initial_stop_loss_ratio: Optional[float] = None
|
||||||
initial_stop_loss_pct: Optional[float]
|
initial_stop_loss_pct: Optional[float] = None
|
||||||
|
|
||||||
min_rate: Optional[float]
|
min_rate: Optional[float] = None
|
||||||
max_rate: Optional[float]
|
max_rate: Optional[float] = None
|
||||||
open_order_id: Optional[str]
|
open_order_id: Optional[str] = None
|
||||||
orders: List[OrderSchema]
|
orders: List[OrderSchema]
|
||||||
|
|
||||||
leverage: Optional[float]
|
leverage: Optional[float] = None
|
||||||
interest_rate: Optional[float]
|
interest_rate: Optional[float] = None
|
||||||
liquidation_price: Optional[float]
|
liquidation_price: Optional[float] = None
|
||||||
funding_fees: Optional[float]
|
funding_fees: Optional[float] = None
|
||||||
trading_mode: Optional[TradingMode]
|
trading_mode: Optional[TradingMode] = None
|
||||||
|
|
||||||
amount_precision: Optional[float]
|
amount_precision: Optional[float] = None
|
||||||
price_precision: Optional[float]
|
price_precision: Optional[float] = None
|
||||||
precision_mode: Optional[int]
|
precision_mode: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class OpenTradeSchema(TradeSchema):
|
class OpenTradeSchema(TradeSchema):
|
||||||
stoploss_current_dist: Optional[float]
|
stoploss_current_dist: Optional[float] = None
|
||||||
stoploss_current_dist_pct: Optional[float]
|
stoploss_current_dist_pct: Optional[float] = None
|
||||||
stoploss_current_dist_ratio: Optional[float]
|
stoploss_current_dist_ratio: Optional[float] = None
|
||||||
stoploss_entry_dist: Optional[float]
|
stoploss_entry_dist: Optional[float] = None
|
||||||
stoploss_entry_dist_ratio: Optional[float]
|
stoploss_entry_dist_ratio: Optional[float] = None
|
||||||
current_rate: float
|
current_rate: float
|
||||||
total_profit_abs: float
|
total_profit_abs: float
|
||||||
total_profit_fiat: Optional[float]
|
total_profit_fiat: Optional[float] = None
|
||||||
total_profit_ratio: Optional[float]
|
total_profit_ratio: Optional[float] = None
|
||||||
|
|
||||||
open_order: Optional[str]
|
open_order: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TradeResponse(BaseModel):
|
class TradeResponse(BaseModel):
|
||||||
@@ -339,8 +339,7 @@ class TradeResponse(BaseModel):
|
|||||||
total_trades: int
|
total_trades: int
|
||||||
|
|
||||||
|
|
||||||
class ForceEnterResponse(BaseModel):
|
ForceEnterResponse = RootModel[Union[TradeSchema, StatusMsg]]
|
||||||
__root__: Union[TradeSchema, StatusMsg]
|
|
||||||
|
|
||||||
|
|
||||||
class LockModel(BaseModel):
|
class LockModel(BaseModel):
|
||||||
@@ -352,7 +351,7 @@ class LockModel(BaseModel):
|
|||||||
lock_timestamp: int
|
lock_timestamp: int
|
||||||
pair: str
|
pair: str
|
||||||
side: str
|
side: str
|
||||||
reason: Optional[str]
|
reason: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class Locks(BaseModel):
|
class Locks(BaseModel):
|
||||||
@@ -361,8 +360,8 @@ class Locks(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class DeleteLockRequest(BaseModel):
|
class DeleteLockRequest(BaseModel):
|
||||||
pair: Optional[str]
|
pair: Optional[str] = None
|
||||||
lockid: Optional[int]
|
lockid: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class Logs(BaseModel):
|
class Logs(BaseModel):
|
||||||
@@ -373,17 +372,17 @@ class Logs(BaseModel):
|
|||||||
class ForceEnterPayload(BaseModel):
|
class ForceEnterPayload(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
side: SignalDirection = SignalDirection.LONG
|
side: SignalDirection = SignalDirection.LONG
|
||||||
price: Optional[float]
|
price: Optional[float] = None
|
||||||
ordertype: Optional[OrderTypeValues]
|
ordertype: Optional[OrderTypeValues] = None
|
||||||
stakeamount: Optional[float]
|
stakeamount: Optional[float] = None
|
||||||
entry_tag: Optional[str]
|
entry_tag: Optional[str] = None
|
||||||
leverage: Optional[float]
|
leverage: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
class ForceExitPayload(BaseModel):
|
class ForceExitPayload(BaseModel):
|
||||||
tradeid: str
|
tradeid: str
|
||||||
ordertype: Optional[OrderTypeValues]
|
ordertype: Optional[OrderTypeValues] = None
|
||||||
amount: Optional[float]
|
amount: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
class BlacklistPayload(BaseModel):
|
class BlacklistPayload(BaseModel):
|
||||||
@@ -405,7 +404,7 @@ class WhitelistResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class WhitelistEvaluateResponse(BackgroundTaskResult):
|
class WhitelistEvaluateResponse(BackgroundTaskResult):
|
||||||
result: Optional[WhitelistResponse]
|
result: Optional[WhitelistResponse] = None
|
||||||
|
|
||||||
|
|
||||||
class DeleteTrade(BaseModel):
|
class DeleteTrade(BaseModel):
|
||||||
@@ -420,8 +419,7 @@ class PlotConfig_(BaseModel):
|
|||||||
subplots: Dict[str, Any]
|
subplots: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class PlotConfig(BaseModel):
|
PlotConfig = RootModel[Union[PlotConfig_, Dict]]
|
||||||
__root__: Union[PlotConfig_, Dict]
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyListResponse(BaseModel):
|
class StrategyListResponse(BaseModel):
|
||||||
@@ -470,7 +468,7 @@ class PairHistory(BaseModel):
|
|||||||
timeframe: str
|
timeframe: str
|
||||||
timeframe_ms: int
|
timeframe_ms: int
|
||||||
columns: List[str]
|
columns: List[str]
|
||||||
data: List[Any]
|
data: SerializeAsAny[List[Any]]
|
||||||
length: int
|
length: int
|
||||||
buy_signals: int
|
buy_signals: int
|
||||||
sell_signals: int
|
sell_signals: int
|
||||||
@@ -484,11 +482,11 @@ class PairHistory(BaseModel):
|
|||||||
data_start: str
|
data_start: str
|
||||||
data_stop: str
|
data_stop: str
|
||||||
data_stop_ts: int
|
data_stop_ts: int
|
||||||
|
# TODO[pydantic]: The following keys were removed: `json_encoders`.
|
||||||
class Config:
|
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information.
|
||||||
json_encoders = {
|
model_config = ConfigDict(json_encoders={
|
||||||
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
|
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
|
||||||
}
|
})
|
||||||
|
|
||||||
|
|
||||||
class BacktestFreqAIInputs(BaseModel):
|
class BacktestFreqAIInputs(BaseModel):
|
||||||
@@ -497,16 +495,16 @@ class BacktestFreqAIInputs(BaseModel):
|
|||||||
|
|
||||||
class BacktestRequest(BaseModel):
|
class BacktestRequest(BaseModel):
|
||||||
strategy: str
|
strategy: str
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str] = None
|
||||||
timeframe_detail: Optional[str]
|
timeframe_detail: Optional[str] = None
|
||||||
timerange: Optional[str]
|
timerange: Optional[str] = None
|
||||||
max_open_trades: Optional[IntOrInf]
|
max_open_trades: Optional[IntOrInf] = None
|
||||||
stake_amount: Optional[str]
|
stake_amount: Optional[Union[str, float]] = None
|
||||||
enable_protections: bool
|
enable_protections: bool
|
||||||
dry_run_wallet: Optional[float]
|
dry_run_wallet: Optional[float] = None
|
||||||
backtest_cache: Optional[str]
|
backtest_cache: Optional[str] = None
|
||||||
freqaimodel: Optional[str]
|
freqaimodel: Optional[str] = None
|
||||||
freqai: Optional[BacktestFreqAIInputs]
|
freqai: Optional[BacktestFreqAIInputs] = None
|
||||||
|
|
||||||
|
|
||||||
class BacktestResponse(BaseModel):
|
class BacktestResponse(BaseModel):
|
||||||
@@ -515,9 +513,9 @@ class BacktestResponse(BaseModel):
|
|||||||
status_msg: str
|
status_msg: str
|
||||||
step: str
|
step: str
|
||||||
progress: float
|
progress: float
|
||||||
trade_count: Optional[float]
|
trade_count: Optional[float] = None
|
||||||
# TODO: Properly type backtestresult...
|
# TODO: Properly type backtestresult...
|
||||||
backtest_result: Optional[Dict[str, Any]]
|
backtest_result: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
# TODO: This is a copy of BacktestHistoryEntryType
|
# TODO: This is a copy of BacktestHistoryEntryType
|
||||||
@@ -540,5 +538,5 @@ class SysInfo(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Health(BaseModel):
|
class Health(BaseModel):
|
||||||
last_process: Optional[datetime]
|
last_process: Optional[datetime] = None
|
||||||
last_process_ts: Optional[int]
|
last_process_ts: Optional[int] = None
|
||||||
|
|||||||
@@ -175,9 +175,9 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
|||||||
leverage=payload.leverage)
|
leverage=payload.leverage)
|
||||||
|
|
||||||
if trade:
|
if trade:
|
||||||
return ForceEnterResponse.parse_obj(trade.to_json())
|
return ForceEnterResponse.model_validate(trade.to_json())
|
||||||
else:
|
else:
|
||||||
return ForceEnterResponse.parse_obj(
|
return ForceEnterResponse.model_validate(
|
||||||
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."})
|
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."})
|
||||||
|
|
||||||
|
|
||||||
@@ -282,14 +282,14 @@ def plot_config(strategy: Optional[str] = None, config=Depends(get_config),
|
|||||||
if not strategy:
|
if not strategy:
|
||||||
if not rpc:
|
if not rpc:
|
||||||
raise RPCException("Strategy is mandatory in webserver mode.")
|
raise RPCException("Strategy is mandatory in webserver mode.")
|
||||||
return PlotConfig.parse_obj(rpc._rpc_plot_config())
|
return PlotConfig.model_validate(rpc._rpc_plot_config())
|
||||||
else:
|
else:
|
||||||
config1 = deepcopy(config)
|
config1 = deepcopy(config)
|
||||||
config1.update({
|
config1.update({
|
||||||
'strategy': strategy
|
'strategy': strategy
|
||||||
})
|
})
|
||||||
try:
|
try:
|
||||||
return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1))
|
return PlotConfig.model_validate(RPC._rpc_plot_config_with_strategy(config1))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=502, detail=str(e))
|
raise HTTPException(status_code=502, detail=str(e))
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ async def _process_consumer_request(
|
|||||||
"""
|
"""
|
||||||
# Validate the request, makes sure it matches the schema
|
# Validate the request, makes sure it matches the schema
|
||||||
try:
|
try:
|
||||||
websocket_request = WSRequestSchema.parse_obj(request)
|
websocket_request = WSRequestSchema.model_validate(request)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Invalid request from {channel}: {e}")
|
logger.error(f"Invalid request from {channel}: {e}")
|
||||||
return
|
return
|
||||||
@@ -94,7 +94,7 @@ async def _process_consumer_request(
|
|||||||
|
|
||||||
# Format response
|
# Format response
|
||||||
response = WSWhitelistMessage(data=whitelist)
|
response = WSWhitelistMessage(data=whitelist)
|
||||||
await channel.send(response.dict(exclude_none=True))
|
await channel.send(response.model_dump(exclude_none=True))
|
||||||
|
|
||||||
elif type_ == RPCRequestType.ANALYZED_DF:
|
elif type_ == RPCRequestType.ANALYZED_DF:
|
||||||
# Limit the amount of candles per dataframe to 'limit' or 1500
|
# Limit the amount of candles per dataframe to 'limit' or 1500
|
||||||
@@ -105,7 +105,7 @@ async def _process_consumer_request(
|
|||||||
for message in rpc._ws_request_analyzed_df(limit, pair):
|
for message in rpc._ws_request_analyzed_df(limit, pair):
|
||||||
# Format response
|
# Format response
|
||||||
response = WSAnalyzedDFMessage(data=message)
|
response = WSAnalyzedDFMessage(data=message)
|
||||||
await channel.send(response.dict(exclude_none=True))
|
await channel.send(response.model_dump(exclude_none=True))
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/message/ws")
|
@router.websocket("/message/ws")
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ from datetime import datetime
|
|||||||
from typing import Any, Dict, List, Optional, TypedDict
|
from typing import Any, Dict, List, Optional, TypedDict
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
from freqtrade.constants import PairWithTimeframe
|
from freqtrade.constants import PairWithTimeframe
|
||||||
from freqtrade.enums.rpcmessagetype import RPCMessageType, RPCRequestType
|
from freqtrade.enums.rpcmessagetype import RPCMessageType, RPCRequestType
|
||||||
|
|
||||||
|
|
||||||
class BaseArbitraryModel(BaseModel):
|
class BaseArbitraryModel(BaseModel):
|
||||||
class Config:
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
arbitrary_types_allowed = True
|
|
||||||
|
|
||||||
|
|
||||||
class WSRequestSchema(BaseArbitraryModel):
|
class WSRequestSchema(BaseArbitraryModel):
|
||||||
@@ -27,9 +26,7 @@ class WSMessageSchemaType(TypedDict):
|
|||||||
class WSMessageSchema(BaseArbitraryModel):
|
class WSMessageSchema(BaseArbitraryModel):
|
||||||
type: RPCMessageType
|
type: RPCMessageType
|
||||||
data: Optional[Any] = None
|
data: Optional[Any] = None
|
||||||
|
model_config = ConfigDict(extra='allow')
|
||||||
class Config:
|
|
||||||
extra = 'allow'
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------ REQUEST SCHEMAS ----------------------------
|
# ------------------------------ REQUEST SCHEMAS ----------------------------
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def schema_to_dict(schema: Union[WSMessageSchema, WSRequestSchema]):
|
def schema_to_dict(schema: Union[WSMessageSchema, WSRequestSchema]):
|
||||||
return schema.dict(exclude_none=True)
|
return schema.model_dump(exclude_none=True)
|
||||||
|
|
||||||
|
|
||||||
class ExternalMessageConsumer:
|
class ExternalMessageConsumer:
|
||||||
@@ -322,7 +322,7 @@ class ExternalMessageConsumer:
|
|||||||
producer_name = producer.get('name', 'default')
|
producer_name = producer.get('name', 'default')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
producer_message = WSMessageSchema.parse_obj(message)
|
producer_message = WSMessageSchema.model_validate(message)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Invalid message from `{producer_name}`: {e}")
|
logger.error(f"Invalid message from `{producer_name}`: {e}")
|
||||||
return
|
return
|
||||||
@@ -344,7 +344,7 @@ class ExternalMessageConsumer:
|
|||||||
def _consume_whitelist_message(self, producer_name: str, message: WSMessageSchema):
|
def _consume_whitelist_message(self, producer_name: str, message: WSMessageSchema):
|
||||||
try:
|
try:
|
||||||
# Validate the message
|
# Validate the message
|
||||||
whitelist_message = WSWhitelistMessage.parse_obj(message)
|
whitelist_message = WSWhitelistMessage.model_validate(message.model_dump())
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Invalid message from `{producer_name}`: {e}")
|
logger.error(f"Invalid message from `{producer_name}`: {e}")
|
||||||
return
|
return
|
||||||
@@ -356,7 +356,7 @@ class ExternalMessageConsumer:
|
|||||||
|
|
||||||
def _consume_analyzed_df_message(self, producer_name: str, message: WSMessageSchema):
|
def _consume_analyzed_df_message(self, producer_name: str, message: WSMessageSchema):
|
||||||
try:
|
try:
|
||||||
df_message = WSAnalyzedDFMessage.parse_obj(message)
|
df_message = WSAnalyzedDFMessage.model_validate(message.model_dump())
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.error(f"Invalid message from `{producer_name}`: {e}")
|
logger.error(f"Invalid message from `{producer_name}`: {e}")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ coingecko_mapping = {
|
|||||||
'sol': 'solana',
|
'sol': 'solana',
|
||||||
'usdt': 'tether',
|
'usdt': 'tether',
|
||||||
'busd': 'binance-usd',
|
'busd': 'binance-usd',
|
||||||
|
'tusd': 'true-usd',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-11
@@ -612,17 +612,13 @@ class RPC:
|
|||||||
est_stake = balance.free
|
est_stake = balance.free
|
||||||
est_bot_stake = amount
|
est_bot_stake = amount
|
||||||
else:
|
else:
|
||||||
try:
|
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
rate: Optional[float] = tickers.get(pair, {}).get('last', None)
|
||||||
rate: Optional[float] = tickers.get(pair, {}).get('last', None)
|
if rate:
|
||||||
if rate:
|
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
rate = 1.0 / rate
|
||||||
rate = 1.0 / rate
|
est_stake = rate * balance.total
|
||||||
est_stake = rate * balance.total
|
est_bot_stake = rate * amount
|
||||||
est_bot_stake = rate * amount
|
|
||||||
except (ExchangeError):
|
|
||||||
logger.warning(f"Could not get rate for pair {coin}.")
|
|
||||||
raise ValueError()
|
|
||||||
|
|
||||||
return est_stake, est_bot_stake
|
return est_stake, est_bot_stake
|
||||||
|
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
When not implemented by a strategy, returns the initial stoploss value
|
When not implemented by a strategy, returns the initial stoploss value.
|
||||||
Only called when use_custom_stoploss is set to True.
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
:param pair: Pair that's currently analyzed
|
:param pair: Pair that's currently analyzed
|
||||||
@@ -1181,7 +1181,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
bound = (low if trade.is_short else high)
|
bound = (low if trade.is_short else high)
|
||||||
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
|
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
|
||||||
if self.use_custom_stoploss and dir_correct:
|
if self.use_custom_stoploss and dir_correct:
|
||||||
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
|
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None,
|
||||||
|
supress_error=True
|
||||||
)(pair=trade.pair, trade=trade,
|
)(pair=trade.pair, trade=trade,
|
||||||
current_time=current_time,
|
current_time=current_time,
|
||||||
current_rate=(bound or current_rate),
|
current_rate=(bound or current_rate),
|
||||||
|
|||||||
@@ -78,19 +78,7 @@ class {{ strategy }}(IStrategy):
|
|||||||
buy_rsi = IntParameter(10, 40, default=30, space="buy")
|
buy_rsi = IntParameter(10, 40, default=30, space="buy")
|
||||||
sell_rsi = IntParameter(60, 90, default=70, space="sell")
|
sell_rsi = IntParameter(60, 90, default=70, space="sell")
|
||||||
|
|
||||||
# Optional order type mapping.
|
{{ attributes | indent(4) }}
|
||||||
order_types = {
|
|
||||||
'entry': 'limit',
|
|
||||||
'exit': 'limit',
|
|
||||||
'stoploss': 'market',
|
|
||||||
'stoploss_on_exchange': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optional order time in force.
|
|
||||||
order_time_in_force = {
|
|
||||||
'entry': 'GTC',
|
|
||||||
'exit': 'GTC'
|
|
||||||
}
|
|
||||||
{{ plot_config | indent(4) }}
|
{{ plot_config | indent(4) }}
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Optional order type mapping.
|
||||||
|
order_types = {
|
||||||
|
'entry': 'limit',
|
||||||
|
'exit': 'limit',
|
||||||
|
'stoploss': 'market',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional order time in force.
|
||||||
|
order_time_in_force = {
|
||||||
|
'entry': 'GTC',
|
||||||
|
'exit': 'GTC'
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humani
|
|||||||
dt_utc, format_ms_time, shorten_date)
|
dt_utc, format_ms_time, shorten_date)
|
||||||
from freqtrade.util.ft_precise import FtPrecise
|
from freqtrade.util.ft_precise import FtPrecise
|
||||||
from freqtrade.util.periodic_cache import PeriodicCache
|
from freqtrade.util.periodic_cache import PeriodicCache
|
||||||
|
from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""
|
||||||
|
Jinja2 rendering utils, used to generate new strategy and configurations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_template(templatefile: str, arguments: dict = {}) -> str:
|
||||||
|
|
||||||
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
|
||||||
|
env = Environment(
|
||||||
|
loader=PackageLoader('freqtrade', 'templates'),
|
||||||
|
autoescape=select_autoescape(['html', 'xml'])
|
||||||
|
)
|
||||||
|
template = env.get_template(templatefile)
|
||||||
|
return template.render(**arguments)
|
||||||
|
|
||||||
|
|
||||||
|
def render_template_with_fallback(templatefile: str, templatefallbackfile: str,
|
||||||
|
arguments: dict = {}) -> str:
|
||||||
|
"""
|
||||||
|
Use templatefile if possible, otherwise fall back to templatefallbackfile
|
||||||
|
"""
|
||||||
|
from jinja2.exceptions import TemplateNotFound
|
||||||
|
try:
|
||||||
|
return render_template(templatefile, arguments)
|
||||||
|
except TemplateNotFound:
|
||||||
|
return render_template(templatefallbackfile, arguments)
|
||||||
@@ -47,7 +47,6 @@ nav:
|
|||||||
- Advanced Hyperopt: advanced-hyperopt.md
|
- Advanced Hyperopt: advanced-hyperopt.md
|
||||||
- Producer/Consumer mode: producer-consumer.md
|
- Producer/Consumer mode: producer-consumer.md
|
||||||
- Edge Positioning: edge.md
|
- Edge Positioning: edge.md
|
||||||
- Sandbox Testing: sandbox-testing.md
|
|
||||||
- FAQ: faq.md
|
- FAQ: faq.md
|
||||||
- SQL Cheat-sheet: sql_cheatsheet.md
|
- SQL Cheat-sheet: sql_cheatsheet.md
|
||||||
- Strategy migration: strategy_migration.md
|
- Strategy migration: strategy_migration.md
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ ignore = ["freqtrade/vendor/**"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
extend-exclude = [".env"]
|
extend-exclude = [".env", ".venv"]
|
||||||
target-version = "py38"
|
target-version = "py38"
|
||||||
extend-select = [
|
extend-select = [
|
||||||
"C90", # mccabe
|
"C90", # mccabe
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
ruff==0.0.282
|
ruff==0.0.285
|
||||||
mypy==1.4.1
|
mypy==1.5.1
|
||||||
pre-commit==3.3.3
|
pre-commit==3.3.3
|
||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
pytest-asyncio==0.21.1
|
pytest-asyncio==0.21.1
|
||||||
@@ -17,10 +17,10 @@ pytest-mock==3.11.1
|
|||||||
pytest-random-order==1.1.0
|
pytest-random-order==1.1.0
|
||||||
isort==5.12.0
|
isort==5.12.0
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.11.0
|
time-machine==2.12.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==7.7.3
|
nbconvert==7.7.4
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.3.0.6
|
types-cachetools==5.3.0.6
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# Required for freqai-rl
|
# Required for freqai-rl
|
||||||
torch==2.0.1
|
torch==2.0.1
|
||||||
#until these branches will be released we can use this
|
#until these branches will be released we can use this
|
||||||
gymnasium==0.28.1
|
gymnasium==0.29.1
|
||||||
stable_baselines3==2.0.0
|
stable_baselines3==2.1.0
|
||||||
sb3_contrib>=2.0.0a9
|
sb3_contrib>=2.0.0a9
|
||||||
# Progress bar for stable-baselines3 and sb3-contrib
|
# Progress bar for stable-baselines3 and sb3-contrib
|
||||||
tqdm==4.65.0
|
tqdm==4.66.1
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
# Required for freqai
|
# Required for freqai
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.1.3
|
||||||
joblib==1.3.1
|
joblib==1.3.2
|
||||||
catboost==1.2; 'arm' not in platform_machine
|
catboost==1.2; 'arm' not in platform_machine
|
||||||
lightgbm==4.0.0
|
lightgbm==4.0.0
|
||||||
xgboost==1.7.6
|
xgboost==1.7.6
|
||||||
tensorboard==2.13.0
|
tensorboard==2.14.0
|
||||||
datasieve==0.1.7
|
datasieve==0.1.7
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.11.1; python_version >= '3.9'
|
scipy==1.11.2; python_version >= '3.9'
|
||||||
scipy==1.10.1; python_version < '3.9'
|
scipy==1.10.1; python_version < '3.9'
|
||||||
scikit-learn==1.1.3
|
scikit-learn==1.1.3
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.15.0
|
plotly==5.16.1
|
||||||
|
|||||||
+11
-11
@@ -3,11 +3,11 @@ numpy==1.24.3; python_version <= '3.8'
|
|||||||
pandas==2.0.3
|
pandas==2.0.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.0.50
|
ccxt==4.0.71
|
||||||
cryptography==41.0.3; platform_machine != 'armv7l'
|
cryptography==41.0.3; platform_machine != 'armv7l'
|
||||||
cryptography==40.0.1; platform_machine == 'armv7l'
|
cryptography==40.0.1; platform_machine == 'armv7l'
|
||||||
aiohttp==3.8.5
|
aiohttp==3.8.5
|
||||||
SQLAlchemy==2.0.19
|
SQLAlchemy==2.0.20
|
||||||
python-telegram-bot==20.4
|
python-telegram-bot==20.4
|
||||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||||
httpx>=0.24.1
|
httpx>=0.24.1
|
||||||
@@ -15,15 +15,15 @@ arrow==1.2.3
|
|||||||
cachetools==5.3.1
|
cachetools==5.3.1
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
urllib3==2.0.4
|
urllib3==2.0.4
|
||||||
jsonschema==4.18.6
|
jsonschema==4.19.0
|
||||||
TA-Lib==0.4.27
|
TA-Lib==0.4.28
|
||||||
technical==1.4.0
|
technical==1.4.0
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
tables==3.8.0
|
tables==3.8.0
|
||||||
blosc==1.11.1
|
blosc==1.11.1
|
||||||
joblib==1.3.1
|
joblib==1.3.2
|
||||||
rich==13.5.2
|
rich==13.5.2
|
||||||
pyarrow==12.0.1; platform_machine != 'armv7l'
|
pyarrow==12.0.1; platform_machine != 'armv7l'
|
||||||
|
|
||||||
@@ -33,24 +33,24 @@ py_find_1st==1.1.5
|
|||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.10
|
python-rapidjson==1.10
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.9.3
|
orjson==3.9.5
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.101.0
|
fastapi==0.101.1
|
||||||
pydantic==1.10.11
|
pydantic==2.2.1
|
||||||
uvicorn==0.23.2
|
uvicorn==0.23.2
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
aiofiles==23.1.0
|
aiofiles==23.2.1
|
||||||
psutil==5.9.5
|
psutil==5.9.5
|
||||||
|
|
||||||
# Support for colorized terminal output
|
# Support for colorized terminal output
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
# Building config files interactively
|
# Building config files interactively
|
||||||
questionary==1.10.0
|
questionary==2.0.0
|
||||||
prompt-toolkit==3.0.39
|
prompt-toolkit==3.0.36
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ setup(
|
|||||||
'rich',
|
'rich',
|
||||||
'pyarrow; platform_machine != "armv7l"',
|
'pyarrow; platform_machine != "armv7l"',
|
||||||
'fastapi',
|
'fastapi',
|
||||||
'pydantic>=1.8.0,<2.0',
|
'pydantic>=2.2.0',
|
||||||
'uvicorn',
|
'uvicorn',
|
||||||
'psutil',
|
'psutil',
|
||||||
'pyjwt',
|
'pyjwt',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function check_installed_pip() {
|
|||||||
${PYTHON} -m pip > /dev/null
|
${PYTHON} -m pip > /dev/null
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo_block "Installing Pip for ${PYTHON}"
|
echo_block "Installing Pip for ${PYTHON}"
|
||||||
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
|
curl https://bootstrap.pypa.io/get-pip.py -s -o get-pip.py
|
||||||
${PYTHON} get-pip.py
|
${PYTHON} get-pip.py
|
||||||
rm get-pip.py
|
rm get-pip.py
|
||||||
fi
|
fi
|
||||||
@@ -41,12 +41,12 @@ function check_installed_python() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateenv() {
|
function updateenv() {
|
||||||
echo_block "Updating your virtual env"
|
echo_block "Updating your virtual environment"
|
||||||
if [ ! -f .env/bin/activate ]; then
|
if [ ! -f .venv/bin/activate ]; then
|
||||||
echo "Something went wrong, no virtual environment found."
|
echo "Something went wrong, no virtual environment found."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
source .env/bin/activate
|
source .venv/bin/activate
|
||||||
SYS_ARCH=$(uname -m)
|
SYS_ARCH=$(uname -m)
|
||||||
echo "pip install in-progress. Please wait..."
|
echo "pip install in-progress. Please wait..."
|
||||||
${PYTHON} -m pip install --upgrade pip wheel setuptools
|
${PYTHON} -m pip install --upgrade pip wheel setuptools
|
||||||
@@ -120,7 +120,7 @@ function updateenv() {
|
|||||||
|
|
||||||
# Install tab lib
|
# Install tab lib
|
||||||
function install_talib() {
|
function install_talib() {
|
||||||
if [ -f /usr/local/lib/libta_lib.a ]; then
|
if [ -f /usr/local/lib/libta_lib.a ] || [ -f /usr/local/lib/libta_lib.so ] || [ -f /usr/lib/libta_lib.so ]; then
|
||||||
echo "ta-lib already installed, skipping"
|
echo "ta-lib already installed, skipping"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -186,7 +186,14 @@ function install_redhat() {
|
|||||||
# Upgrade the bot
|
# Upgrade the bot
|
||||||
function update() {
|
function update() {
|
||||||
git pull
|
git pull
|
||||||
|
if [ -f .env/bin/activate ]; then
|
||||||
|
# Old environment found - updating to new environment.
|
||||||
|
recreate_environments
|
||||||
|
fi
|
||||||
updateenv
|
updateenv
|
||||||
|
echo "Update completed."
|
||||||
|
echo_block "Don't forget to activate your virtual enviorment with 'source .venv/bin/activate'!"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_git_changes() {
|
function check_git_changes() {
|
||||||
@@ -199,6 +206,27 @@ function check_git_changes() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recreate_environments() {
|
||||||
|
if [ -d ".env" ]; then
|
||||||
|
# Remove old virtual env
|
||||||
|
echo "- Deleting your previous virtual env"
|
||||||
|
echo "Warning: Your new environment will be at .venv!"
|
||||||
|
rm -rf .env
|
||||||
|
fi
|
||||||
|
if [ -d ".venv" ]; then
|
||||||
|
echo "- Deleting your previous virtual env"
|
||||||
|
rm -rf .venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
${PYTHON} -m venv .venv
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Could not create virtual environment. Leaving now"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
# Reset Develop or Stable branch
|
# Reset Develop or Stable branch
|
||||||
function reset() {
|
function reset() {
|
||||||
echo_block "Resetting branch and virtual env"
|
echo_block "Resetting branch and virtual env"
|
||||||
@@ -225,22 +253,13 @@ function reset() {
|
|||||||
else
|
else
|
||||||
echo "Reset ignored because you are not on 'stable' or 'develop'."
|
echo "Reset ignored because you are not on 'stable' or 'develop'."
|
||||||
fi
|
fi
|
||||||
|
recreate_environments
|
||||||
|
|
||||||
if [ -d ".env" ]; then
|
|
||||||
echo "- Deleting your previous virtual env"
|
|
||||||
rm -rf .env
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
${PYTHON} -m venv .env
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Could not create virtual environment. Leaving now"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
updateenv
|
updateenv
|
||||||
}
|
}
|
||||||
|
|
||||||
function config() {
|
function config() {
|
||||||
echo_block "Please use 'freqtrade new-config -c config.json' to generate a new configuration file."
|
echo_block "Please use 'freqtrade new-config -c user_data/config.json' to generate a new configuration file."
|
||||||
}
|
}
|
||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
@@ -266,9 +285,9 @@ function install() {
|
|||||||
reset
|
reset
|
||||||
config
|
config
|
||||||
echo_block "Run the bot !"
|
echo_block "Run the bot !"
|
||||||
echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade <subcommand>'."
|
echo "You can now use the bot by executing 'source .venv/bin/activate; freqtrade <subcommand>'."
|
||||||
echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'."
|
echo "You can see the list of available bot sub-commands by executing 'source .venv/bin/activate; freqtrade --help'."
|
||||||
echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'."
|
echo "You verify that freqtrade is installed successfully by running 'source .venv/bin/activate; freqtrade --version'."
|
||||||
}
|
}
|
||||||
|
|
||||||
function plot() {
|
function plot() {
|
||||||
|
|||||||
+10
-2
@@ -14,7 +14,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
from freqtrade.data.converter import ohlcv_to_dataframe, trades_list_to_df
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.enums import CandleType, MarginMode, RunMode, SignalDirection, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, RunMode, SignalDirection, TradingMode
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
@@ -2346,7 +2346,15 @@ def trades_history():
|
|||||||
[1565798399629, '1261813bb30', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
|
[1565798399629, '1261813bb30', None, 'buy', 0.019627, 0.244, 0.004788987999999999],
|
||||||
[1565798399752, '1261813cc31', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
[1565798399752, '1261813cc31', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
||||||
[1565798399862, '126181cc332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
[1565798399862, '126181cc332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999],
|
||||||
[1565798399872, '1261aa81333', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]]
|
[1565798399862, '126181cc333', None, 'sell', 0.019626, 0.012, 0.00021588599999999999],
|
||||||
|
[1565798399872, '1261aa81334', None, 'sell', 0.019626, 0.011, 0.00021588599999999999]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def trades_history_df(trades_history):
|
||||||
|
trades = trades_list_to_df(trades_history)
|
||||||
|
trades['date'] = pd.to_datetime(trades['timestamp'], unit='ms', utc=True)
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ from pathlib import Path
|
|||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.configuration.timerange import TimeRange
|
from freqtrade.configuration.timerange import TimeRange
|
||||||
from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format,
|
from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format,
|
||||||
ohlcv_fill_up_missing_data, ohlcv_to_dataframe,
|
ohlcv_fill_up_missing_data, ohlcv_to_dataframe,
|
||||||
reduce_dataframe_footprint, trades_dict_to_list,
|
reduce_dataframe_footprint, trades_df_remove_duplicates,
|
||||||
trades_remove_duplicates, trades_to_ohlcv, trim_dataframe)
|
trades_dict_to_list, trades_to_ohlcv, trim_dataframe)
|
||||||
from freqtrade.data.history import (get_timerange, load_data, load_pair_history,
|
from freqtrade.data.history import (get_timerange, load_data, load_pair_history,
|
||||||
validate_backtest_data)
|
validate_backtest_data)
|
||||||
from freqtrade.data.history.idatahandler import IDataHandler
|
from freqtrade.data.history.idatahandler import IDataHandler
|
||||||
@@ -34,26 +35,21 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog):
|
|||||||
assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog)
|
assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_trades_to_ohlcv(ohlcv_history_list, caplog):
|
def test_trades_to_ohlcv(trades_history_df, caplog):
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
with pytest.raises(ValueError, match="Trade-list empty."):
|
with pytest.raises(ValueError, match="Trade-list empty."):
|
||||||
trades_to_ohlcv([], '1m')
|
trades_to_ohlcv(pd.DataFrame(columns=trades_history_df.columns), '1m')
|
||||||
|
|
||||||
trades = [
|
df = trades_to_ohlcv(trades_history_df, '1m')
|
||||||
[1570752011620, "13519807", None, "sell", 0.00141342, 23.0, 0.03250866],
|
|
||||||
[1570752011620, "13519808", None, "sell", 0.00141266, 54.0, 0.07628364],
|
|
||||||
[1570752017964, "13519809", None, "sell", 0.00141266, 8.0, 0.01130128]]
|
|
||||||
|
|
||||||
df = trades_to_ohlcv(trades, '1m')
|
|
||||||
assert not df.empty
|
assert not df.empty
|
||||||
assert len(df) == 1
|
assert len(df) == 1
|
||||||
assert 'open' in df.columns
|
assert 'open' in df.columns
|
||||||
assert 'high' in df.columns
|
assert 'high' in df.columns
|
||||||
assert 'low' in df.columns
|
assert 'low' in df.columns
|
||||||
assert 'close' in df.columns
|
assert 'close' in df.columns
|
||||||
assert df.loc[:, 'high'][0] == 0.00141342
|
assert df.loc[:, 'high'][0] == 0.019627
|
||||||
assert df.loc[:, 'low'][0] == 0.00141266
|
assert df.loc[:, 'low'][0] == 0.019626
|
||||||
|
|
||||||
|
|
||||||
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
||||||
@@ -302,13 +298,13 @@ def test_trim_dataframe(testdatadir) -> None:
|
|||||||
assert all(data_modify.iloc[0] == data.iloc[25])
|
assert all(data_modify.iloc[0] == data.iloc[25])
|
||||||
|
|
||||||
|
|
||||||
def test_trades_remove_duplicates(trades_history):
|
def test_trades_df_remove_duplicates(trades_history_df):
|
||||||
trades_history1 = trades_history * 3
|
trades_history1 = pd.concat([trades_history_df, trades_history_df, trades_history_df]
|
||||||
assert len(trades_history1) == len(trades_history) * 3
|
).reset_index(drop=True)
|
||||||
res = trades_remove_duplicates(trades_history1)
|
assert len(trades_history1) == len(trades_history_df) * 3
|
||||||
assert len(res) == len(trades_history)
|
res = trades_df_remove_duplicates(trades_history1)
|
||||||
for i, t in enumerate(res):
|
assert len(res) == len(trades_history_df)
|
||||||
assert t == trades_history[i]
|
assert res.equals(trades_history_df)
|
||||||
|
|
||||||
|
|
||||||
def test_trades_dict_to_list(fetch_trades_result):
|
def test_trades_dict_to_list(fetch_trades_result):
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from pathlib import Path
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame, Timestamp
|
||||||
|
from pandas.testing import assert_frame_equal
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import AVAILABLE_DATAHANDLERS
|
from freqtrade.constants import AVAILABLE_DATAHANDLERS
|
||||||
@@ -117,12 +118,6 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
|
|||||||
assert set(paircombs) == {('UNITTEST/BTC', '5m', CandleType.SPOT)}
|
assert set(paircombs) == {('UNITTEST/BTC', '5m', CandleType.SPOT)}
|
||||||
|
|
||||||
|
|
||||||
def test_jsondatahandler_trades_get_pairs(testdatadir):
|
|
||||||
pairs = JsonGzDataHandler.trades_get_pairs(testdatadir)
|
|
||||||
# Convert to set to avoid failures due to sorting
|
|
||||||
assert set(pairs) == {'XRP/ETH', 'XRP/OLD'}
|
|
||||||
|
|
||||||
|
|
||||||
def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
|
def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
||||||
@@ -246,8 +241,10 @@ def test_datahandler__check_empty_df(testdatadir, caplog):
|
|||||||
assert log_has_re(expected_text, caplog)
|
assert log_has_re(expected_text, caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('datahandler', ['parquet'])
|
# @pytest.mark.parametrize('datahandler', [])
|
||||||
|
@pytest.mark.skip("All datahandlers currently support trades data.")
|
||||||
def test_datahandler_trades_not_supported(datahandler, testdatadir, ):
|
def test_datahandler_trades_not_supported(datahandler, testdatadir, ):
|
||||||
|
# Currently disabled. Reenable should a new provider not support trades data.
|
||||||
dh = get_datahandler(testdatadir, datahandler)
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
dh.trades_load('UNITTEST/ETH')
|
dh.trades_load('UNITTEST/ETH')
|
||||||
@@ -266,18 +263,6 @@ def test_jsondatahandler_trades_load(testdatadir, caplog):
|
|||||||
assert log_has(logmsg, caplog)
|
assert log_has(logmsg, caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_jsondatahandler_trades_purge(mocker, testdatadir):
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
|
||||||
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
|
||||||
dh = JsonGzDataHandler(testdatadir)
|
|
||||||
assert not dh.trades_purge('UNITTEST/NONEXIST')
|
|
||||||
assert unlinkmock.call_count == 0
|
|
||||||
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
|
||||||
assert dh.trades_purge('UNITTEST/NONEXIST')
|
|
||||||
assert unlinkmock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS)
|
@pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS)
|
||||||
def test_datahandler_ohlcv_append(datahandler, testdatadir, ):
|
def test_datahandler_ohlcv_append(datahandler, testdatadir, ):
|
||||||
dh = get_datahandler(testdatadir, datahandler)
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
@@ -291,79 +276,48 @@ def test_datahandler_ohlcv_append(datahandler, testdatadir, ):
|
|||||||
def test_datahandler_trades_append(datahandler, testdatadir):
|
def test_datahandler_trades_append(datahandler, testdatadir):
|
||||||
dh = get_datahandler(testdatadir, datahandler)
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
dh.trades_append('UNITTEST/ETH', [])
|
dh.trades_append('UNITTEST/ETH', DataFrame())
|
||||||
|
|
||||||
|
|
||||||
def test_hdf5datahandler_trades_get_pairs(testdatadir):
|
@pytest.mark.parametrize('datahandler,expected', [
|
||||||
pairs = HDF5DataHandler.trades_get_pairs(testdatadir)
|
('jsongz', {'XRP/ETH', 'XRP/OLD'}),
|
||||||
|
('hdf5', {'XRP/ETH'}),
|
||||||
|
('feather', {'XRP/ETH'}),
|
||||||
|
('parquet', {'XRP/ETH'}),
|
||||||
|
])
|
||||||
|
def test_datahandler_trades_get_pairs(testdatadir, datahandler, expected):
|
||||||
|
|
||||||
|
pairs = get_datahandlerclass(datahandler).trades_get_pairs(testdatadir)
|
||||||
# Convert to set to avoid failures due to sorting
|
# Convert to set to avoid failures due to sorting
|
||||||
assert set(pairs) == {'XRP/ETH'}
|
assert set(pairs) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_hdf5datahandler_trades_load(testdatadir):
|
def test_hdf5datahandler_trades_load(testdatadir):
|
||||||
dh = get_datahandler(testdatadir, 'hdf5')
|
dh = get_datahandler(testdatadir, 'hdf5')
|
||||||
trades = dh.trades_load('XRP/ETH')
|
trades = dh.trades_load('XRP/ETH')
|
||||||
assert isinstance(trades, list)
|
assert isinstance(trades, DataFrame)
|
||||||
|
|
||||||
trades1 = dh.trades_load('UNITTEST/NONEXIST')
|
trades1 = dh.trades_load('UNITTEST/NONEXIST')
|
||||||
assert trades1 == []
|
assert isinstance(trades1, DataFrame)
|
||||||
|
assert trades1.empty
|
||||||
# data goes from 2019-10-11 - 2019-10-13
|
# data goes from 2019-10-11 - 2019-10-13
|
||||||
timerange = TimeRange.parse_timerange('20191011-20191012')
|
timerange = TimeRange.parse_timerange('20191011-20191012')
|
||||||
|
|
||||||
trades2 = dh._trades_load('XRP/ETH', timerange)
|
trades2 = dh._trades_load('XRP/ETH', timerange)
|
||||||
assert len(trades) > len(trades2)
|
assert len(trades) > len(trades2)
|
||||||
# Check that ID is None (If it's nan, it's wrong)
|
# Check that ID is None (If it's nan, it's wrong)
|
||||||
assert trades2[0][2] is None
|
assert trades2.iloc[0]['type'] is None
|
||||||
|
|
||||||
# unfiltered load has trades before starttime
|
# unfiltered load has trades before starttime
|
||||||
assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0
|
|
||||||
|
assert len(trades.loc[trades['timestamp'] < timerange.startts * 1000]) >= 0
|
||||||
# filtered list does not have trades before starttime
|
# filtered list does not have trades before starttime
|
||||||
assert len([t for t in trades2 if t[0] < timerange.startts * 1000]) == 0
|
assert len(trades2.loc[trades2['timestamp'] < timerange.startts * 1000]) == 0
|
||||||
# unfiltered load has trades after endtime
|
# unfiltered load has trades after endtime
|
||||||
assert len([t for t in trades if t[0] > timerange.stopts * 1000]) > 0
|
assert len(trades.loc[trades['timestamp'] > timerange.stopts * 1000]) >= 0
|
||||||
# filtered list does not have trades after endtime
|
# filtered list does not have trades after endtime
|
||||||
assert len([t for t in trades2 if t[0] > timerange.stopts * 1000]) == 0
|
assert len(trades2.loc[trades2['timestamp'] > timerange.stopts * 1000]) == 0
|
||||||
|
# assert len([t for t in trades2 if t[0] > timerange.stopts * 1000]) == 0
|
||||||
|
|
||||||
def test_hdf5datahandler_trades_store(testdatadir, tmpdir):
|
|
||||||
tmpdir1 = Path(tmpdir)
|
|
||||||
dh = get_datahandler(testdatadir, 'hdf5')
|
|
||||||
trades = dh.trades_load('XRP/ETH')
|
|
||||||
|
|
||||||
dh1 = get_datahandler(tmpdir1, 'hdf5')
|
|
||||||
dh1.trades_store('XRP/NEW', trades)
|
|
||||||
file = tmpdir1 / 'XRP_NEW-trades.h5'
|
|
||||||
assert file.is_file()
|
|
||||||
# Load trades back
|
|
||||||
trades_new = dh1.trades_load('XRP/NEW')
|
|
||||||
|
|
||||||
assert len(trades_new) == len(trades)
|
|
||||||
assert trades[0][0] == trades_new[0][0]
|
|
||||||
assert trades[0][1] == trades_new[0][1]
|
|
||||||
# assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense
|
|
||||||
assert trades[0][3] == trades_new[0][3]
|
|
||||||
assert trades[0][4] == trades_new[0][4]
|
|
||||||
assert trades[0][5] == trades_new[0][5]
|
|
||||||
assert trades[0][6] == trades_new[0][6]
|
|
||||||
assert trades[-1][0] == trades_new[-1][0]
|
|
||||||
assert trades[-1][1] == trades_new[-1][1]
|
|
||||||
# assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense
|
|
||||||
assert trades[-1][3] == trades_new[-1][3]
|
|
||||||
assert trades[-1][4] == trades_new[-1][4]
|
|
||||||
assert trades[-1][5] == trades_new[-1][5]
|
|
||||||
assert trades[-1][6] == trades_new[-1][6]
|
|
||||||
|
|
||||||
|
|
||||||
def test_hdf5datahandler_trades_purge(mocker, testdatadir):
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
|
||||||
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
|
||||||
dh = get_datahandler(testdatadir, 'hdf5')
|
|
||||||
assert not dh.trades_purge('UNITTEST/NONEXIST')
|
|
||||||
assert unlinkmock.call_count == 0
|
|
||||||
|
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
|
||||||
assert dh.trades_purge('UNITTEST/NONEXIST')
|
|
||||||
assert unlinkmock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('pair,timeframe,candle_type,candle_append,startdt,enddt', [
|
@pytest.mark.parametrize('pair,timeframe,candle_type,candle_append,startdt,enddt', [
|
||||||
@@ -490,50 +444,42 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir):
|
|||||||
assert unlinkmock.call_count == 2
|
assert unlinkmock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_featherdatahandler_trades_load(testdatadir):
|
@pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet'])
|
||||||
dh = get_datahandler(testdatadir, 'feather')
|
def test_datahandler_trades_load(testdatadir, datahandler):
|
||||||
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
trades = dh.trades_load('XRP/ETH')
|
trades = dh.trades_load('XRP/ETH')
|
||||||
assert isinstance(trades, list)
|
assert isinstance(trades, DataFrame)
|
||||||
assert trades[0][0] == 1570752011620
|
assert trades.iloc[0]['timestamp'] == 1570752011620
|
||||||
assert trades[-1][-1] == 0.1986231
|
assert trades.iloc[0]['date'] == Timestamp('2019-10-11 00:00:11.620000+0000')
|
||||||
|
assert trades.iloc[-1]['cost'] == 0.1986231
|
||||||
|
|
||||||
trades1 = dh.trades_load('UNITTEST/NONEXIST')
|
trades1 = dh.trades_load('UNITTEST/NONEXIST')
|
||||||
assert trades1 == []
|
assert isinstance(trades, DataFrame)
|
||||||
|
assert trades1.empty
|
||||||
|
|
||||||
|
|
||||||
def test_featherdatahandler_trades_store(testdatadir, tmpdir):
|
@pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet'])
|
||||||
|
def test_datahandler_trades_store(testdatadir, tmpdir, datahandler):
|
||||||
tmpdir1 = Path(tmpdir)
|
tmpdir1 = Path(tmpdir)
|
||||||
dh = get_datahandler(testdatadir, 'feather')
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
trades = dh.trades_load('XRP/ETH')
|
trades = dh.trades_load('XRP/ETH')
|
||||||
|
|
||||||
dh1 = get_datahandler(tmpdir1, 'feather')
|
dh1 = get_datahandler(tmpdir1, datahandler)
|
||||||
dh1.trades_store('XRP/NEW', trades)
|
dh1.trades_store('XRP/NEW', trades)
|
||||||
file = tmpdir1 / 'XRP_NEW-trades.feather'
|
|
||||||
|
file = tmpdir1 / f'XRP_NEW-trades.{dh1._get_file_extension()}'
|
||||||
assert file.is_file()
|
assert file.is_file()
|
||||||
# Load trades back
|
# Load trades back
|
||||||
trades_new = dh1.trades_load('XRP/NEW')
|
trades_new = dh1.trades_load('XRP/NEW')
|
||||||
|
assert_frame_equal(trades, trades_new, check_exact=True)
|
||||||
assert len(trades_new) == len(trades)
|
assert len(trades_new) == len(trades)
|
||||||
assert trades[0][0] == trades_new[0][0]
|
|
||||||
assert trades[0][1] == trades_new[0][1]
|
|
||||||
# assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense
|
|
||||||
assert trades[0][3] == trades_new[0][3]
|
|
||||||
assert trades[0][4] == trades_new[0][4]
|
|
||||||
assert trades[0][5] == trades_new[0][5]
|
|
||||||
assert trades[0][6] == trades_new[0][6]
|
|
||||||
assert trades[-1][0] == trades_new[-1][0]
|
|
||||||
assert trades[-1][1] == trades_new[-1][1]
|
|
||||||
# assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense
|
|
||||||
assert trades[-1][3] == trades_new[-1][3]
|
|
||||||
assert trades[-1][4] == trades_new[-1][4]
|
|
||||||
assert trades[-1][5] == trades_new[-1][5]
|
|
||||||
assert trades[-1][6] == trades_new[-1][6]
|
|
||||||
|
|
||||||
|
|
||||||
def test_featherdatahandler_trades_purge(mocker, testdatadir):
|
@pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet'])
|
||||||
|
def test_datahandler_trades_purge(mocker, testdatadir, datahandler):
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
|
||||||
dh = get_datahandler(testdatadir, 'feather')
|
dh = get_datahandler(testdatadir, datahandler)
|
||||||
assert not dh.trades_purge('UNITTEST/NONEXIST')
|
assert not dh.trades_purge('UNITTEST/NONEXIST')
|
||||||
assert unlinkmock.call_count == 0
|
assert unlinkmock.call_count == 0
|
||||||
|
|
||||||
|
|||||||
@@ -129,9 +129,14 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type):
|
|||||||
default_conf["runmode"] = RunMode.BACKTEST
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
assert dp.runmode == RunMode.BACKTEST
|
assert dp.runmode == RunMode.BACKTEST
|
||||||
assert isinstance(dp.get_pair_dataframe(
|
df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)
|
||||||
"UNITTEST/BTC", timeframe, candle_type=candle_type), DataFrame)
|
assert isinstance(df, DataFrame)
|
||||||
# assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty
|
assert len(df) == 3 # ohlcv_history mock has just 3 rows
|
||||||
|
|
||||||
|
dp._set_dataframe_max_date(ohlcv_history.iloc[-1]['date'])
|
||||||
|
df = dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)
|
||||||
|
assert isinstance(df, DataFrame)
|
||||||
|
assert len(df) == 2 # ohlcv_history is limited to 2 rows now
|
||||||
|
|
||||||
|
|
||||||
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
def test_available_pairs(mocker, default_conf, ohlcv_history):
|
||||||
@@ -259,7 +264,7 @@ def test_orderbook(mocker, default_conf, order_book_l2):
|
|||||||
assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC'
|
assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC'
|
||||||
assert order_book_l2.call_args_list[0][0][1] >= 5
|
assert order_book_l2.call_args_list[0][0][1] >= 5
|
||||||
|
|
||||||
assert type(res) is dict
|
assert isinstance(res, dict)
|
||||||
assert 'bids' in res
|
assert 'bids' in res
|
||||||
assert 'asks' in res
|
assert 'asks' in res
|
||||||
|
|
||||||
@@ -272,7 +277,7 @@ def test_market(mocker, default_conf, markets):
|
|||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
res = dp.market('ETH/BTC')
|
res = dp.market('ETH/BTC')
|
||||||
|
|
||||||
assert type(res) is dict
|
assert isinstance(res, dict)
|
||||||
assert 'symbol' in res
|
assert 'symbol' in res
|
||||||
assert res['symbol'] == 'ETH/BTC'
|
assert res['symbol'] == 'ETH/BTC'
|
||||||
|
|
||||||
@@ -286,7 +291,7 @@ def test_ticker(mocker, default_conf, tickers):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
dp = DataProvider(default_conf, exchange)
|
dp = DataProvider(default_conf, exchange)
|
||||||
res = dp.ticker('ETH/BTC')
|
res = dp.ticker('ETH/BTC')
|
||||||
assert type(res) is dict
|
assert isinstance(res, dict)
|
||||||
assert 'symbol' in res
|
assert 'symbol' in res
|
||||||
assert res['symbol'] == 'ETH/BTC'
|
assert res['symbol'] == 'ETH/BTC'
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
@@ -26,7 +27,7 @@ from freqtrade.enums import CandleType
|
|||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.util import dt_utc
|
from freqtrade.util import dt_ts, dt_utc
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_patched_exchange, log_has, log_has_re,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_patched_exchange, log_has, log_has_re,
|
||||||
patch_exchange)
|
patch_exchange)
|
||||||
|
|
||||||
@@ -569,7 +570,10 @@ def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, tes
|
|||||||
|
|
||||||
|
|
||||||
def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog,
|
def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog,
|
||||||
tmpdir) -> None:
|
tmpdir, time_machine) -> None:
|
||||||
|
start_dt = dt_utc(2023, 1, 1)
|
||||||
|
time_machine.move_to(start_dt, tick=False)
|
||||||
|
|
||||||
tmpdir1 = Path(tmpdir)
|
tmpdir1 = Path(tmpdir)
|
||||||
ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history))
|
ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history))
|
||||||
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
mocker.patch(f'{EXMS}.get_historic_trades', ght_mock)
|
||||||
@@ -581,8 +585,13 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
|||||||
|
|
||||||
assert _download_trades_history(data_handler=data_handler, exchange=exchange,
|
assert _download_trades_history(data_handler=data_handler, exchange=exchange,
|
||||||
pair='ETH/BTC')
|
pair='ETH/BTC')
|
||||||
assert log_has("New Amount of trades: 5", caplog)
|
assert log_has("Current Amount of trades: 0", caplog)
|
||||||
|
assert log_has("New Amount of trades: 6", caplog)
|
||||||
|
assert ght_mock.call_count == 1
|
||||||
|
# Default "since" - 30 days before current day.
|
||||||
|
assert ght_mock.call_args_list[0][1]['since'] == dt_ts(start_dt - timedelta(days=30))
|
||||||
assert file1.is_file()
|
assert file1.is_file()
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
ght_mock.reset_mock()
|
ght_mock.reset_mock()
|
||||||
since_time = int(trades_history[-3][0] // 1000)
|
since_time = int(trades_history[-3][0] // 1000)
|
||||||
@@ -599,6 +608,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
|||||||
file1.unlink()
|
file1.unlink()
|
||||||
|
|
||||||
mocker.patch(f'{EXMS}.get_historic_trades', MagicMock(side_effect=ValueError))
|
mocker.patch(f'{EXMS}.get_historic_trades', MagicMock(side_effect=ValueError))
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
assert not _download_trades_history(data_handler=data_handler, exchange=exchange,
|
assert not _download_trades_history(data_handler=data_handler, exchange=exchange,
|
||||||
pair='ETH/BTC')
|
pair='ETH/BTC')
|
||||||
@@ -620,7 +630,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad
|
|||||||
|
|
||||||
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time
|
assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time
|
||||||
assert ght_mock.call_args_list[0][1]['from_id'] is None
|
assert ght_mock.call_args_list[0][1]['from_id'] is None
|
||||||
assert log_has_re(r'Start earlier than available data. Redownloading trades for.*', caplog)
|
assert log_has_re(r'Start .* earlier than available data. Redownloading trades for.*', caplog)
|
||||||
_clean_test_file(file2)
|
_clean_test_file(file2)
|
||||||
|
|
||||||
|
|
||||||
@@ -651,10 +661,10 @@ def test_convert_trades_to_ohlcv(testdatadir, tmpdir, caplog):
|
|||||||
|
|
||||||
assert_frame_equal(dfbak_1m, df_1m, check_exact=True)
|
assert_frame_equal(dfbak_1m, df_1m, check_exact=True)
|
||||||
assert_frame_equal(dfbak_5m, df_5m, check_exact=True)
|
assert_frame_equal(dfbak_5m, df_5m, check_exact=True)
|
||||||
|
msg = 'Could not convert NoDatapair to OHLCV.'
|
||||||
assert not log_has('Could not convert NoDatapair to OHLCV.', caplog)
|
assert not log_has(msg, caplog)
|
||||||
|
|
||||||
convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'],
|
convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'],
|
||||||
data_format_trades='jsongz',
|
data_format_trades='jsongz',
|
||||||
datadir=tmpdir1, timerange=tr, erase=True)
|
datadir=tmpdir1, timerange=tr, erase=True)
|
||||||
assert log_has('Could not convert NoDatapair to OHLCV.', caplog)
|
assert log_has(msg, caplog)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def test__get_params_binance(default_conf, mocker, side, type, time_in_force, ex
|
|||||||
])
|
])
|
||||||
def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
|
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
|||||||
@@ -556,41 +556,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
|||||||
assert result == 4000
|
assert result == 4000
|
||||||
|
|
||||||
|
|
||||||
def test_set_sandbox(default_conf, mocker):
|
|
||||||
"""
|
|
||||||
Test working scenario
|
|
||||||
"""
|
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.load_markets = MagicMock(return_value={
|
|
||||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
|
||||||
})
|
|
||||||
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
|
|
||||||
'api': 'https://api.gdax.com'})
|
|
||||||
type(api_mock).urls = url_mock
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
liveurl = exchange._api.urls['api']
|
|
||||||
default_conf['exchange']['sandbox'] = True
|
|
||||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
|
||||||
assert exchange._api.urls['api'] != liveurl
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_sandbox_exception(default_conf, mocker):
|
|
||||||
"""
|
|
||||||
Test Fail scenario
|
|
||||||
"""
|
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.load_markets = MagicMock(return_value={
|
|
||||||
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
|
||||||
})
|
|
||||||
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
|
|
||||||
type(api_mock).urls = url_mock
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
default_conf['exchange']['sandbox'] = True
|
|
||||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
|
||||||
|
|
||||||
|
|
||||||
def test__load_async_markets(default_conf, mocker, caplog):
|
def test__load_async_markets(default_conf, mocker, caplog):
|
||||||
mocker.patch(f'{EXMS}._init_ccxt')
|
mocker.patch(f'{EXMS}._init_ccxt')
|
||||||
mocker.patch(f'{EXMS}.validate_pairs')
|
mocker.patch(f'{EXMS}.validate_pairs')
|
||||||
@@ -1372,7 +1337,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name):
|
def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
|
order_id = f'test_prod_{side}_{randint(0, 10 ** 6)}'
|
||||||
api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True}
|
api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@@ -1452,7 +1417,7 @@ def test_buy_dry_run(default_conf, mocker, exchange_name):
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_buy_prod(default_conf, mocker, exchange_name):
|
def test_buy_prod(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'market'
|
order_type = 'market'
|
||||||
time_in_force = 'gtc'
|
time_in_force = 'gtc'
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
@@ -1541,7 +1506,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@@ -1608,7 +1573,7 @@ def test_sell_dry_run(default_conf, mocker):
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_sell_prod(default_conf, mocker, exchange_name):
|
def test_sell_prod(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'market'
|
order_type = 'market'
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
@@ -1686,7 +1651,7 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'symbol': 'ETH/BTC',
|
'symbol': 'ETH/BTC',
|
||||||
@@ -2505,7 +2470,7 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
|||||||
assert exchange._klines
|
assert exchange._klines
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
|
|
||||||
assert type(res) is dict
|
assert isinstance(res, dict)
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
# Test that each is in list at least once as order is not guaranteed
|
# Test that each is in list at least once as order is not guaranteed
|
||||||
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
|
||||||
@@ -2889,7 +2854,7 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
|||||||
|
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
res = await exchange._async_fetch_trades(pair, since=None, params=None)
|
res = await exchange._async_fetch_trades(pair, since=None, params=None)
|
||||||
assert type(res) is list
|
assert isinstance(res, list)
|
||||||
assert isinstance(res[0], list)
|
assert isinstance(res[0], list)
|
||||||
assert isinstance(res[1], list)
|
assert isinstance(res[1], list)
|
||||||
|
|
||||||
@@ -2989,9 +2954,9 @@ async def test__async_get_trade_history_id(default_conf, mocker, exchange_name,
|
|||||||
ret = await exchange._async_get_trade_history_id(pair,
|
ret = await exchange._async_get_trade_history_id(pair,
|
||||||
since=fetch_trades_result[0]['timestamp'],
|
since=fetch_trades_result[0]['timestamp'],
|
||||||
until=fetch_trades_result[-1]['timestamp'] - 1)
|
until=fetch_trades_result[-1]['timestamp'] - 1)
|
||||||
assert type(ret) is tuple
|
assert isinstance(ret, tuple)
|
||||||
assert ret[0] == pair
|
assert ret[0] == pair
|
||||||
assert type(ret[1]) is list
|
assert isinstance(ret[1], list)
|
||||||
assert len(ret[1]) == len(fetch_trades_result)
|
assert len(ret[1]) == len(fetch_trades_result)
|
||||||
assert exchange._api_async.fetch_trades.call_count == 3
|
assert exchange._api_async.fetch_trades.call_count == 3
|
||||||
fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
|
fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
|
||||||
@@ -3027,9 +2992,9 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
|
|||||||
pair,
|
pair,
|
||||||
since=fetch_trades_result[0]['timestamp'],
|
since=fetch_trades_result[0]['timestamp'],
|
||||||
until=fetch_trades_result[-1]['timestamp'] - 1)
|
until=fetch_trades_result[-1]['timestamp'] - 1)
|
||||||
assert type(ret) is tuple
|
assert isinstance(ret, tuple)
|
||||||
assert ret[0] == pair
|
assert ret[0] == pair
|
||||||
assert type(ret[1]) is list
|
assert isinstance(ret[1], list)
|
||||||
assert len(ret[1]) == len(fetch_trades_result)
|
assert len(ret[1]) == len(fetch_trades_result)
|
||||||
assert exchange._api_async.fetch_trades.call_count == 2
|
assert exchange._api_async.fetch_trades.call_count == 2
|
||||||
fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
|
fetch_trades_cal = exchange._api_async.fetch_trades.call_args_list
|
||||||
@@ -3063,9 +3028,9 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
|
|||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
|
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
|
||||||
until=trades_history[-1][0] - 1)
|
until=trades_history[-1][0] - 1)
|
||||||
assert type(ret) is tuple
|
assert isinstance(ret, tuple)
|
||||||
assert ret[0] == pair
|
assert ret[0] == pair
|
||||||
assert type(ret[1]) is list
|
assert isinstance(ret[1], list)
|
||||||
assert len(ret[1]) == len(trades_history) - 1
|
assert len(ret[1]) == len(trades_history) - 1
|
||||||
assert exchange._async_fetch_trades.call_count == 2
|
assert exchange._async_fetch_trades.call_count == 2
|
||||||
fetch_trades_cal = exchange._async_fetch_trades.call_args_list
|
fetch_trades_cal = exchange._async_fetch_trades.call_args_list
|
||||||
@@ -3557,7 +3522,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
|
|||||||
|
|
||||||
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
|
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
|
||||||
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
||||||
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
|
with pytest.raises(ValueError, match=r"Could not combine.* to get a valid pair."):
|
||||||
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
||||||
|
|
||||||
|
|
||||||
@@ -5392,7 +5357,7 @@ def test_get_liquidation_price(
|
|||||||
])
|
])
|
||||||
def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amount):
|
def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amount):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
|||||||
])
|
])
|
||||||
def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'stop-limit'
|
order_type = 'stop-limit'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit'
|
|||||||
|
|
||||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
time_in_force = 'ioc'
|
time_in_force = 'ioc'
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
@@ -56,7 +56,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
|||||||
|
|
||||||
def test_sell_kraken_trading_agreement(default_conf, mocker):
|
def test_sell_kraken_trading_agreement(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_sell_{randint(0, 10 ** 6)}'
|
||||||
order_type = 'market'
|
order_type = 'market'
|
||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
@@ -181,7 +181,7 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
])
|
])
|
||||||
def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
|
def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
|||||||
])
|
])
|
||||||
def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
|
def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = f'test_prod_buy_{randint(0, 10 ** 6)}'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@@ -136,7 +136,7 @@ def test_stoploss_adjust_kucoin(mocker, default_conf):
|
|||||||
])
|
])
|
||||||
def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate):
|
def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
|
order_id = f'test_prod_{side}_{randint(0, 10 ** 6)}'
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'info': {
|
'info': {
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.constants import Config
|
||||||
|
from freqtrade.exchange.exchange import Exchange
|
||||||
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
|
from tests.conftest import EXMS, get_default_conf_usdt
|
||||||
|
|
||||||
|
|
||||||
|
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
|
||||||
|
|
||||||
|
# Exchanges that should be tested online
|
||||||
|
EXCHANGES = {
|
||||||
|
'bittrex': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': False,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'leverage_tiers_public': False,
|
||||||
|
'leverage_in_spot_market': False,
|
||||||
|
},
|
||||||
|
'binance': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'use_ci_proxy': True,
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'futures': True,
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'hasQuoteVolumeFutures': True,
|
||||||
|
'leverage_tiers_public': False,
|
||||||
|
'leverage_in_spot_market': False,
|
||||||
|
'trades_lookback_hours': 4,
|
||||||
|
'private_methods': [
|
||||||
|
'fapiPrivateGetPositionSideDual',
|
||||||
|
'fapiPrivateGetMultiAssetsMargin'
|
||||||
|
],
|
||||||
|
'sample_order': [{
|
||||||
|
"symbol": "SOLUSDT",
|
||||||
|
"orderId": 3551312894,
|
||||||
|
"orderListId": -1,
|
||||||
|
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
||||||
|
"transactTime": 1674493798550,
|
||||||
|
"price": "15.50000000",
|
||||||
|
"origQty": "1.10000000",
|
||||||
|
"executedQty": "0.00000000",
|
||||||
|
"cummulativeQuoteQty": "0.00000000",
|
||||||
|
"status": "NEW",
|
||||||
|
"timeInForce": "GTC",
|
||||||
|
"type": "LIMIT",
|
||||||
|
"side": "BUY",
|
||||||
|
"workingTime": 1674493798550,
|
||||||
|
"fills": [],
|
||||||
|
"selfTradePreventionMode": "NONE",
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
'binanceus': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'futures': False,
|
||||||
|
'sample_order': [{
|
||||||
|
"symbol": "SOLUSDT",
|
||||||
|
"orderId": 3551312894,
|
||||||
|
"orderListId": -1,
|
||||||
|
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
||||||
|
"transactTime": 1674493798550,
|
||||||
|
"price": "15.50000000",
|
||||||
|
"origQty": "1.10000000",
|
||||||
|
"executedQty": "0.00000000",
|
||||||
|
"cummulativeQuoteQty": "0.00000000",
|
||||||
|
"status": "NEW",
|
||||||
|
"timeInForce": "GTC",
|
||||||
|
"type": "LIMIT",
|
||||||
|
"side": "BUY",
|
||||||
|
"workingTime": 1674493798550,
|
||||||
|
"fills": [],
|
||||||
|
"selfTradePreventionMode": "NONE",
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
'kraken': {
|
||||||
|
'pair': 'BTC/USD',
|
||||||
|
'stake_currency': 'USD',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'leverage_tiers_public': False,
|
||||||
|
'leverage_in_spot_market': True,
|
||||||
|
'trades_lookback_hours': 12,
|
||||||
|
},
|
||||||
|
'kucoin': {
|
||||||
|
'pair': 'XRP/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'leverage_tiers_public': False,
|
||||||
|
'leverage_in_spot_market': True,
|
||||||
|
'sample_order': [
|
||||||
|
{'id': '63d6742d0adc5570001d2bbf7'}, # create order
|
||||||
|
{
|
||||||
|
'id': '63d6742d0adc5570001d2bbf7',
|
||||||
|
'symbol': 'SOL-USDT',
|
||||||
|
'opType': 'DEAL',
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'price': '15.5',
|
||||||
|
'size': '1.1',
|
||||||
|
'funds': '0',
|
||||||
|
'dealFunds': '17.05',
|
||||||
|
'dealSize': '1.1',
|
||||||
|
'fee': '0.000065252',
|
||||||
|
'feeCurrency': 'USDT',
|
||||||
|
'stp': '',
|
||||||
|
'stop': '',
|
||||||
|
'stopTriggered': False,
|
||||||
|
'stopPrice': '0',
|
||||||
|
'timeInForce': 'GTC',
|
||||||
|
'postOnly': False,
|
||||||
|
'hidden': False,
|
||||||
|
'iceberg': False,
|
||||||
|
'visibleSize': '0',
|
||||||
|
'cancelAfter': 0,
|
||||||
|
'channel': 'API',
|
||||||
|
'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1',
|
||||||
|
'remark': None,
|
||||||
|
'tags': 'partner:ccxt',
|
||||||
|
'isActive': False,
|
||||||
|
'cancelExist': False,
|
||||||
|
'createdAt': 1674493798550,
|
||||||
|
'tradeType': 'TRADE'
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
'gate': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'futures': True,
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'hasQuoteVolumeFutures': True,
|
||||||
|
'leverage_tiers_public': True,
|
||||||
|
'leverage_in_spot_market': True,
|
||||||
|
'sample_order': [
|
||||||
|
{
|
||||||
|
"id": "276266139423",
|
||||||
|
"text": "apiv4",
|
||||||
|
"create_time": "1674493798",
|
||||||
|
"update_time": "1674493798",
|
||||||
|
"create_time_ms": "1674493798550",
|
||||||
|
"update_time_ms": "1674493798550",
|
||||||
|
"status": "closed",
|
||||||
|
"currency_pair": "SOL_USDT",
|
||||||
|
"type": "limit",
|
||||||
|
"account": "spot",
|
||||||
|
"side": "buy",
|
||||||
|
"amount": "1.1",
|
||||||
|
"price": "15.5",
|
||||||
|
"time_in_force": "gtc",
|
||||||
|
"iceberg": "0",
|
||||||
|
"left": "0",
|
||||||
|
"fill_price": "17.05",
|
||||||
|
"filled_total": "17.05",
|
||||||
|
"avg_deal_price": "15.5",
|
||||||
|
"fee": "0.0000018",
|
||||||
|
"fee_currency": "SOL",
|
||||||
|
"point_fee": "0",
|
||||||
|
"gt_fee": "0",
|
||||||
|
"gt_maker_fee": "0",
|
||||||
|
"gt_taker_fee": "0.0015",
|
||||||
|
"gt_discount": True,
|
||||||
|
"rebated_fee": "0",
|
||||||
|
"rebated_fee_currency": "USDT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# market order
|
||||||
|
'id': '276401180529',
|
||||||
|
'text': 'apiv4',
|
||||||
|
'create_time': '1674493798',
|
||||||
|
'update_time': '1674493798',
|
||||||
|
'create_time_ms': '1674493798550',
|
||||||
|
'update_time_ms': '1674493798550',
|
||||||
|
'status': 'cancelled',
|
||||||
|
'currency_pair': 'SOL_USDT',
|
||||||
|
'type': 'market',
|
||||||
|
'account': 'spot',
|
||||||
|
'side': 'buy',
|
||||||
|
'amount': '17.05',
|
||||||
|
'price': '0',
|
||||||
|
'time_in_force': 'ioc',
|
||||||
|
'iceberg': '0',
|
||||||
|
'left': '0.0000000016228',
|
||||||
|
'fill_price': '17.05',
|
||||||
|
'filled_total': '17.05',
|
||||||
|
'avg_deal_price': '15.5',
|
||||||
|
'fee': '0',
|
||||||
|
'fee_currency': 'SOL',
|
||||||
|
'point_fee': '0.0199999999967544',
|
||||||
|
'gt_fee': '0',
|
||||||
|
'gt_maker_fee': '0',
|
||||||
|
'gt_taker_fee': '0',
|
||||||
|
'gt_discount': False,
|
||||||
|
'rebated_fee': '0',
|
||||||
|
'rebated_fee_currency': 'USDT'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'okx': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'futures': True,
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'hasQuoteVolumeFutures': False,
|
||||||
|
'leverage_tiers_public': True,
|
||||||
|
'leverage_in_spot_market': True,
|
||||||
|
'private_methods': ['fetch_accounts'],
|
||||||
|
},
|
||||||
|
'bybit': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'use_ci_proxy': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'futures': True,
|
||||||
|
'leverage_tiers_public': True,
|
||||||
|
'leverage_in_spot_market': True,
|
||||||
|
'sample_order': [
|
||||||
|
{
|
||||||
|
"orderId": "1274754916287346280",
|
||||||
|
"orderLinkId": "1666798627015730",
|
||||||
|
"symbol": "SOLUSDT",
|
||||||
|
"createTime": "1674493798550",
|
||||||
|
"orderPrice": "15.5",
|
||||||
|
"orderQty": "1.1",
|
||||||
|
"orderType": "LIMIT",
|
||||||
|
"side": "BUY",
|
||||||
|
"status": "NEW",
|
||||||
|
"timeInForce": "GTC",
|
||||||
|
"accountId": "5555555",
|
||||||
|
"execQty": "0",
|
||||||
|
"orderCategory": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'huobi': {
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'futures': False,
|
||||||
|
},
|
||||||
|
'bitvavo': {
|
||||||
|
'pair': 'BTC/EUR',
|
||||||
|
'stake_currency': 'EUR',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '1h',
|
||||||
|
'leverage_tiers_public': False,
|
||||||
|
'leverage_in_spot_market': False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="class")
|
||||||
|
def exchange_conf():
|
||||||
|
config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve())
|
||||||
|
config['exchange']['pair_whitelist'] = []
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
config['dry_run'] = False
|
||||||
|
config['entry_pricing']['use_order_book'] = True
|
||||||
|
config['exit_pricing']['use_order_book'] = True
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def set_test_proxy(config: Config, use_proxy: bool) -> Config:
|
||||||
|
# Set proxy to test in CI.
|
||||||
|
import os
|
||||||
|
if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')):
|
||||||
|
config1 = deepcopy(config)
|
||||||
|
config1['exchange']['ccxt_config'] = {
|
||||||
|
"httpsProxy": proxy,
|
||||||
|
}
|
||||||
|
return config1
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange(exchange_name, exchange_conf):
|
||||||
|
exchange_conf = set_test_proxy(
|
||||||
|
exchange_conf, EXCHANGES[exchange_name].get('use_ci_proxy', False))
|
||||||
|
exchange_conf['exchange']['name'] = exchange_name
|
||||||
|
exchange_conf['stake_currency'] = EXCHANGES[exchange_name]['stake_currency']
|
||||||
|
exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True,
|
||||||
|
load_leverage_tiers=True)
|
||||||
|
|
||||||
|
yield exchange, exchange_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_futures_exchange(exchange_name, exchange_conf, class_mocker):
|
||||||
|
if EXCHANGES[exchange_name].get('futures') is not True:
|
||||||
|
pytest.skip(f"Exchange {exchange_name} does not support futures.")
|
||||||
|
else:
|
||||||
|
exchange_conf = deepcopy(exchange_conf)
|
||||||
|
exchange_conf = set_test_proxy(
|
||||||
|
exchange_conf, EXCHANGES[exchange_name].get('use_ci_proxy', False))
|
||||||
|
exchange_conf['trading_mode'] = 'futures'
|
||||||
|
exchange_conf['margin_mode'] = 'isolated'
|
||||||
|
|
||||||
|
class_mocker.patch(
|
||||||
|
'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
|
||||||
|
class_mocker.patch(f'{EXMS}.fetch_trading_fees')
|
||||||
|
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
|
||||||
|
class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init')
|
||||||
|
class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init')
|
||||||
|
class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None)
|
||||||
|
class_mocker.patch(f'{EXMS}.cache_leverage_tiers')
|
||||||
|
|
||||||
|
yield from get_exchange(exchange_name, exchange_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||||
|
def exchange(request, exchange_conf):
|
||||||
|
yield from get_exchange(request.param, exchange_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||||
|
def exchange_futures(request, exchange_conf, class_mocker):
|
||||||
|
|
||||||
|
yield from get_futures_exchange(request.param, exchange_conf, class_mocker)
|
||||||
@@ -5,338 +5,14 @@ However, these tests should give a good idea to determine if a new exchange is
|
|||||||
suitable to run with freqtrade.
|
suitable to run with freqtrade.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from copy import deepcopy
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
|
from freqtrade.exchange.exchange import timeframe_to_msecs
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES
|
||||||
from tests.conftest import EXMS, get_default_conf_usdt
|
|
||||||
|
|
||||||
|
|
||||||
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
|
|
||||||
|
|
||||||
# Exchanges that should be tested
|
|
||||||
EXCHANGES = {
|
|
||||||
'bittrex': {
|
|
||||||
'pair': 'BTC/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'hasQuoteVolume': False,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'leverage_tiers_public': False,
|
|
||||||
'leverage_in_spot_market': False,
|
|
||||||
},
|
|
||||||
'binance': {
|
|
||||||
'pair': 'BTC/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'use_ci_proxy': True,
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'futures': True,
|
|
||||||
'futures_pair': 'BTC/USDT:USDT',
|
|
||||||
'hasQuoteVolumeFutures': True,
|
|
||||||
'leverage_tiers_public': False,
|
|
||||||
'leverage_in_spot_market': False,
|
|
||||||
'trades_lookback_hours': 4,
|
|
||||||
'private_methods': [
|
|
||||||
'fapiPrivateGetPositionSideDual',
|
|
||||||
'fapiPrivateGetMultiAssetsMargin'
|
|
||||||
],
|
|
||||||
'sample_order': [{
|
|
||||||
"symbol": "SOLUSDT",
|
|
||||||
"orderId": 3551312894,
|
|
||||||
"orderListId": -1,
|
|
||||||
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
|
||||||
"transactTime": 1674493798550,
|
|
||||||
"price": "15.50000000",
|
|
||||||
"origQty": "1.10000000",
|
|
||||||
"executedQty": "0.00000000",
|
|
||||||
"cummulativeQuoteQty": "0.00000000",
|
|
||||||
"status": "NEW",
|
|
||||||
"timeInForce": "GTC",
|
|
||||||
"type": "LIMIT",
|
|
||||||
"side": "BUY",
|
|
||||||
"workingTime": 1674493798550,
|
|
||||||
"fills": [],
|
|
||||||
"selfTradePreventionMode": "NONE",
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
'binanceus': {
|
|
||||||
'pair': 'BTC/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'futures': False,
|
|
||||||
'sample_order': [{
|
|
||||||
"symbol": "SOLUSDT",
|
|
||||||
"orderId": 3551312894,
|
|
||||||
"orderListId": -1,
|
|
||||||
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
|
|
||||||
"transactTime": 1674493798550,
|
|
||||||
"price": "15.50000000",
|
|
||||||
"origQty": "1.10000000",
|
|
||||||
"executedQty": "0.00000000",
|
|
||||||
"cummulativeQuoteQty": "0.00000000",
|
|
||||||
"status": "NEW",
|
|
||||||
"timeInForce": "GTC",
|
|
||||||
"type": "LIMIT",
|
|
||||||
"side": "BUY",
|
|
||||||
"workingTime": 1674493798550,
|
|
||||||
"fills": [],
|
|
||||||
"selfTradePreventionMode": "NONE",
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
'kraken': {
|
|
||||||
'pair': 'BTC/USD',
|
|
||||||
'stake_currency': 'USD',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'leverage_tiers_public': False,
|
|
||||||
'leverage_in_spot_market': True,
|
|
||||||
'trades_lookback_hours': 12,
|
|
||||||
},
|
|
||||||
'kucoin': {
|
|
||||||
'pair': 'XRP/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'leverage_tiers_public': False,
|
|
||||||
'leverage_in_spot_market': True,
|
|
||||||
'sample_order': [
|
|
||||||
{'id': '63d6742d0adc5570001d2bbf7'}, # create order
|
|
||||||
{
|
|
||||||
'id': '63d6742d0adc5570001d2bbf7',
|
|
||||||
'symbol': 'SOL-USDT',
|
|
||||||
'opType': 'DEAL',
|
|
||||||
'type': 'limit',
|
|
||||||
'side': 'buy',
|
|
||||||
'price': '15.5',
|
|
||||||
'size': '1.1',
|
|
||||||
'funds': '0',
|
|
||||||
'dealFunds': '17.05',
|
|
||||||
'dealSize': '1.1',
|
|
||||||
'fee': '0.000065252',
|
|
||||||
'feeCurrency': 'USDT',
|
|
||||||
'stp': '',
|
|
||||||
'stop': '',
|
|
||||||
'stopTriggered': False,
|
|
||||||
'stopPrice': '0',
|
|
||||||
'timeInForce': 'GTC',
|
|
||||||
'postOnly': False,
|
|
||||||
'hidden': False,
|
|
||||||
'iceberg': False,
|
|
||||||
'visibleSize': '0',
|
|
||||||
'cancelAfter': 0,
|
|
||||||
'channel': 'API',
|
|
||||||
'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1',
|
|
||||||
'remark': None,
|
|
||||||
'tags': 'partner:ccxt',
|
|
||||||
'isActive': False,
|
|
||||||
'cancelExist': False,
|
|
||||||
'createdAt': 1674493798550,
|
|
||||||
'tradeType': 'TRADE'
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
'gate': {
|
|
||||||
'pair': 'BTC/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'futures': True,
|
|
||||||
'futures_pair': 'BTC/USDT:USDT',
|
|
||||||
'hasQuoteVolumeFutures': True,
|
|
||||||
'leverage_tiers_public': True,
|
|
||||||
'leverage_in_spot_market': True,
|
|
||||||
'sample_order': [
|
|
||||||
{
|
|
||||||
"id": "276266139423",
|
|
||||||
"text": "apiv4",
|
|
||||||
"create_time": "1674493798",
|
|
||||||
"update_time": "1674493798",
|
|
||||||
"create_time_ms": "1674493798550",
|
|
||||||
"update_time_ms": "1674493798550",
|
|
||||||
"status": "closed",
|
|
||||||
"currency_pair": "SOL_USDT",
|
|
||||||
"type": "limit",
|
|
||||||
"account": "spot",
|
|
||||||
"side": "buy",
|
|
||||||
"amount": "1.1",
|
|
||||||
"price": "15.5",
|
|
||||||
"time_in_force": "gtc",
|
|
||||||
"iceberg": "0",
|
|
||||||
"left": "0",
|
|
||||||
"fill_price": "17.05",
|
|
||||||
"filled_total": "17.05",
|
|
||||||
"avg_deal_price": "15.5",
|
|
||||||
"fee": "0.0000018",
|
|
||||||
"fee_currency": "SOL",
|
|
||||||
"point_fee": "0",
|
|
||||||
"gt_fee": "0",
|
|
||||||
"gt_maker_fee": "0",
|
|
||||||
"gt_taker_fee": "0.0015",
|
|
||||||
"gt_discount": True,
|
|
||||||
"rebated_fee": "0",
|
|
||||||
"rebated_fee_currency": "USDT"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# market order
|
|
||||||
'id': '276401180529',
|
|
||||||
'text': 'apiv4',
|
|
||||||
'create_time': '1674493798',
|
|
||||||
'update_time': '1674493798',
|
|
||||||
'create_time_ms': '1674493798550',
|
|
||||||
'update_time_ms': '1674493798550',
|
|
||||||
'status': 'cancelled',
|
|
||||||
'currency_pair': 'SOL_USDT',
|
|
||||||
'type': 'market',
|
|
||||||
'account': 'spot',
|
|
||||||
'side': 'buy',
|
|
||||||
'amount': '17.05',
|
|
||||||
'price': '0',
|
|
||||||
'time_in_force': 'ioc',
|
|
||||||
'iceberg': '0',
|
|
||||||
'left': '0.0000000016228',
|
|
||||||
'fill_price': '17.05',
|
|
||||||
'filled_total': '17.05',
|
|
||||||
'avg_deal_price': '15.5',
|
|
||||||
'fee': '0',
|
|
||||||
'fee_currency': 'SOL',
|
|
||||||
'point_fee': '0.0199999999967544',
|
|
||||||
'gt_fee': '0',
|
|
||||||
'gt_maker_fee': '0',
|
|
||||||
'gt_taker_fee': '0',
|
|
||||||
'gt_discount': False,
|
|
||||||
'rebated_fee': '0',
|
|
||||||
'rebated_fee_currency': 'USDT'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'okx': {
|
|
||||||
'pair': 'BTC/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'futures': True,
|
|
||||||
'futures_pair': 'BTC/USDT:USDT',
|
|
||||||
'hasQuoteVolumeFutures': False,
|
|
||||||
'leverage_tiers_public': True,
|
|
||||||
'leverage_in_spot_market': True,
|
|
||||||
'private_methods': ['fetch_accounts'],
|
|
||||||
},
|
|
||||||
'bybit': {
|
|
||||||
'pair': 'BTC/USDT',
|
|
||||||
'stake_currency': 'USDT',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'use_ci_proxy': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'futures_pair': 'BTC/USDT:USDT',
|
|
||||||
'futures': True,
|
|
||||||
'leverage_tiers_public': True,
|
|
||||||
'leverage_in_spot_market': True,
|
|
||||||
'sample_order': [
|
|
||||||
{
|
|
||||||
"orderId": "1274754916287346280",
|
|
||||||
"orderLinkId": "1666798627015730",
|
|
||||||
"symbol": "SOLUSDT",
|
|
||||||
"createTime": "1674493798550",
|
|
||||||
"orderPrice": "15.5",
|
|
||||||
"orderQty": "1.1",
|
|
||||||
"orderType": "LIMIT",
|
|
||||||
"side": "BUY",
|
|
||||||
"status": "NEW",
|
|
||||||
"timeInForce": "GTC",
|
|
||||||
"accountId": "5555555",
|
|
||||||
"execQty": "0",
|
|
||||||
"orderCategory": "0"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'huobi': {
|
|
||||||
'pair': 'ETH/BTC',
|
|
||||||
'stake_currency': 'BTC',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'futures': False,
|
|
||||||
},
|
|
||||||
'bitvavo': {
|
|
||||||
'pair': 'BTC/EUR',
|
|
||||||
'stake_currency': 'EUR',
|
|
||||||
'hasQuoteVolume': True,
|
|
||||||
'timeframe': '1h',
|
|
||||||
'leverage_tiers_public': False,
|
|
||||||
'leverage_in_spot_market': False,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
|
||||||
def exchange_conf():
|
|
||||||
config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve())
|
|
||||||
config['exchange']['pair_whitelist'] = []
|
|
||||||
config['exchange']['key'] = ''
|
|
||||||
config['exchange']['secret'] = ''
|
|
||||||
config['dry_run'] = False
|
|
||||||
config['entry_pricing']['use_order_book'] = True
|
|
||||||
config['exit_pricing']['use_order_book'] = True
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def set_test_proxy(config: Config, use_proxy: bool) -> Config:
|
|
||||||
# Set proxy to test in CI.
|
|
||||||
import os
|
|
||||||
if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')):
|
|
||||||
config1 = deepcopy(config)
|
|
||||||
config1['exchange']['ccxt_config'] = {
|
|
||||||
"httpsProxy": proxy,
|
|
||||||
}
|
|
||||||
return config1
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
|
||||||
def exchange(request, exchange_conf):
|
|
||||||
exchange_conf = set_test_proxy(
|
|
||||||
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
|
||||||
exchange_conf['exchange']['name'] = request.param
|
|
||||||
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
|
||||||
exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True)
|
|
||||||
|
|
||||||
yield exchange, request.param
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=EXCHANGES, scope="class")
|
|
||||||
def exchange_futures(request, exchange_conf, class_mocker):
|
|
||||||
if EXCHANGES[request.param].get('futures') is not True:
|
|
||||||
yield None, request.param
|
|
||||||
else:
|
|
||||||
exchange_conf = set_test_proxy(
|
|
||||||
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
|
|
||||||
exchange_conf = deepcopy(exchange_conf)
|
|
||||||
exchange_conf['exchange']['name'] = request.param
|
|
||||||
exchange_conf['trading_mode'] = 'futures'
|
|
||||||
exchange_conf['margin_mode'] = 'isolated'
|
|
||||||
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
|
|
||||||
|
|
||||||
class_mocker.patch(
|
|
||||||
'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
|
|
||||||
class_mocker.patch(f'{EXMS}.fetch_trading_fees')
|
|
||||||
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
|
|
||||||
class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init')
|
|
||||||
class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init')
|
|
||||||
class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None)
|
|
||||||
class_mocker.patch(f'{EXMS}.cache_leverage_tiers')
|
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange(
|
|
||||||
exchange_conf, validate=True, load_leverage_tiers=True)
|
|
||||||
|
|
||||||
yield exchange, request.param
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.longrun
|
@pytest.mark.longrun
|
||||||
@@ -371,9 +47,6 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
|
||||||
# exchange_futures only returns values for supported exchanges
|
|
||||||
return
|
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', pair)
|
pair = EXCHANGES[exchangename].get('futures_pair', pair)
|
||||||
markets = exchange.markets
|
markets = exchange.markets
|
||||||
@@ -561,9 +234,6 @@ class TestCCXTExchange:
|
|||||||
def test_ccxt__async_get_candle_history_futures(
|
def test_ccxt__async_get_candle_history_futures(
|
||||||
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type):
|
self, exchange_futures: EXCHANGE_FIXTURE_TYPE, candle_type):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
|
||||||
# exchange_futures only returns values for supported exchanges
|
|
||||||
return
|
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||||
if candle_type == CandleType.FUNDING_RATE:
|
if candle_type == CandleType.FUNDING_RATE:
|
||||||
@@ -579,9 +249,6 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
|
||||||
# exchange_futures only returns values for supported exchanges
|
|
||||||
return
|
|
||||||
|
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||||
@@ -617,9 +284,6 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
|
||||||
# exchange_futures only returns values for supported exchanges
|
|
||||||
return
|
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||||
pair_tf = (pair, '1h', CandleType.MARK)
|
pair_tf = (pair, '1h', CandleType.MARK)
|
||||||
@@ -641,9 +305,6 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
|
||||||
# exchange_futures only returns values for supported exchanges
|
|
||||||
return
|
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
since = datetime.now(timezone.utc) - timedelta(days=5)
|
since = datetime.now(timezone.utc) - timedelta(days=5)
|
||||||
|
|
||||||
@@ -690,31 +351,29 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures:
|
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
||||||
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
if leverage_tiers_public:
|
||||||
if leverage_tiers_public:
|
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
|
||||||
'futures_pair',
|
|
||||||
EXCHANGES[futures_name]['pair']
|
|
||||||
)
|
|
||||||
futures_leverage = futures.get_max_leverage(futures_pair, 20)
|
|
||||||
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
|
||||||
assert futures_leverage >= 1.0
|
|
||||||
|
|
||||||
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
|
||||||
futures, futures_name = exchange_futures
|
|
||||||
if futures:
|
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
'futures_pair',
|
'futures_pair',
|
||||||
EXCHANGES[futures_name]['pair']
|
EXCHANGES[futures_name]['pair']
|
||||||
)
|
)
|
||||||
contract_size = futures.get_contract_size(futures_pair)
|
futures_leverage = futures.get_max_leverage(futures_pair, 20)
|
||||||
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
|
||||||
assert contract_size >= 0.0
|
assert futures_leverage >= 1.0
|
||||||
|
|
||||||
|
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
|
futures, futures_name = exchange_futures
|
||||||
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
|
'futures_pair',
|
||||||
|
EXCHANGES[futures_name]['pair']
|
||||||
|
)
|
||||||
|
contract_size = futures.get_contract_size(futures_pair)
|
||||||
|
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
|
||||||
|
assert contract_size >= 0.0
|
||||||
|
|
||||||
def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
if EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||||
leverage_tiers = futures.load_leverage_tiers()
|
leverage_tiers = futures.load_leverage_tiers()
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
'futures_pair',
|
'futures_pair',
|
||||||
@@ -747,7 +406,7 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
if EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||||
|
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
'futures_pair',
|
'futures_pair',
|
||||||
@@ -780,14 +439,13 @@ class TestCCXTExchange:
|
|||||||
|
|
||||||
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures:
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
'futures_pair',
|
||||||
'futures_pair',
|
EXCHANGES[futures_name]['pair']
|
||||||
EXCHANGES[futures_name]['pair']
|
)
|
||||||
)
|
max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000)
|
||||||
max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000)
|
assert (isinstance(max_stake_amount, float))
|
||||||
assert (isinstance(max_stake_amount, float))
|
assert max_stake_amount >= 0.0
|
||||||
assert max_stake_amount >= 0.0
|
|
||||||
|
|
||||||
def test_private_method_presence(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
def test_private_method_presence(self, exchange: EXCHANGE_FIXTURE_TYPE):
|
||||||
exch, exchangename = exchange
|
exch, exchangename = exchange
|
||||||
@@ -97,9 +97,9 @@ def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"learning_rate": 3e-4,
|
"learning_rate": 3e-4,
|
||||||
"trainer_kwargs": {
|
"trainer_kwargs": {
|
||||||
"max_iters": 1,
|
"n_steps": None,
|
||||||
"batch_size": 64,
|
"batch_size": 64,
|
||||||
"max_n_eval_batches": 1,
|
"n_epochs": 1,
|
||||||
},
|
},
|
||||||
"model_kwargs": {
|
"model_kwargs": {
|
||||||
"hidden_dim": 32,
|
"hidden_dim": 32,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from freqtrade.data.dataprovider import DataProvider
|
|||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.enums import CandleType, ExitType, RunMode
|
from freqtrade.enums import CandleType, ExitType, RunMode
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange import timeframe_to_next_date, timeframe_to_prev_date
|
||||||
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename, get_strategy_run_id
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence import LocalTrade, Trade
|
from freqtrade.persistence import LocalTrade, Trade
|
||||||
@@ -1122,10 +1122,10 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
|||||||
processed = backtesting.strategy.advise_all_indicators(data)
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
|
|
||||||
global count
|
|
||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
def tmp_confirm_entry(pair, current_time, **kwargs):
|
def tmp_confirm_entry(pair, current_time, **kwargs):
|
||||||
|
nonlocal count
|
||||||
dp = backtesting.strategy.dp
|
dp = backtesting.strategy.dp
|
||||||
df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe)
|
df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe)
|
||||||
current_candle = df.iloc[-1].squeeze()
|
current_candle = df.iloc[-1].squeeze()
|
||||||
@@ -1135,8 +1135,13 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
|||||||
assert candle_date == current_time
|
assert candle_date == current_time
|
||||||
# These asserts don't properly raise as they are nested,
|
# These asserts don't properly raise as they are nested,
|
||||||
# therefore we increment count and assert for that.
|
# therefore we increment count and assert for that.
|
||||||
global count
|
df = dp.get_pair_dataframe(pair, backtesting.strategy.timeframe)
|
||||||
count = count + 1
|
prior_time = timeframe_to_prev_date(backtesting.strategy.timeframe,
|
||||||
|
candle_date - timedelta(seconds=1))
|
||||||
|
assert prior_time == df.iloc[-1].squeeze()['date']
|
||||||
|
assert df.iloc[-1].squeeze()['date'] < current_time
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
|
backtesting.strategy.confirm_trade_entry = tmp_confirm_entry
|
||||||
backtesting.backtest(
|
backtesting.backtest(
|
||||||
@@ -1354,11 +1359,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
|
|
||||||
# Cached data correctly removed amounts
|
# Cached data correctly removed amounts
|
||||||
offset = 1 if tres == 0 else 0
|
offset = 1 if tres == 0 else 0
|
||||||
removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count
|
removed_candles = len(data[pair]) - offset
|
||||||
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
|
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
|
||||||
assert len(
|
assert len(
|
||||||
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
|
||||||
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
|
) == len(data['NXT/BTC']) - 1
|
||||||
|
|
||||||
backtesting.strategy.max_open_trades = 1
|
backtesting.strategy.max_open_trades = 1
|
||||||
backtesting.config.update({'max_open_trades': 1})
|
backtesting.config.update({'max_open_trades': 1})
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from tests.conftest import EXMS, get_args, log_has_re, patch_exchange
|
|||||||
def lookahead_conf(default_conf_usdt):
|
def lookahead_conf(default_conf_usdt):
|
||||||
default_conf_usdt['minimum_trade_amount'] = 10
|
default_conf_usdt['minimum_trade_amount'] = 10
|
||||||
default_conf_usdt['targeted_trade_amount'] = 20
|
default_conf_usdt['targeted_trade_amount'] = 20
|
||||||
|
default_conf_usdt['timerange'] = '20220101-20220501'
|
||||||
|
|
||||||
default_conf_usdt['strategy_path'] = str(
|
default_conf_usdt['strategy_path'] = str(
|
||||||
Path(__file__).parent.parent / "strategy/strats/lookahead_bias")
|
Path(__file__).parent.parent / "strategy/strats/lookahead_bias")
|
||||||
default_conf_usdt['strategy'] = 'strategy_test_v3_with_lookahead_bias'
|
default_conf_usdt['strategy'] = 'strategy_test_v3_with_lookahead_bias'
|
||||||
@@ -43,7 +45,9 @@ def test_start_lookahead_analysis(mocker):
|
|||||||
"--pairs",
|
"--pairs",
|
||||||
"UNITTEST/BTC",
|
"UNITTEST/BTC",
|
||||||
"--max-open-trades",
|
"--max-open-trades",
|
||||||
"1"
|
"1",
|
||||||
|
"--timerange",
|
||||||
|
"20220101-20220201"
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@@ -72,6 +76,24 @@ def test_start_lookahead_analysis(mocker):
|
|||||||
match=r"Targeted trade amount can't be smaller than minimum trade amount.*"):
|
match=r"Targeted trade amount can't be smaller than minimum trade amount.*"):
|
||||||
start_lookahead_analysis(pargs)
|
start_lookahead_analysis(pargs)
|
||||||
|
|
||||||
|
# Missing timerange
|
||||||
|
args = [
|
||||||
|
"lookahead-analysis",
|
||||||
|
"--strategy",
|
||||||
|
"strategy_test_v3_with_lookahead_bias",
|
||||||
|
"--strategy-path",
|
||||||
|
str(Path(__file__).parent.parent / "strategy/strats/lookahead_bias"),
|
||||||
|
"--pairs",
|
||||||
|
"UNITTEST/BTC",
|
||||||
|
"--max-open-trades",
|
||||||
|
"1",
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"Please set a timerange\..*"):
|
||||||
|
start_lookahead_analysis(pargs)
|
||||||
|
|
||||||
|
|
||||||
def test_lookahead_helper_invalid_config(lookahead_conf) -> None:
|
def test_lookahead_helper_invalid_config(lookahead_conf) -> None:
|
||||||
conf = deepcopy(lookahead_conf)
|
conf = deepcopy(lookahead_conf)
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
|||||||
assert isinstance(whitelist, list)
|
assert isinstance(whitelist, list)
|
||||||
|
|
||||||
# Verify length of pairlist matches (used for ShuffleFilter without seed)
|
# Verify length of pairlist matches (used for ShuffleFilter without seed)
|
||||||
if type(whitelist_result) is list:
|
if isinstance(whitelist_result, list):
|
||||||
assert whitelist == whitelist_result
|
assert whitelist == whitelist_result
|
||||||
else:
|
else:
|
||||||
len(whitelist) == whitelist_result
|
len(whitelist) == whitelist_result
|
||||||
|
|||||||
@@ -1424,12 +1424,12 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
|||||||
assert len(rc.json()['data']) == amount
|
assert len(rc.json()['data']) == amount
|
||||||
|
|
||||||
assert (rc.json()['data'] ==
|
assert (rc.json()['data'] ==
|
||||||
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
|
[['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
|
||||||
None, 0, 0, 0, 0, 1511686200000, None, None, None, None],
|
None, 0, 0, 0, 0, 1511686200000, None, None, None, None],
|
||||||
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
|
['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05,
|
||||||
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000, 8.893e-05,
|
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000, 8.893e-05,
|
||||||
None, None, None],
|
None, None, None],
|
||||||
['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
|
['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
|
||||||
0.7039405, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None]
|
0.7039405, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None]
|
||||||
|
|
||||||
])
|
])
|
||||||
@@ -1443,13 +1443,13 @@ def test_api_pair_candles(botclient, ohlcv_history):
|
|||||||
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
|
f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert (rc.json()['data'] ==
|
assert (rc.json()['data'] ==
|
||||||
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
|
[['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
|
||||||
None, 0, None, 0, 0, None, 1511686200000, None, None, None, None],
|
None, 0, None, 0, 0, None, 1511686200000, None, None, None, None],
|
||||||
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
|
['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05,
|
||||||
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, 0, 0, '2017-11-26 08:55:00',
|
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, 0, 0, '2017-11-26T08:55:00Z',
|
||||||
1511686500000, 8.893e-05, None, None, None],
|
1511686500000, 8.893e-05, None, None, None],
|
||||||
['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
|
['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
|
||||||
0.7039405, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26 09:00:00', 1511686800000,
|
0.7039405, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26T09:00:00Z', 1511686800000,
|
||||||
None, None, None, None]
|
None, None, None, None]
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -1506,7 +1506,7 @@ def test_api_pair_history(botclient, mocker):
|
|||||||
date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0]
|
date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0]
|
||||||
rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0]
|
rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0]
|
||||||
|
|
||||||
assert data[0][date_col_idx] == '2018-01-11 00:00:00'
|
assert data[0][date_col_idx] == '2018-01-11T00:00:00Z'
|
||||||
assert data[0][rsi_col_idx] is not None
|
assert data[0][rsi_col_idx] is not None
|
||||||
assert data[0][rsi_col_idx] > 0
|
assert data[0][rsi_col_idx] > 0
|
||||||
assert lfm.call_count == 1
|
assert lfm.call_count == 1
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user