Merge branch 'develop' into pr/nicolaspapp/6715
This commit is contained in:
commit
4262f84744
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -24,4 +24,3 @@ Have you search for this feature before requesting it? It's highly likely that a
|
|||||||
## Describe the enhancement
|
## Describe the enhancement
|
||||||
|
|
||||||
*Explain the enhancement you would like*
|
*Explain the enhancement you would like*
|
||||||
|
|
||||||
|
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
@ -100,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts tests
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@v1
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
@ -157,6 +157,12 @@ jobs:
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
|
if: (runner.os != 'Linux' || matrix.python-version != '3.8')
|
||||||
|
run: |
|
||||||
|
pytest --random-order
|
||||||
|
|
||||||
|
- name: Tests (with cov)
|
||||||
|
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||||
|
|
||||||
@ -229,7 +235,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order
|
||||||
|
|
||||||
- name: Backtesting
|
- name: Backtesting
|
||||||
run: |
|
run: |
|
||||||
@ -249,7 +255,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Mypy
|
- name: Mypy
|
||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts
|
mypy freqtrade scripts tests
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@v1
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
@ -259,6 +265,21 @@ jobs:
|
|||||||
details: Test Failed
|
details: Test Failed
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
|
mypy_version_check:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
|
||||||
|
- name: pre-commit dependencies
|
||||||
|
run: |
|
||||||
|
pip install pyaml
|
||||||
|
python build_helpers/pre_commit_update.py
|
||||||
|
|
||||||
docs_check:
|
docs_check:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
@ -271,7 +292,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
|
|
||||||
- name: Documentation build
|
- name: Documentation build
|
||||||
run: |
|
run: |
|
||||||
@ -288,6 +309,9 @@ jobs:
|
|||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
cleanup-prior-runs:
|
cleanup-prior-runs:
|
||||||
|
permissions:
|
||||||
|
actions: write # for rokroskar/workflow-run-cleanup-action to obtain workflow name & cancel it
|
||||||
|
contents: read # for rokroskar/workflow-run-cleanup-action to obtain branch
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Cleanup previous runs on this branch
|
- name: Cleanup previous runs on this branch
|
||||||
@ -298,8 +322,12 @@ jobs:
|
|||||||
|
|
||||||
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
# Notify only once - when CI completes (and after deploy) in case it's successfull
|
||||||
notify-complete:
|
notify-complete:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
# Discord notification can't handle schedule events
|
||||||
|
if: (github.event_name != 'schedule')
|
||||||
|
permissions:
|
||||||
|
repository-projects: read
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check user permission
|
- name: Check user permission
|
||||||
@ -319,7 +347,7 @@ jobs:
|
|||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: [ build_linux, build_macos, build_windows, docs_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade'
|
||||||
|
1
.github/workflows/docker_update_readme.yml
vendored
1
.github/workflows/docker_update_readme.yml
vendored
@ -15,4 +15,3 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade
|
DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade
|
||||||
|
|
||||||
|
@ -1,21 +1,46 @@
|
|||||||
# See https://pre-commit.com for more information
|
# See https://pre-commit.com for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: '4.0.1'
|
rev: "4.0.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: 'v0.942'
|
rev: "v0.942"
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
|
exclude: build_helpers
|
||||||
|
additional_dependencies:
|
||||||
|
- types-cachetools==5.0.1
|
||||||
|
- types-filelock==3.2.5
|
||||||
|
- types-requests==2.27.20
|
||||||
|
- types-tabulate==0.8.7
|
||||||
|
- types-python-dateutil==2.8.12
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: '5.10.1'
|
rev: "5.10.1"
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.4.0
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
tests/.*|
|
||||||
|
.*\.svg
|
||||||
|
)$
|
||||||
|
- id: mixed-line-ending
|
||||||
|
- id: debug-statements
|
||||||
|
- id: check-ast
|
||||||
|
- id: trailing-whitespace
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
.*\.md
|
||||||
|
)$
|
||||||
|
@ -7,4 +7,3 @@ ignore=vendor
|
|||||||
|
|
||||||
[TYPECHECK]
|
[TYPECHECK]
|
||||||
ignored-modules=numpy,talib,talib.abstract
|
ignored-modules=numpy,talib,talib.abstract
|
||||||
|
|
||||||
|
@ -39,6 +39,14 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
|
|||||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
|
### Experimentally, freqtrade also supports futures on the following exchanges
|
||||||
|
|
||||||
|
- [X] [Binance](https://www.binance.com/)
|
||||||
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [OKX](https://okx.com/).
|
||||||
|
|
||||||
|
Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in.
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
|
||||||
Exchanges confirmed working by the community:
|
Exchanges confirmed working by the community:
|
||||||
|
42
build_helpers/pre_commit_update.py
Normal file
42
build_helpers/pre_commit_update.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# File used in CI to ensure pre-commit dependencies are kept uptodate.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
pre_commit_file = Path('.pre-commit-config.yaml')
|
||||||
|
require_dev = Path('requirements-dev.txt')
|
||||||
|
|
||||||
|
with require_dev.open('r') as rfile:
|
||||||
|
requirements = rfile.readlines()
|
||||||
|
|
||||||
|
# Extract types only
|
||||||
|
type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')]
|
||||||
|
|
||||||
|
with pre_commit_file.open('r') as file:
|
||||||
|
f = yaml.load(file, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
|
|
||||||
|
mypy_repo = [repo for repo in f['repos'] if repo['repo']
|
||||||
|
== 'https://github.com/pre-commit/mirrors-mypy']
|
||||||
|
|
||||||
|
hooks = mypy_repo[0]['hooks'][0]['additional_dependencies']
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
for hook in hooks:
|
||||||
|
if hook not in type_reqs:
|
||||||
|
errors.append(f"{hook} is missing in requirements-dev.txt.")
|
||||||
|
|
||||||
|
for req in type_reqs:
|
||||||
|
if req not in hooks:
|
||||||
|
errors.append(f"{req} is missing in pre-config file.")
|
||||||
|
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for e in errors:
|
||||||
|
print(e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
@ -90,7 +90,7 @@
|
|||||||
},
|
},
|
||||||
"bot_name": "freqtrade",
|
"bot_name": "freqtrade",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"force_enter_enable": false,
|
"force_entry_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
}
|
}
|
||||||
|
@ -182,6 +182,7 @@
|
|||||||
"disable_dataframe_checks": false,
|
"disable_dataframe_checks": false,
|
||||||
"strategy": "SampleStrategy",
|
"strategy": "SampleStrategy",
|
||||||
"strategy_path": "user_data/strategies/",
|
"strategy_path": "user_data/strategies/",
|
||||||
|
"recursive_strategy_search": false,
|
||||||
"add_config_files": [],
|
"add_config_files": [],
|
||||||
"dataformat_ohlcv": "json",
|
"dataformat_ohlcv": "json",
|
||||||
"dataformat_trades": "jsongz"
|
"dataformat_trades": "jsongz"
|
||||||
|
73
docs/advanced-backtesting.md
Normal file
73
docs/advanced-backtesting.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Advanced Backtesting Analysis
|
||||||
|
|
||||||
|
## Analyze the buy/entry and sell/exit tags
|
||||||
|
|
||||||
|
It can be helpful to understand how a strategy behaves according to the buy/entry tags used to
|
||||||
|
mark up different buy conditions. You might want to see more complex statistics about each buy and
|
||||||
|
sell condition above those provided by the default backtesting output. You may also want to
|
||||||
|
determine indicator values on the signal candle that resulted in a trade opening.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
The following buy reason analysis is only available for backtesting, *not hyperopt*.
|
||||||
|
|
||||||
|
We need to run backtesting with the `--export` option set to `signals` to enable the exporting of
|
||||||
|
signals **and** trades:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals
|
||||||
|
```
|
||||||
|
|
||||||
|
This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding
|
||||||
|
DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy
|
||||||
|
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
|
||||||
|
folder to delete old exports.
|
||||||
|
|
||||||
|
To analyze the buy tags, we need to use the `buy_reasons.py` script from
|
||||||
|
[froggleston's repo](https://github.com/froggleston/freqtrade-buyreasons). Follow the instructions
|
||||||
|
in their README to copy the script into your `freqtrade/scripts/` folder.
|
||||||
|
|
||||||
|
Before running your next backtest, make sure you either delete your old backtest results or run
|
||||||
|
backtesting with the `--cache none` option to make sure no cached results are used.
|
||||||
|
|
||||||
|
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
|
||||||
|
`user_data/backtest_results` folder.
|
||||||
|
|
||||||
|
Now run the `buy_reasons.py` script, supplying a few options:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0)
|
||||||
|
to the most detailed per pair, per buy and per sell tag (4). More options are available by
|
||||||
|
running with the `-h` option.
|
||||||
|
|
||||||
|
### Tuning the buy tags and sell tags to display
|
||||||
|
|
||||||
|
To show only certain buy and sell tags in the displayed output, use the following two options:
|
||||||
|
|
||||||
|
```
|
||||||
|
--enter_reason_list : Comma separated list of enter signals to analyse. Default: "all"
|
||||||
|
--exit_reason_list : Comma separated list of exit signals to analyse. Default: "stop_loss,trailing_stop_loss"
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Outputting signal candle indicators
|
||||||
|
|
||||||
|
The real power of the buy_reasons.py script comes from the ability to print out the indicator
|
||||||
|
values present on signal candles to allow fine-grained investigation and tuning of buy signal
|
||||||
|
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
|
||||||
|
option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4 --enter_reason_list "enter_tag_a,enter_tag_b" --exit_reason_list "roi,custom_exit_tag_a,stop_loss" --indicator_list "rsi,rsi_1h,bb_lowerband,ema_9,macd,macdsignal"
|
||||||
|
```
|
||||||
|
|
||||||
|
The indicators have to be present in your strategy's main DataFrame (either for your main
|
||||||
|
timeframe or for informative timeframes) otherwise they will simply be ignored in the script
|
||||||
|
output.
|
@ -20,7 +20,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--dry-run-wallet DRY_RUN_WALLET]
|
[--dry-run-wallet DRY_RUN_WALLET]
|
||||||
[--timeframe-detail TIMEFRAME_DETAIL]
|
[--timeframe-detail TIMEFRAME_DETAIL]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export {none,trades}] [--export-filename PATH]
|
[--export {none,trades,signals}]
|
||||||
|
[--export-filename PATH]
|
||||||
[--breakdown {day,week,month} [{day,week,month} ...]]
|
[--breakdown {day,week,month} [{day,week,month} ...]]
|
||||||
[--cache {none,day,week,month}]
|
[--cache {none,day,week,month}]
|
||||||
|
|
||||||
@ -63,18 +64,17 @@ optional arguments:
|
|||||||
`30m`, `1h`, `1d`).
|
`30m`, `1h`, `1d`).
|
||||||
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
Provide a space-separated list of strategies to
|
Provide a space-separated list of strategies to
|
||||||
backtest. Please note that timeframe needs to be
|
backtest. Please note that timeframe needs to be set
|
||||||
set either in config or via command line. When using
|
either in config or via command line. When using this
|
||||||
this together with `--export trades`, the strategy-
|
together with `--export trades`, the strategy-name is
|
||||||
name is injected into the filename (so `backtest-
|
injected into the filename (so `backtest-data.json`
|
||||||
data.json` becomes `backtest-data-SampleStrategy.json`
|
becomes `backtest-data-SampleStrategy.json`
|
||||||
--export {none,trades}
|
--export {none,trades,signals}
|
||||||
Export backtest results (default: trades).
|
Export backtest results (default: trades).
|
||||||
--export-filename PATH
|
--export-filename PATH, --backtest-filename PATH
|
||||||
Save backtest results to the file with this filename.
|
Use this filename for backtest results.Requires
|
||||||
Requires `--export` to be set as well. Example:
|
`--export` to be set as well. Example: `--export-filen
|
||||||
`--export-filename=user_data/backtest_results/backtest
|
ame=user_data/backtest_results/backtest_today.json`
|
||||||
_today.json`
|
|
||||||
--breakdown {day,week,month} [{day,week,month} ...]
|
--breakdown {day,week,month} [{day,week,month} ...]
|
||||||
Show backtesting breakdown per [day, week, month].
|
Show backtesting breakdown per [day, week, month].
|
||||||
--cache {none,day,week,month}
|
--cache {none,day,week,month}
|
||||||
@ -299,6 +299,7 @@ A backtesting result will look like that:
|
|||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
|
| CAGR % | 460.87% |
|
||||||
| Trades per day | 3.575 |
|
| Trades per day | 3.575 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
@ -388,6 +389,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
|
| CAGR % | 460.87% |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
|
@ -11,7 +11,7 @@ Per default, the bot loads the configuration from the `config.json` file, locate
|
|||||||
|
|
||||||
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
You can specify a different configuration file used by the bot with the `-c/--config` command-line option.
|
||||||
|
|
||||||
If you used the [Quick start](installation.md/#quick-start) method for installing
|
If you used the [Quick start](installation.md/#quick-start) method for installing
|
||||||
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
|
||||||
|
|
||||||
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file.
|
||||||
@ -64,7 +64,7 @@ This is similar to using multiple `--config` parameters, but simpler in usage as
|
|||||||
"config-private.json"
|
"config-private.json"
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade trade --config user_data/config.json <...>
|
freqtrade trade --config user_data/config.json <...>
|
||||||
```
|
```
|
||||||
@ -100,7 +100,7 @@ This is similar to using multiple `--config` parameters, but simpler in usage as
|
|||||||
"stake_amount": "unlimited",
|
"stake_amount": "unlimited",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Resulting combined configuration:
|
Resulting combined configuration:
|
||||||
|
|
||||||
``` json title="Result"
|
``` json title="Result"
|
||||||
@ -173,6 +173,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `order_types` | Configure order-types depending on the action (`"entry"`, `"exit"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
| `order_types` | Configure order-types depending on the action (`"entry"`, `"exit"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
||||||
| `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
| `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
|
||||||
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
|
||||||
|
| `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy. <br> **Datatype:** Boolean
|
||||||
| `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.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
|
||||||
|
@ -122,5 +122,6 @@ Best avoid relative paths, since this starts at the storage location of the jupy
|
|||||||
|
|
||||||
* [Strategy debugging](strategy_analysis_example.md) - also available as Jupyter notebook (`user_data/notebooks/strategy_analysis_example.ipynb`)
|
* [Strategy debugging](strategy_analysis_example.md) - also available as Jupyter notebook (`user_data/notebooks/strategy_analysis_example.ipynb`)
|
||||||
* [Plotting](plotting.md)
|
* [Plotting](plotting.md)
|
||||||
|
* [Tag Analysis](advanced-backtesting.md)
|
||||||
|
|
||||||
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.
|
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.
|
||||||
|
@ -64,7 +64,10 @@ Binance supports [time_in_force](configuration.md#understand-order_time_in_force
|
|||||||
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
|
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
### Binance Futures' order pricing
|
### Binance Futures
|
||||||
|
|
||||||
|
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
|
||||||
|
Violating these rules will result in a trading restriction.
|
||||||
|
|
||||||
When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures.
|
When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures.
|
||||||
|
|
||||||
|
@ -51,6 +51,14 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
|
|||||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
|
### Experimentally, freqtrade also supports futures on the following exchanges:
|
||||||
|
|
||||||
|
- [X] [Binance](https://www.binance.com/)
|
||||||
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [OKX](https://okx.com/).
|
||||||
|
|
||||||
|
Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in.
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
|
||||||
Exchanges confirmed working by the community:
|
Exchanges confirmed working by the community:
|
||||||
|
@ -9,4 +9,4 @@ window.MathJax = {
|
|||||||
ignoreHtmlClass: ".*|",
|
ignoreHtmlClass: ".*|",
|
||||||
processHtmlClass: "arithmatex"
|
processHtmlClass: "arithmatex"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
mkdocs==1.3.0
|
mkdocs==1.3.0
|
||||||
mkdocs-material==8.2.9
|
mkdocs-material==8.2.10
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.3
|
pymdown-extensions==9.4
|
||||||
jinja2==3.1.1
|
jinja2==3.1.1
|
||||||
|
@ -7,6 +7,7 @@ Depending on the callback used, they may be called when entering / exiting a tra
|
|||||||
|
|
||||||
Currently available callbacks:
|
Currently available callbacks:
|
||||||
|
|
||||||
|
* [`bot_start()`](#bot-start)
|
||||||
* [`bot_loop_start()`](#bot-loop-start)
|
* [`bot_loop_start()`](#bot-loop-start)
|
||||||
* [`custom_stake_amount()`](#stake-size-management)
|
* [`custom_stake_amount()`](#stake-size-management)
|
||||||
* [`custom_exit()`](#custom-exit-signal)
|
* [`custom_exit()`](#custom-exit-signal)
|
||||||
@ -21,6 +22,29 @@ Currently available callbacks:
|
|||||||
!!! Tip "Callback calling sequence"
|
!!! Tip "Callback calling sequence"
|
||||||
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||||
|
|
||||||
|
## Bot start
|
||||||
|
|
||||||
|
A simple callback which is called once when the strategy is loaded.
|
||||||
|
This can be used to perform actions that must only be performed once and runs after dataprovider and wallet are set
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# ... populate_* methods
|
||||||
|
|
||||||
|
def bot_start(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Called only once after bot instantiation.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
"""
|
||||||
|
if self.config['runmode'].value in ('live', 'dry_run'):
|
||||||
|
# Assign this to the class by using self.*
|
||||||
|
# can then be used by populate_* methods
|
||||||
|
self.cust_remote_data = requests.get('https://some_remote_source.example.com')
|
||||||
|
|
||||||
|
```
|
||||||
## Bot loop start
|
## Bot loop start
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently).
|
||||||
@ -122,11 +146,11 @@ See [Dataframe access](strategy-advanced.md#dataframe-access) for more informati
|
|||||||
|
|
||||||
## Custom stoploss
|
## Custom stoploss
|
||||||
|
|
||||||
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
|
Called for open trade every iteration (roughly every 5 seconds) until a trade is closed.
|
||||||
|
|
||||||
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
||||||
|
|
||||||
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade).
|
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade), and is still mandatory.
|
||||||
|
|
||||||
The method must return a stoploss value (float / number) as a percentage of the current price.
|
The method must return a stoploss value (float / number) as a percentage of the current price.
|
||||||
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
|
||||||
@ -365,30 +389,30 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
timeframe=self.timeframe)
|
timeframe=self.timeframe)
|
||||||
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1]
|
||||||
|
|
||||||
return new_entryprice
|
return new_entryprice
|
||||||
|
|
||||||
def custom_exit_price(self, pair: str, trade: Trade,
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
current_time: datetime, proposed_rate: float,
|
current_time: datetime, proposed_rate: float,
|
||||||
current_profit: float, **kwargs) -> float:
|
current_profit: float, exit_tag: Optional[str], **kwargs) -> float:
|
||||||
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
||||||
timeframe=self.timeframe)
|
timeframe=self.timeframe)
|
||||||
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
new_exitprice = dataframe['bollinger_10_upperband'].iat[-1]
|
||||||
|
|
||||||
return new_exitprice
|
return new_exitprice
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter.
|
||||||
**Example**:
|
**Example**:
|
||||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||||
|
|
||||||
!!! Warning "Backtesting"
|
!!! Warning "Backtesting"
|
||||||
@ -418,7 +442,7 @@ The function must return either `True` (cancel order) or `False` (keep order ali
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade, Order
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
@ -430,7 +454,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
'exit': 60 * 25
|
'exit': 60 * 25
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict,
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||||
return True
|
return True
|
||||||
@ -441,7 +465,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_exit_timeout(self, pair: str, trade: Trade, order: 'Order',
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
|
||||||
return True
|
return True
|
||||||
@ -459,7 +483,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade, Order
|
||||||
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
@ -471,22 +495,22 @@ class AwesomeStrategy(IStrategy):
|
|||||||
'exit': 60 * 25
|
'exit': 60 * 25
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_entry_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
ob = self.dp.orderbook(pair, 1)
|
ob = self.dp.orderbook(pair, 1)
|
||||||
current_price = ob['bids'][0][0]
|
current_price = ob['bids'][0][0]
|
||||||
# Cancel buy order if price is more than 2% above the order.
|
# Cancel buy order if price is more than 2% above the order.
|
||||||
if current_price > order['price'] * 1.02:
|
if current_price > order.price * 1.02:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
ob = self.dp.orderbook(pair, 1)
|
ob = self.dp.orderbook(pair, 1)
|
||||||
current_price = ob['asks'][0][0]
|
current_price = ob['asks'][0][0]
|
||||||
# Cancel sell order if price is more than 2% below the order.
|
# Cancel sell order if price is more than 2% below the order.
|
||||||
if current_price < order['price'] * 0.98:
|
if current_price < order.price * 0.98:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
@ -508,7 +532,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||||
side: str, **kwargs) -> bool:
|
side: str, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Called right before placing a entry order.
|
Called right before placing a entry order.
|
||||||
@ -616,35 +640,35 @@ from freqtrade.persistence import Trade
|
|||||||
|
|
||||||
|
|
||||||
class DigDeeperStrategy(IStrategy):
|
class DigDeeperStrategy(IStrategy):
|
||||||
|
|
||||||
position_adjustment_enable = True
|
position_adjustment_enable = True
|
||||||
|
|
||||||
# Attempts to handle large drops with DCA. High stoploss is required.
|
# Attempts to handle large drops with DCA. High stoploss is required.
|
||||||
stoploss = -0.30
|
stoploss = -0.30
|
||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
# Example specific variables
|
# Example specific variables
|
||||||
max_entry_position_adjustment = 3
|
max_entry_position_adjustment = 3
|
||||||
# This number is explained a bit further down
|
# This number is explained a bit further down
|
||||||
max_dca_multiplier = 5.5
|
max_dca_multiplier = 5.5
|
||||||
|
|
||||||
# This is called when placing the initial order (opening trade)
|
# This is called when placing the initial order (opening trade)
|
||||||
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_stake: float, min_stake: float, max_stake: float,
|
proposed_stake: float, min_stake: float, max_stake: float,
|
||||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||||
|
|
||||||
# We need to leave most of the funds for possible further DCA orders
|
# We need to leave most of the funds for possible further DCA orders
|
||||||
# This also applies to fixed stakes
|
# This also applies to fixed stakes
|
||||||
return proposed_stake / self.max_dca_multiplier
|
return proposed_stake / self.max_dca_multiplier
|
||||||
|
|
||||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||||
current_rate: float, current_profit: float, min_stake: float,
|
current_rate: float, current_profit: float, min_stake: float,
|
||||||
max_stake: float, **kwargs):
|
max_stake: float, **kwargs):
|
||||||
"""
|
"""
|
||||||
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
|
||||||
This means extra buy orders with additional fees.
|
This means extra buy orders with additional fees.
|
||||||
|
|
||||||
:param trade: trade object.
|
:param trade: trade object.
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param current_rate: Current buy rate.
|
:param current_rate: Current buy rate.
|
||||||
@ -654,7 +678,7 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return float: Stake amount to adjust your trade
|
:return float: Stake amount to adjust your trade
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if current_profit > -0.05:
|
if current_profit > -0.05:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
|
|||||||
|
|
||||||
# if backtest_dir points to a directory, it'll automatically load the last backtest file.
|
# if backtest_dir points to a directory, it'll automatically load the last backtest file.
|
||||||
backtest_dir = config["user_data_dir"] / "backtest_results"
|
backtest_dir = config["user_data_dir"] / "backtest_results"
|
||||||
# backtest_dir can also point to a specific file
|
# backtest_dir can also point to a specific file
|
||||||
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
|
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -183,11 +183,11 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
``` python hl_lines="2 6"
|
``` python hl_lines="2 6"
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict,
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict,
|
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
@ -32,6 +32,7 @@ dependencies:
|
|||||||
- prompt-toolkit
|
- prompt-toolkit
|
||||||
- schedule
|
- schedule
|
||||||
- python-dateutil
|
- python-dateutil
|
||||||
|
- joblib
|
||||||
|
|
||||||
|
|
||||||
# ============================
|
# ============================
|
||||||
@ -54,7 +55,6 @@ dependencies:
|
|||||||
- scikit-learn
|
- scikit-learn
|
||||||
- filelock
|
- filelock
|
||||||
- scikit-optimize
|
- scikit-optimize
|
||||||
- joblib
|
|
||||||
- progressbar2
|
- progressbar2
|
||||||
# ============================
|
# ============================
|
||||||
# 4/4 req plot
|
# 4/4 req plot
|
||||||
|
@ -11,4 +11,3 @@ Restart=on-failure
|
|||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|
||||||
|
@ -27,4 +27,3 @@ WatchdogSec=20
|
|||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from freqtrade.constants import DEFAULT_CONFIG
|
|||||||
|
|
||||||
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
|
||||||
|
|
||||||
ARGS_STRATEGY = ["strategy", "strategy_path"]
|
ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search"]
|
||||||
|
|
||||||
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
|
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
|
||||||
|
|
||||||
@ -37,7 +37,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
|||||||
|
|
||||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||||
|
|
||||||
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"]
|
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized",
|
||||||
|
"recursive_strategy_search"]
|
||||||
|
|
||||||
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"]
|
||||||
|
|
||||||
|
@ -83,6 +83,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Reset sample files to their original state.',
|
help='Reset sample files to their original state.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
|
"recursive_strategy_search": Arg(
|
||||||
|
'--recursive-strategy-search',
|
||||||
|
help='Recursively search for a strategy in the strategies folder.',
|
||||||
|
action='store_true',
|
||||||
|
),
|
||||||
# Main options
|
# Main options
|
||||||
"strategy": Arg(
|
"strategy": Arg(
|
||||||
'-s', '--strategy',
|
'-s', '--strategy',
|
||||||
|
@ -41,7 +41,7 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
|||||||
print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason']))
|
print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason']))
|
||||||
|
|
||||||
|
|
||||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
def _print_objs_tabular(objs: List, print_colorized: bool, base_dir: Path) -> None:
|
||||||
if print_colorized:
|
if print_colorized:
|
||||||
colorama_init(autoreset=True)
|
colorama_init(autoreset=True)
|
||||||
red = Fore.RED
|
red = Fore.RED
|
||||||
@ -55,7 +55,7 @@ def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
|||||||
names = [s['name'] for s in objs]
|
names = [s['name'] for s in objs]
|
||||||
objs_to_print = [{
|
objs_to_print = [{
|
||||||
'name': s['name'] if s['name'] else "--",
|
'name': s['name'] if s['name'] else "--",
|
||||||
'location': s['location'].name,
|
'location': s['location'].relative_to(base_dir),
|
||||||
'status': (red + "LOAD FAILED" + reset if s['class'] is None
|
'status': (red + "LOAD FAILED" + reset if s['class'] is None
|
||||||
else "OK" if names.count(s['name']) == 1
|
else "OK" if names.count(s['name']) == 1
|
||||||
else yellow + "DUPLICATE NAME" + reset)
|
else yellow + "DUPLICATE NAME" + reset)
|
||||||
@ -77,7 +77,8 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
|||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||||
strategy_objs = StrategyResolver.search_all_objects(directory, not args['print_one_column'])
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
|
directory, not args['print_one_column'], config.get('recursive_strategy_search', False))
|
||||||
# Sort alphabetically
|
# Sort alphabetically
|
||||||
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
|
strategy_objs = sorted(strategy_objs, key=lambda x: x['name'])
|
||||||
for obj in strategy_objs:
|
for obj in strategy_objs:
|
||||||
@ -89,7 +90,7 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
|||||||
if args['print_one_column']:
|
if args['print_one_column']:
|
||||||
print('\n'.join([s['name'] for s in strategy_objs]))
|
print('\n'.join([s['name'] for s in strategy_objs]))
|
||||||
else:
|
else:
|
||||||
_print_objs_tabular(strategy_objs, config.get('print_colorized', False))
|
_print_objs_tabular(strategy_objs, config.get('print_colorized', False), directory)
|
||||||
|
|
||||||
|
|
||||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||||
|
@ -248,6 +248,12 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='strategy_list',
|
self._args_to_config(config, argname='strategy_list',
|
||||||
logstring='Using strategy list of {} strategies', logfun=len)
|
logstring='Using strategy list of {} strategies', logfun=len)
|
||||||
|
|
||||||
|
self._args_to_config(
|
||||||
|
config,
|
||||||
|
argname='recursive_strategy_search',
|
||||||
|
logstring='Recursively searching for a strategy in the strategies folder.',
|
||||||
|
)
|
||||||
|
|
||||||
self._args_to_config(config, argname='timeframe',
|
self._args_to_config(config, argname='timeframe',
|
||||||
logstring='Overriding timeframe with Command line argument')
|
logstring='Overriding timeframe with Command line argument')
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ PROCESS_THROTTLE_SECS = 5 # sec
|
|||||||
HYPEROPT_EPOCH = 100 # epochs
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
RETRY_TIMEOUT = 30 # sec
|
RETRY_TIMEOUT = 30 # sec
|
||||||
TIMEOUT_UNITS = ['minutes', 'seconds']
|
TIMEOUT_UNITS = ['minutes', 'seconds']
|
||||||
EXPORT_OPTIONS = ['none', 'trades']
|
EXPORT_OPTIONS = ['none', 'trades', 'signals']
|
||||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
|
||||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
|
@ -12,7 +12,8 @@ import pandas as pd
|
|||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import get_backtest_metadata_filename, json_load
|
from freqtrade.misc import json_load
|
||||||
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
|
|
||||||
|
|
||||||
@ -149,7 +150,14 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def _load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Load one strategy from multi-strategy result
|
||||||
|
and merge it with results
|
||||||
|
:param strategy_name: Name of the strategy contained in the result
|
||||||
|
:param filename: Backtest-result-filename to load
|
||||||
|
:param results: dict to merge the result to.
|
||||||
|
"""
|
||||||
bt_data = load_backtest_stats(filename)
|
bt_data = load_backtest_stats(filename)
|
||||||
for k in ('metadata', 'strategy'):
|
for k in ('metadata', 'strategy'):
|
||||||
results[k][strategy_name] = bt_data[k][strategy_name]
|
results[k][strategy_name] = bt_data[k][strategy_name]
|
||||||
@ -160,6 +168,30 @@ def _load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _get_backtest_files(dirname: Path) -> List[Path]:
|
||||||
|
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_resultlist(dirname: Path):
|
||||||
|
"""
|
||||||
|
Get list of backtest results read from metadata files
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for filename in _get_backtest_files(dirname):
|
||||||
|
metadata = load_backtest_metadata(filename)
|
||||||
|
if not metadata:
|
||||||
|
continue
|
||||||
|
for s, v in metadata.items():
|
||||||
|
results.append({
|
||||||
|
'filename': filename.name,
|
||||||
|
'strategy': s,
|
||||||
|
'run_id': v['run_id'],
|
||||||
|
'backtest_start_time': v['backtest_start_time'],
|
||||||
|
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
|
||||||
min_backtest_date: datetime = None) -> Dict[str, Any]:
|
min_backtest_date: datetime = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -179,7 +211,7 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Weird glob expression here avoids including .meta.json files.
|
# Weird glob expression here avoids including .meta.json files.
|
||||||
for filename in reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))):
|
for filename in _get_backtest_files(dirname):
|
||||||
metadata = load_backtest_metadata(filename)
|
metadata = load_backtest_metadata(filename)
|
||||||
if not metadata:
|
if not metadata:
|
||||||
# Files are sorted from newest to oldest. When file without metadata is encountered it
|
# Files are sorted from newest to oldest. When file without metadata is encountered it
|
||||||
@ -202,7 +234,7 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
|
|||||||
|
|
||||||
if strategy_metadata['run_id'] == run_id:
|
if strategy_metadata['run_id'] == run_id:
|
||||||
del run_ids[strategy_name]
|
del run_ids[strategy_name]
|
||||||
_load_and_merge_backtest_result(strategy_name, filename, results)
|
load_and_merge_backtest_result(strategy_name, filename, results)
|
||||||
|
|
||||||
if len(run_ids) == 0:
|
if len(run_ids) == 0:
|
||||||
break
|
break
|
||||||
@ -541,3 +573,14 @@ def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[f
|
|||||||
csum_max = csum_df['sum'].max() + starting_balance
|
csum_max = csum_df['sum'].max() + starting_balance
|
||||||
|
|
||||||
return csum_min, csum_max
|
return csum_min, csum_max
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_cagr(days_passed: int, starting_balance: float, final_balance: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculate CAGR
|
||||||
|
:param days_passed: Days passed between start and ending balance
|
||||||
|
:param starting_balance: Starting balance
|
||||||
|
:param final_balance: Final balance to calculate CAGR against
|
||||||
|
:return: CAGR
|
||||||
|
"""
|
||||||
|
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ import logging
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
from threading import Lock
|
||||||
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@ -64,6 +65,7 @@ class Exchange:
|
|||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
"ohlcv_partial_candle": True,
|
"ohlcv_partial_candle": True,
|
||||||
|
"ohlcv_require_since": False,
|
||||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||||
"ohlcv_volume_currency": "base", # "base" or "quote"
|
"ohlcv_volume_currency": "base", # "base" or "quote"
|
||||||
"tickers_have_quoteVolume": True,
|
"tickers_have_quoteVolume": True,
|
||||||
@ -95,6 +97,9 @@ class Exchange:
|
|||||||
self._markets: Dict = {}
|
self._markets: Dict = {}
|
||||||
self._trading_fees: Dict[str, Any] = {}
|
self._trading_fees: Dict[str, Any] = {}
|
||||||
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
||||||
|
# Lock event loop. This is necessary to avoid race-conditions when using force* commands
|
||||||
|
# Due to funding fee fetching.
|
||||||
|
self._loop_lock = Lock()
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(self.loop)
|
asyncio.set_event_loop(self.loop)
|
||||||
self._config: Dict = {}
|
self._config: Dict = {}
|
||||||
@ -166,7 +171,7 @@ class Exchange:
|
|||||||
self._api_async = self._init_ccxt(
|
self._api_async = self._init_ccxt(
|
||||||
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
||||||
|
|
||||||
logger.info('Using Exchange "%s"', self.name)
|
logger.info(f'Using Exchange "{self.name}"')
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
@ -368,6 +373,9 @@ class Exchange:
|
|||||||
return (
|
return (
|
||||||
market.get('quote', None) is not None
|
market.get('quote', None) is not None
|
||||||
and market.get('base', None) is not None
|
and market.get('base', None) is not None
|
||||||
|
and (self.precisionMode != TICK_SIZE
|
||||||
|
# Too low precision will falsify calculations
|
||||||
|
or market.get('precision', {}).get('price', None) > 1e-11)
|
||||||
and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
|
and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
|
||||||
or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
|
or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
|
||||||
or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
|
or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
|
||||||
@ -551,7 +559,7 @@ class Exchange:
|
|||||||
# Therefore we also show that.
|
# Therefore we also show that.
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"The ccxt library does not provide the list of timeframes "
|
f"The ccxt library does not provide the list of timeframes "
|
||||||
f"for the exchange \"{self.name}\" and this exchange "
|
f"for the exchange {self.name} and this exchange "
|
||||||
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
|
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
|
||||||
|
|
||||||
if timeframe and (timeframe not in self.timeframes):
|
if timeframe and (timeframe not in self.timeframes):
|
||||||
@ -651,7 +659,7 @@ class Exchange:
|
|||||||
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||||
based on our definitions.
|
based on our definitions.
|
||||||
"""
|
"""
|
||||||
if self.markets[pair]['precision']['amount']:
|
if self.markets[pair]['precision']['amount'] is not None:
|
||||||
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||||
precision=self.markets[pair]['precision']['amount'],
|
precision=self.markets[pair]['precision']['amount'],
|
||||||
counting_mode=self.precisionMode,
|
counting_mode=self.precisionMode,
|
||||||
@ -781,7 +789,9 @@ class Exchange:
|
|||||||
rate: float, leverage: float, params: Dict = {},
|
rate: float, leverage: float, params: Dict = {},
|
||||||
stop_loss: bool = False) -> Dict[str, Any]:
|
stop_loss: bool = False) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||||
_amount = self.amount_to_precision(pair, amount)
|
# Rounding here must respect to contract sizes
|
||||||
|
_amount = self._contracts_to_amount(
|
||||||
|
pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
|
||||||
dry_order: Dict[str, Any] = {
|
dry_order: Dict[str, Any] = {
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'symbol': pair,
|
'symbol': pair,
|
||||||
@ -1710,7 +1720,8 @@ class Exchange:
|
|||||||
def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType,
|
def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType,
|
||||||
since_ms: Optional[int]) -> Coroutine:
|
since_ms: Optional[int]) -> Coroutine:
|
||||||
|
|
||||||
if not since_ms and self.required_candle_call_count > 1:
|
if (not since_ms
|
||||||
|
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
|
||||||
# Multiple calls for one pair - to get more history
|
# Multiple calls for one pair - to get more history
|
||||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||||
move_to = one_call * self.required_candle_call_count
|
move_to = one_call * self.required_candle_call_count
|
||||||
@ -1770,7 +1781,8 @@ class Exchange:
|
|||||||
async def gather_stuff():
|
async def gather_stuff():
|
||||||
return await asyncio.gather(*input_coro, return_exceptions=True)
|
return await asyncio.gather(*input_coro, return_exceptions=True)
|
||||||
|
|
||||||
results = self.loop.run_until_complete(gather_stuff())
|
with self._loop_lock:
|
||||||
|
results = self.loop.run_until_complete(gather_stuff())
|
||||||
|
|
||||||
for res in results:
|
for res in results:
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
@ -1829,17 +1841,18 @@ class Exchange:
|
|||||||
pair, timeframe, since_ms, s
|
pair, timeframe, since_ms, s
|
||||||
)
|
)
|
||||||
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
||||||
|
candle_limit = self.ohlcv_candle_limit(timeframe)
|
||||||
if candle_type != CandleType.SPOT:
|
if candle_type != CandleType.SPOT:
|
||||||
params.update({'price': candle_type})
|
params.update({'price': candle_type})
|
||||||
if candle_type != CandleType.FUNDING_RATE:
|
if candle_type != CandleType.FUNDING_RATE:
|
||||||
data = await self._api_async.fetch_ohlcv(
|
data = await self._api_async.fetch_ohlcv(
|
||||||
pair, timeframe=timeframe, since=since_ms,
|
pair, timeframe=timeframe, since=since_ms,
|
||||||
limit=self.ohlcv_candle_limit(timeframe), params=params)
|
limit=candle_limit, params=params)
|
||||||
else:
|
else:
|
||||||
# Funding rate
|
# Funding rate
|
||||||
data = await self._api_async.fetch_funding_rate_history(
|
data = await self._api_async.fetch_funding_rate_history(
|
||||||
pair, since=since_ms,
|
pair, since=since_ms,
|
||||||
limit=self.ohlcv_candle_limit(timeframe))
|
limit=candle_limit)
|
||||||
# Convert funding rate to candle pattern
|
# Convert funding rate to candle pattern
|
||||||
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||||
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
||||||
@ -2026,9 +2039,10 @@ class Exchange:
|
|||||||
if not self.exchange_has("fetchTrades"):
|
if not self.exchange_has("fetchTrades"):
|
||||||
raise OperationalException("This exchange does not support downloading Trades.")
|
raise OperationalException("This exchange does not support downloading Trades.")
|
||||||
|
|
||||||
return self.loop.run_until_complete(
|
with self._loop_lock:
|
||||||
self._async_get_trade_history(pair=pair, since=since,
|
return self.loop.run_until_complete(
|
||||||
until=until, from_id=from_id))
|
self._async_get_trade_history(pair=pair, since=since,
|
||||||
|
until=until, from_id=from_id))
|
||||||
|
|
||||||
@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:
|
||||||
@ -2137,8 +2151,8 @@ class Exchange:
|
|||||||
def parse_leverage_tier(self, tier) -> Dict:
|
def parse_leverage_tier(self, tier) -> Dict:
|
||||||
info = tier.get('info', {})
|
info = tier.get('info', {})
|
||||||
return {
|
return {
|
||||||
'min': tier['notionalFloor'],
|
'min': tier['minNotional'],
|
||||||
'max': tier['notionalCap'],
|
'max': tier['maxNotional'],
|
||||||
'mmr': tier['maintenanceMarginRate'],
|
'mmr': tier['maintenanceMarginRate'],
|
||||||
'lev': tier['maxLeverage'],
|
'lev': tier['maxLeverage'],
|
||||||
'maintAmt': float(info['cum']) if 'cum' in info else None,
|
'maintAmt': float(info['cum']) if 'cum' in info else None,
|
||||||
|
@ -20,6 +20,7 @@ class Ftx(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"ohlcv_candle_limit": 1500,
|
"ohlcv_candle_limit": 1500,
|
||||||
|
"ohlcv_require_since": True,
|
||||||
"ohlcv_volume_currency": "quote",
|
"ohlcv_volume_currency": "quote",
|
||||||
"mark_ohlcv_price": "index",
|
"mark_ohlcv_price": "index",
|
||||||
"mark_ohlcv_timeframe": "1h",
|
"mark_ohlcv_timeframe": "1h",
|
||||||
|
@ -122,6 +122,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self._schedule.every().day.at(t).do(update)
|
self._schedule.every().day.at(t).do(update)
|
||||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
self.strategy.bot_start()
|
||||||
|
|
||||||
def notify_status(self, msg: str) -> None:
|
def notify_status(self, msg: str) -> None:
|
||||||
"""
|
"""
|
||||||
Public method for users of this class (worker, etc.) to send notifications
|
Public method for users of this class (worker, etc.) to send notifications
|
||||||
@ -585,7 +587,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
:param stake_amount: amount of stake-currency for the pair
|
:param stake_amount: amount of stake-currency for the pair
|
||||||
:param leverage: amount of leverage applied to this trade
|
|
||||||
:return: True if a buy order is created, false if it fails.
|
:return: True if a buy order is created, false if it fails.
|
||||||
"""
|
"""
|
||||||
time_in_force = self.strategy.order_time_in_force['entry']
|
time_in_force = self.strategy.order_time_in_force['entry']
|
||||||
@ -664,16 +665,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
|
|
||||||
# TODO: this might be unnecessary, as we're calling it in update_trade_state.
|
|
||||||
isolated_liq = self.exchange.get_liquidation_price(
|
|
||||||
leverage=leverage,
|
|
||||||
pair=pair,
|
|
||||||
amount=amount,
|
|
||||||
open_rate=enter_limit_filled_price,
|
|
||||||
is_short=is_short
|
|
||||||
)
|
|
||||||
interest_rate = self.exchange.get_interest_rate()
|
|
||||||
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
base_currency = self.exchange.get_pair_base_currency(pair)
|
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||||
@ -702,8 +693,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
interest_rate=interest_rate,
|
|
||||||
liquidation_price=isolated_liq,
|
|
||||||
trading_mode=self.trading_mode,
|
trading_mode=self.trading_mode,
|
||||||
funding_fees=funding_fees
|
funding_fees=funding_fees
|
||||||
)
|
)
|
||||||
@ -1373,7 +1362,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
default_retval=proposed_limit_rate)(
|
default_retval=proposed_limit_rate)(
|
||||||
pair=trade.pair, trade=trade,
|
pair=trade.pair, trade=trade,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(timezone.utc),
|
||||||
proposed_rate=proposed_limit_rate, current_profit=current_profit)
|
proposed_rate=proposed_limit_rate, current_profit=current_profit,
|
||||||
|
exit_tag=exit_check.exit_reason)
|
||||||
|
|
||||||
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
|
||||||
|
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
Various tool function for Freqtrade and scripts
|
Various tool function for Freqtrade and scripts
|
||||||
"""
|
"""
|
||||||
import gzip
|
import gzip
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterator, List, Union
|
from typing import Any, Iterator, List
|
||||||
from typing.io import IO
|
from typing.io import IO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -86,6 +84,22 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool =
|
|||||||
logger.debug(f'done json to "{filename}"')
|
logger.debug(f'done json to "{filename}"')
|
||||||
|
|
||||||
|
|
||||||
|
def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None:
|
||||||
|
"""
|
||||||
|
Dump object data into a file
|
||||||
|
:param filename: file to create
|
||||||
|
:param data: Object data to save
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
import joblib
|
||||||
|
|
||||||
|
if log:
|
||||||
|
logger.info(f'dumping joblib to "{filename}"')
|
||||||
|
with open(filename, 'wb') as fp:
|
||||||
|
joblib.dump(data, fp)
|
||||||
|
logger.debug(f'done joblib dump to "{filename}"')
|
||||||
|
|
||||||
|
|
||||||
def json_load(datafile: IO) -> Any:
|
def json_load(datafile: IO) -> Any:
|
||||||
"""
|
"""
|
||||||
load data with rapidjson
|
load data with rapidjson
|
||||||
@ -235,34 +249,3 @@ def parse_db_uri_for_logging(uri: str):
|
|||||||
return uri
|
return uri
|
||||||
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0]
|
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0]
|
||||||
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
||||||
|
|
||||||
|
|
||||||
def get_strategy_run_id(strategy) -> str:
|
|
||||||
"""
|
|
||||||
Generate unique identification hash for a backtest run. Identical config and strategy file will
|
|
||||||
always return an identical hash.
|
|
||||||
:param strategy: strategy object.
|
|
||||||
:return: hex string id.
|
|
||||||
"""
|
|
||||||
digest = hashlib.sha1()
|
|
||||||
config = deepcopy(strategy.config)
|
|
||||||
|
|
||||||
# Options that have no impact on results of individual backtest.
|
|
||||||
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
|
|
||||||
for k in not_important_keys:
|
|
||||||
if k in config:
|
|
||||||
del config[k]
|
|
||||||
|
|
||||||
# Explicitly allow NaN values (e.g. max_open_trades).
|
|
||||||
# as it does not matter for getting the hash.
|
|
||||||
digest.update(rapidjson.dumps(config, default=str,
|
|
||||||
number_mode=rapidjson.NM_NAN).encode('utf-8'))
|
|
||||||
with open(strategy.__file__, 'rb') as fp:
|
|
||||||
digest.update(fp.read())
|
|
||||||
return digest.hexdigest().lower()
|
|
||||||
|
|
||||||
|
|
||||||
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
|
|
||||||
"""Return metadata filename for specified backtest results file."""
|
|
||||||
filename = Path(filename)
|
|
||||||
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')
|
|
||||||
|
40
freqtrade/optimize/backtest_caching.py
Normal file
40
freqtrade/optimize/backtest_caching.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import hashlib
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import rapidjson
|
||||||
|
|
||||||
|
|
||||||
|
def get_strategy_run_id(strategy) -> str:
|
||||||
|
"""
|
||||||
|
Generate unique identification hash for a backtest run. Identical config and strategy file will
|
||||||
|
always return an identical hash.
|
||||||
|
:param strategy: strategy object.
|
||||||
|
:return: hex string id.
|
||||||
|
"""
|
||||||
|
digest = hashlib.sha1()
|
||||||
|
config = deepcopy(strategy.config)
|
||||||
|
|
||||||
|
# Options that have no impact on results of individual backtest.
|
||||||
|
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
|
||||||
|
for k in not_important_keys:
|
||||||
|
if k in config:
|
||||||
|
del config[k]
|
||||||
|
|
||||||
|
# Explicitly allow NaN values (e.g. max_open_trades).
|
||||||
|
# as it does not matter for getting the hash.
|
||||||
|
digest.update(rapidjson.dumps(config, default=str,
|
||||||
|
number_mode=rapidjson.NM_NAN).encode('utf-8'))
|
||||||
|
# Include _ft_params_from_file - so changing parameter files cause cache eviction
|
||||||
|
digest.update(rapidjson.dumps(
|
||||||
|
strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8'))
|
||||||
|
with open(strategy.__file__, 'rb') as fp:
|
||||||
|
digest.update(fp.read())
|
||||||
|
return digest.hexdigest().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
|
||||||
|
"""Return metadata filename for specified backtest results file."""
|
||||||
|
filename = Path(filename)
|
||||||
|
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')
|
189
freqtrade/optimize/backtesting.py
Normal file → Executable file
189
freqtrade/optimize/backtesting.py
Normal file → Executable file
@ -9,6 +9,7 @@ from copy import deepcopy
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from numpy import nan
|
from numpy import nan
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
@ -19,13 +20,15 @@ from freqtrade.data import history
|
|||||||
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
||||||
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode
|
from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, RunMode,
|
||||||
|
TradingMode)
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
from freqtrade.misc import get_strategy_run_id
|
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
|
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||||
from freqtrade.optimize.bt_progress import BTProgress
|
from freqtrade.optimize.bt_progress import BTProgress
|
||||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||||
|
store_backtest_signal_candles,
|
||||||
store_backtest_stats)
|
store_backtest_stats)
|
||||||
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
@ -51,6 +54,11 @@ ESHORT_IDX = 8 # Exit short
|
|||||||
ENTER_TAG_IDX = 9
|
ENTER_TAG_IDX = 9
|
||||||
EXIT_TAG_IDX = 10
|
EXIT_TAG_IDX = 10
|
||||||
|
|
||||||
|
# Every change to this headers list must evaluate further usages of the resulting tuple
|
||||||
|
# and eventually change the constants for indexes at the top
|
||||||
|
HEADERS = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
|
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
||||||
|
|
||||||
|
|
||||||
class Backtesting:
|
class Backtesting:
|
||||||
"""
|
"""
|
||||||
@ -73,6 +81,8 @@ class Backtesting:
|
|||||||
self.run_ids: Dict[str, str] = {}
|
self.run_ids: Dict[str, str] = {}
|
||||||
self.strategylist: List[IStrategy] = []
|
self.strategylist: List[IStrategy] = []
|
||||||
self.all_results: Dict[str, Dict] = {}
|
self.all_results: Dict[str, Dict] = {}
|
||||||
|
self.processed_dfs: Dict[str, Dict] = {}
|
||||||
|
|
||||||
self._exchange_name = self.config['exchange']['name']
|
self._exchange_name = self.config['exchange']['name']
|
||||||
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
|
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
@ -174,9 +184,10 @@ class Backtesting:
|
|||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to Strategy baseclass
|
||||||
strategy.wallets = self.wallets
|
strategy.wallets = self.wallets
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-exit is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
self.strategy.bot_start()
|
||||||
|
|
||||||
def _load_protections(self, strategy: IStrategy):
|
def _load_protections(self, strategy: IStrategy):
|
||||||
if self.config.get('enable_protections', False):
|
if self.config.get('enable_protections', False):
|
||||||
@ -259,10 +270,18 @@ class Backtesting:
|
|||||||
candle_type=CandleType.from_string(self.exchange._ft_has["mark_ohlcv_price"])
|
candle_type=CandleType.from_string(self.exchange._ft_has["mark_ohlcv_price"])
|
||||||
)
|
)
|
||||||
# Combine data to avoid combining the data per trade.
|
# Combine data to avoid combining the data per trade.
|
||||||
|
unavailable_pairs = []
|
||||||
for pair in self.pairlists.whitelist:
|
for pair in self.pairlists.whitelist:
|
||||||
|
if pair not in self.exchange._leverage_tiers:
|
||||||
|
unavailable_pairs.append(pair)
|
||||||
|
continue
|
||||||
self.futures_data[pair] = funding_rates_dict[pair].merge(
|
self.futures_data[pair] = funding_rates_dict[pair].merge(
|
||||||
mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"])
|
mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"])
|
||||||
|
|
||||||
|
if unavailable_pairs:
|
||||||
|
raise OperationalException(
|
||||||
|
f"Pairs {', '.join(unavailable_pairs)} got no leverage tiers available. "
|
||||||
|
"It is therefore impossible to backtest with this pair at the moment.")
|
||||||
else:
|
else:
|
||||||
self.futures_data = {}
|
self.futures_data = {}
|
||||||
|
|
||||||
@ -300,10 +319,7 @@ class Backtesting:
|
|||||||
:param processed: a processed dictionary with format {pair, data}, which gets cleared to
|
:param processed: a processed dictionary with format {pair, data}, which gets cleared to
|
||||||
optimize memory usage!
|
optimize memory usage!
|
||||||
"""
|
"""
|
||||||
# Every change to this headers list must evaluate further usages of the resulting tuple
|
|
||||||
# and eventually change the constants for indexes at the top
|
|
||||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
|
||||||
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
|
||||||
data: Dict = {}
|
data: Dict = {}
|
||||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||||
|
|
||||||
@ -315,7 +331,7 @@ class Backtesting:
|
|||||||
|
|
||||||
if not pair_data.empty:
|
if not pair_data.empty:
|
||||||
# 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.advise_exit(
|
df_analyzed = self.strategy.advise_exit(
|
||||||
self.strategy.advise_entry(pair_data, {'pair': pair}),
|
self.strategy.advise_entry(pair_data, {'pair': pair}),
|
||||||
@ -328,13 +344,13 @@ class Backtesting:
|
|||||||
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'])
|
||||||
|
|
||||||
# Create a copy of the dataframe before shifting, that way the buy 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()
|
||||||
|
|
||||||
# To avoid using data from future, we use buy/sell signals shifted
|
# To avoid using data from future, we use entry/exit signals shifted
|
||||||
# from the previous candle
|
# from the previous candle
|
||||||
for col in headers[5:]:
|
for col in HEADERS[5:]:
|
||||||
tag_col = col in ('enter_tag', 'exit_tag')
|
tag_col = col in ('enter_tag', 'exit_tag')
|
||||||
if col in df_analyzed.columns:
|
if col in df_analyzed.columns:
|
||||||
df_analyzed.loc[:, col] = df_analyzed.loc[:, col].replace(
|
df_analyzed.loc[:, col] = df_analyzed.loc[:, col].replace(
|
||||||
@ -346,27 +362,27 @@ class Backtesting:
|
|||||||
|
|
||||||
# Convert from Pandas to list for performance reasons
|
# Convert from Pandas to list for performance reasons
|
||||||
# (Looping Pandas is slow.)
|
# (Looping Pandas is slow.)
|
||||||
data[pair] = df_analyzed[headers].values.tolist() if not df_analyzed.empty else []
|
data[pair] = df_analyzed[HEADERS].values.tolist() if not df_analyzed.empty else []
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_close_rate(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
def _get_close_rate(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
"""
|
"""
|
||||||
Get close rate for backtesting result
|
Get close rate for backtesting result
|
||||||
"""
|
"""
|
||||||
# Special handling if high or low hit STOP_LOSS or ROI
|
# Special handling if high or low hit STOP_LOSS or ROI
|
||||||
if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
||||||
return self._get_close_rate_for_stoploss(row, trade, sell, trade_dur)
|
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur)
|
||||||
elif sell.exit_type == (ExitType.ROI):
|
elif exit.exit_type == (ExitType.ROI):
|
||||||
return self._get_close_rate_for_roi(row, trade, sell, trade_dur)
|
return self._get_close_rate_for_roi(row, trade, exit, trade_dur)
|
||||||
else:
|
else:
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
def _get_close_rate_for_stoploss(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
# our stoploss was already lower than candle high,
|
# our stoploss was already lower than candle high,
|
||||||
# possibly due to a cancelled trade exit.
|
# possibly due to a cancelled trade exit.
|
||||||
# sell at open price.
|
# exit at open price.
|
||||||
is_short = trade.is_short or False
|
is_short = trade.is_short or False
|
||||||
leverage = trade.leverage or 1.0
|
leverage = trade.leverage or 1.0
|
||||||
side_1 = -1 if is_short else 1
|
side_1 = -1 if is_short else 1
|
||||||
@ -380,7 +396,7 @@ class Backtesting:
|
|||||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||||
# pessimistic price movement, which is moving just enough to arm stoploss and
|
# pessimistic price movement, which is moving just enough to arm stoploss and
|
||||||
# immediately going down to stop price.
|
# immediately going down to stop price.
|
||||||
if sell.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0:
|
if exit.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0:
|
||||||
if (
|
if (
|
||||||
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
|
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
|
||||||
and self.strategy.trailing_only_offset_is_reached
|
and self.strategy.trailing_only_offset_is_reached
|
||||||
@ -399,7 +415,7 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
assert stop_rate < row[HIGH_IDX]
|
assert stop_rate < row[HIGH_IDX]
|
||||||
|
|
||||||
# Limit lower-end to candle low to avoid sells below the low.
|
# Limit lower-end to candle low to avoid exits below the low.
|
||||||
# This still remains "worst case" - but "worst realistic case".
|
# This still remains "worst case" - but "worst realistic case".
|
||||||
if is_short:
|
if is_short:
|
||||||
return min(row[HIGH_IDX], stop_rate)
|
return min(row[HIGH_IDX], stop_rate)
|
||||||
@ -409,7 +425,7 @@ class Backtesting:
|
|||||||
# Set close_rate to stoploss
|
# Set close_rate to stoploss
|
||||||
return trade.stop_loss
|
return trade.stop_loss
|
||||||
|
|
||||||
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
|
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
is_short = trade.is_short or False
|
is_short = trade.is_short or False
|
||||||
leverage = trade.leverage or 1.0
|
leverage = trade.leverage or 1.0
|
||||||
@ -434,7 +450,7 @@ class Backtesting:
|
|||||||
and roi_entry % self.timeframe_min == 0
|
and roi_entry % self.timeframe_min == 0
|
||||||
and is_new_roi):
|
and is_new_roi):
|
||||||
# new ROI entry came into effect.
|
# new ROI entry came into effect.
|
||||||
# use Open rate if open_rate > calculated sell rate
|
# use Open rate if open_rate > calculated exit rate
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
|
|
||||||
if (trade_dur == 0 and (
|
if (trade_dur == 0 and (
|
||||||
@ -457,11 +473,11 @@ class Backtesting:
|
|||||||
# ROI on opening candles with custom pricing can only
|
# ROI on opening candles with custom pricing can only
|
||||||
# trigger if the entry was at Open or lower wick.
|
# trigger if the entry was at Open or lower wick.
|
||||||
# details: https: // github.com/freqtrade/freqtrade/issues/6261
|
# details: https: // github.com/freqtrade/freqtrade/issues/6261
|
||||||
# If open_rate is < open, only allow sells below the close on red candles.
|
# If open_rate is < open, only allow exits below the close on red candles.
|
||||||
raise ValueError("Opening candle ROI on red candles.")
|
raise ValueError("Opening candle ROI on red candles.")
|
||||||
|
|
||||||
# Use the maximum between close_rate and low as we
|
# Use the maximum between close_rate and low as we
|
||||||
# cannot sell outside of a candle.
|
# cannot exit outside of a candle.
|
||||||
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||||
return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX])
|
return min(max(close_rate, row[LOW_IDX]), row[HIGH_IDX])
|
||||||
|
|
||||||
@ -496,7 +512,7 @@ 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 _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
def _get_exit_trade_entry_for_candle(self, trade: LocalTrade,
|
||||||
row: Tuple) -> Optional[LocalTrade]:
|
row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
# Check if we need to adjust our current positions
|
# Check if we need to adjust our current positions
|
||||||
@ -508,34 +524,35 @@ class Backtesting:
|
|||||||
if check_adjust_entry:
|
if check_adjust_entry:
|
||||||
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
trade = self._get_adjust_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
|
||||||
exit_ = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
|
||||||
sell = self.strategy.should_exit(
|
exit_ = self.strategy.should_exit(
|
||||||
trade, row[OPEN_IDX], sell_candle_time, # type: ignore
|
trade, row[OPEN_IDX], exit_candle_time, # type: ignore
|
||||||
enter=enter, exit_=exit_,
|
enter=enter, exit_=exit_sig,
|
||||||
low=row[LOW_IDX], high=row[HIGH_IDX]
|
low=row[LOW_IDX], high=row[HIGH_IDX]
|
||||||
)
|
)
|
||||||
|
|
||||||
if sell.exit_flag:
|
if exit_.exit_flag:
|
||||||
trade.close_date = sell_candle_time
|
trade.close_date = exit_candle_time
|
||||||
|
|
||||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||||
try:
|
try:
|
||||||
closerate = self._get_close_rate(row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(row, trade, exit_, trade_dur)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
# call the custom exit price,with default value as previous closerate
|
# call the custom exit price,with default value as previous closerate
|
||||||
current_profit = trade.calc_profit_ratio(closerate)
|
current_profit = trade.calc_profit_ratio(closerate)
|
||||||
order_type = self.strategy.order_types['exit']
|
order_type = self.strategy.order_types['exit']
|
||||||
if sell.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
|
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
|
||||||
# Custom exit pricing only for sell-signals
|
# Custom exit pricing only for exit-signals
|
||||||
if order_type == 'limit':
|
if order_type == 'limit':
|
||||||
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
default_retval=closerate)(
|
default_retval=closerate)(
|
||||||
pair=trade.pair, trade=trade,
|
pair=trade.pair, trade=trade,
|
||||||
current_time=sell_candle_time,
|
current_time=exit_candle_time,
|
||||||
proposed_rate=closerate, current_profit=current_profit)
|
proposed_rate=closerate, current_profit=current_profit,
|
||||||
|
exit_tag=exit_.exit_reason)
|
||||||
# We can't place orders lower than current low.
|
# We can't place orders lower than current low.
|
||||||
# freqtrade does not support this in live, and the order would fill immediately
|
# freqtrade does not support this in live, and the order would fill immediately
|
||||||
if trade.is_short:
|
if trade.is_short:
|
||||||
@ -549,12 +566,12 @@ class Backtesting:
|
|||||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||||
rate=closerate,
|
rate=closerate,
|
||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
sell_reason=sell.exit_reason, # deprecated
|
sell_reason=exit_.exit_reason, # deprecated
|
||||||
exit_reason=sell.exit_reason,
|
exit_reason=exit_.exit_reason,
|
||||||
current_time=sell_candle_time):
|
current_time=exit_candle_time):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trade.exit_reason = sell.exit_reason
|
trade.exit_reason = exit_.exit_reason
|
||||||
|
|
||||||
# Checks and adds an exit tag, after checking that the length of the
|
# Checks and adds an exit tag, after checking that the length of the
|
||||||
# row has the length for an exit tag column
|
# row has the length for an exit tag column
|
||||||
@ -562,6 +579,7 @@ class Backtesting:
|
|||||||
len(row) > EXIT_TAG_IDX
|
len(row) > EXIT_TAG_IDX
|
||||||
and row[EXIT_TAG_IDX] is not None
|
and row[EXIT_TAG_IDX] is not None
|
||||||
and len(row[EXIT_TAG_IDX]) > 0
|
and len(row[EXIT_TAG_IDX]) > 0
|
||||||
|
and exit_.exit_type in (ExitType.EXIT_SIGNAL,)
|
||||||
):
|
):
|
||||||
trade.exit_reason = row[EXIT_TAG_IDX]
|
trade.exit_reason = row[EXIT_TAG_IDX]
|
||||||
|
|
||||||
@ -569,8 +587,8 @@ class Backtesting:
|
|||||||
order = Order(
|
order = Order(
|
||||||
id=self.order_id_counter,
|
id=self.order_id_counter,
|
||||||
ft_trade_id=trade.id,
|
ft_trade_id=trade.id,
|
||||||
order_date=sell_candle_time,
|
order_date=exit_candle_time,
|
||||||
order_update_date=sell_candle_time,
|
order_update_date=exit_candle_time,
|
||||||
ft_is_open=True,
|
ft_is_open=True,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
order_id=str(self.order_id_counter),
|
order_id=str(self.order_id_counter),
|
||||||
@ -591,8 +609,8 @@ class Backtesting:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _get_sell_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
def _get_exit_trade_entry(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
|
||||||
sell_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
trade.funding_fees = self.exchange.calculate_funding_fees(
|
trade.funding_fees = self.exchange.calculate_funding_fees(
|
||||||
@ -600,37 +618,35 @@ class Backtesting:
|
|||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
is_short=trade.is_short,
|
is_short=trade.is_short,
|
||||||
open_date=trade.open_date_utc,
|
open_date=trade.open_date_utc,
|
||||||
close_date=sell_candle_time,
|
close_date=exit_candle_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.timeframe_detail and trade.pair in self.detail_data:
|
if self.timeframe_detail and trade.pair in self.detail_data:
|
||||||
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
|
exit_candle_end = exit_candle_time + timedelta(minutes=self.timeframe_min)
|
||||||
|
|
||||||
detail_data = self.detail_data[trade.pair]
|
detail_data = self.detail_data[trade.pair]
|
||||||
detail_data = detail_data.loc[
|
detail_data = detail_data.loc[
|
||||||
(detail_data['date'] >= sell_candle_time) &
|
(detail_data['date'] >= exit_candle_time) &
|
||||||
(detail_data['date'] < sell_candle_end)
|
(detail_data['date'] < exit_candle_end)
|
||||||
].copy()
|
].copy()
|
||||||
if len(detail_data) == 0:
|
if len(detail_data) == 0:
|
||||||
# Fall back to "regular" data if no detail data was found for this candle
|
# Fall back to "regular" data if no detail data was found for this candle
|
||||||
return self._get_sell_trade_entry_for_candle(trade, row)
|
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||||
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
|
||||||
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
|
||||||
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]
|
||||||
detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
|
detail_data.loc[:, 'exit_short'] = row[ESHORT_IDX]
|
||||||
detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
|
detail_data.loc[:, 'enter_tag'] = row[ENTER_TAG_IDX]
|
||||||
detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
|
detail_data.loc[:, 'exit_tag'] = row[EXIT_TAG_IDX]
|
||||||
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
for det_row in detail_data[HEADERS].values.tolist():
|
||||||
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
|
res = self._get_exit_trade_entry_for_candle(trade, det_row)
|
||||||
for det_row in detail_data[headers].values.tolist():
|
|
||||||
res = self._get_sell_trade_entry_for_candle(trade, det_row)
|
|
||||||
if res:
|
if res:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return self._get_sell_trade_entry_for_candle(trade, row)
|
return self._get_exit_trade_entry_for_candle(trade, row)
|
||||||
|
|
||||||
def get_valid_price_and_stake(
|
def get_valid_price_and_stake(
|
||||||
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
||||||
@ -645,7 +661,7 @@ class Backtesting:
|
|||||||
proposed_rate=propose_rate, entry_tag=entry_tag,
|
proposed_rate=propose_rate, entry_tag=entry_tag,
|
||||||
side=direction,
|
side=direction,
|
||||||
) # default value is the open rate
|
) # default value is the open rate
|
||||||
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
|
# We can't place orders higher than current high (otherwise it'd be a stop limit entry)
|
||||||
# which freqtrade does not support in live.
|
# which freqtrade does not support in live.
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
propose_rate = max(propose_rate, row[LOW_IDX])
|
propose_rate = max(propose_rate, row[LOW_IDX])
|
||||||
@ -809,13 +825,13 @@ class Backtesting:
|
|||||||
if len(open_trades[pair]) > 0:
|
if len(open_trades[pair]) > 0:
|
||||||
for trade in open_trades[pair]:
|
for trade in open_trades[pair]:
|
||||||
if trade.open_order_id and trade.nr_of_successful_entries == 0:
|
if trade.open_order_id and trade.nr_of_successful_entries == 0:
|
||||||
# Ignore trade if buy-order did not fill yet
|
# Ignore trade if entry-order did not fill yet
|
||||||
continue
|
continue
|
||||||
sell_row = data[pair][-1]
|
exit_row = data[pair][-1]
|
||||||
|
|
||||||
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
trade.close_date = exit_row[DATE_IDX].to_pydatetime()
|
||||||
trade.exit_reason = ExitType.FORCE_EXIT.value
|
trade.exit_reason = ExitType.FORCE_EXIT.value
|
||||||
trade.close(sell_row[OPEN_IDX], show_msg=False)
|
trade.close(exit_row[OPEN_IDX], show_msg=False)
|
||||||
LocalTrade.close_bt_trade(trade)
|
LocalTrade.close_bt_trade(trade)
|
||||||
# Deepcopy object to have wallets update correctly
|
# Deepcopy object to have wallets update correctly
|
||||||
trade1 = deepcopy(trade)
|
trade1 = deepcopy(trade)
|
||||||
@ -865,7 +881,7 @@ class Backtesting:
|
|||||||
# Remove trade due to entry timeout expiration.
|
# Remove trade due to entry timeout expiration.
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# Close additional buy order
|
# Close additional entry order
|
||||||
del trade.orders[trade.orders.index(order)]
|
del trade.orders[trade.orders.index(order)]
|
||||||
if order.side == trade.exit_side:
|
if order.side == trade.exit_side:
|
||||||
self.timedout_exit_orders += 1
|
self.timedout_exit_orders += 1
|
||||||
@ -878,7 +894,7 @@ class Backtesting:
|
|||||||
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
|
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
|
||||||
try:
|
try:
|
||||||
# Row is treated as "current incomplete candle".
|
# Row is treated as "current incomplete candle".
|
||||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
# entry / exit signals are shifted by 1 to compensate for this.
|
||||||
row = data[pair][row_index]
|
row = data[pair][row_index]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
# missing Data for one pair at the end.
|
# missing Data for one pair at the end.
|
||||||
@ -943,14 +959,14 @@ class Backtesting:
|
|||||||
self.dataprovider._set_dataframe_max_index(row_index)
|
self.dataprovider._set_dataframe_max_index(row_index)
|
||||||
|
|
||||||
for t in list(open_trades[pair]):
|
for t in list(open_trades[pair]):
|
||||||
# 1. Cancel expired buy/sell orders.
|
# 1. Cancel expired entry/exit orders.
|
||||||
if self.check_order_cancel(t, current_time):
|
if self.check_order_cancel(t, current_time):
|
||||||
# Close trade due to buy timeout expiration.
|
# Close trade due to entry timeout expiration.
|
||||||
open_trade_count -= 1
|
open_trade_count -= 1
|
||||||
open_trades[pair].remove(t)
|
open_trades[pair].remove(t)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# 2. Process buys.
|
# 2. Process entries.
|
||||||
# without positionstacking, we can only have one open trade per pair.
|
# without positionstacking, we can only have one open trade per pair.
|
||||||
# max_open_trades must be respected
|
# max_open_trades must be respected
|
||||||
# don't open on the last row
|
# don't open on the last row
|
||||||
@ -966,7 +982,7 @@ class Backtesting:
|
|||||||
if trade:
|
if trade:
|
||||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||||
# This emulates previous behavior - not sure if this is correct
|
# This emulates previous behavior - not sure if this is correct
|
||||||
# Prevents buying if the trade-slot was freed in this candle
|
# Prevents entering if the trade-slot was freed in this candle
|
||||||
open_trade_count_start += 1
|
open_trade_count_start += 1
|
||||||
open_trade_count += 1
|
open_trade_count += 1
|
||||||
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
|
||||||
@ -981,18 +997,18 @@ class Backtesting:
|
|||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# 4. Create sell orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
self._get_sell_trade_entry(trade, row) # Place sell order if necessary
|
self._get_exit_trade_entry(trade, row) # Place exit order if necessary
|
||||||
|
|
||||||
# 5. Process sell 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.price, row):
|
if order and self._get_order_filled(order.price, row):
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.close_date = current_time
|
trade.close_date = current_time
|
||||||
trade.close(order.price, show_msg=False)
|
trade.close(order.price, show_msg=False)
|
||||||
|
|
||||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
# logger.debug(f"{pair} - Backtesting exit {trade}")
|
||||||
open_trade_count -= 1
|
open_trade_count -= 1
|
||||||
open_trades[pair].remove(trade)
|
open_trades[pair].remove(trade)
|
||||||
LocalTrade.close_bt_trade(trade)
|
LocalTrade.close_bt_trade(trade)
|
||||||
@ -1048,7 +1064,7 @@ class Backtesting:
|
|||||||
"No data left after adjusting for startup candles.")
|
"No data left after adjusting for startup candles.")
|
||||||
|
|
||||||
# Use preprocessed_tmp for date generation (the trimmed dataframe).
|
# Use preprocessed_tmp for date generation (the trimmed dataframe).
|
||||||
# Backtesting will re-trim the dataframes after buy/sell signal generation.
|
# Backtesting will re-trim the dataframes after entry/exit signal generation.
|
||||||
min_date, max_date = history.get_timerange(preprocessed_tmp)
|
min_date, max_date = history.get_timerange(preprocessed_tmp)
|
||||||
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||||
@ -1070,8 +1086,31 @@ class Backtesting:
|
|||||||
})
|
})
|
||||||
self.all_results[self.strategy.get_strategy_name()] = results
|
self.all_results[self.strategy.get_strategy_name()] = results
|
||||||
|
|
||||||
|
if (self.config.get('export', 'none') == 'signals' and
|
||||||
|
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||||
|
self._generate_trade_signal_candles(preprocessed_tmp, results)
|
||||||
|
|
||||||
return min_date, max_date
|
return min_date, max_date
|
||||||
|
|
||||||
|
def _generate_trade_signal_candles(self, preprocessed_df, bt_results):
|
||||||
|
signal_candles_only = {}
|
||||||
|
for pair in preprocessed_df.keys():
|
||||||
|
signal_candles_only_df = DataFrame()
|
||||||
|
|
||||||
|
pairdf = preprocessed_df[pair]
|
||||||
|
resdf = bt_results['results']
|
||||||
|
pairresults = resdf.loc[(resdf["pair"] == pair)]
|
||||||
|
|
||||||
|
if pairdf.shape[0] > 0:
|
||||||
|
for t, v in pairresults.open_date.items():
|
||||||
|
allinds = pairdf.loc[(pairdf['date'] < v)]
|
||||||
|
signal_inds = allinds.iloc[[-1]]
|
||||||
|
signal_candles_only_df = pd.concat([signal_candles_only_df, signal_inds])
|
||||||
|
|
||||||
|
signal_candles_only[pair] = signal_candles_only_df
|
||||||
|
|
||||||
|
self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only
|
||||||
|
|
||||||
def _get_min_cached_backtest_date(self):
|
def _get_min_cached_backtest_date(self):
|
||||||
min_backtest_date = None
|
min_backtest_date = None
|
||||||
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
|
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
|
||||||
@ -1130,9 +1169,13 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
self.results = results
|
self.results = results
|
||||||
|
|
||||||
if self.config.get('export', 'none') == 'trades':
|
if self.config.get('export', 'none') in ('trades', 'signals'):
|
||||||
store_backtest_stats(self.config['exportfilename'], self.results)
|
store_backtest_stats(self.config['exportfilename'], self.results)
|
||||||
|
|
||||||
|
if (self.config.get('export', 'none') == 'signals' and
|
||||||
|
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||||
|
store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs)
|
||||||
|
|
||||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||||
if 'strategy_list' in self.config and len(self.results) > 0:
|
if 'strategy_list' in self.config and len(self.results) > 0:
|
||||||
self.results['strategy_comparison'] = sorted(
|
self.results['strategy_comparison'] = sorted(
|
||||||
|
@ -44,6 +44,7 @@ class EdgeCli:
|
|||||||
|
|
||||||
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
self.edge._timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
|
self.strategy.bot_start()
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
result = self.edge.calculate(self.config['exchange']['pair_whitelist'])
|
result = self.edge.calculate(self.config['exchange']['pair_whitelist'])
|
||||||
|
@ -10,7 +10,7 @@ import warnings
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import progressbar
|
import progressbar
|
||||||
import rapidjson
|
import rapidjson
|
||||||
@ -290,7 +290,7 @@ class Hyperopt:
|
|||||||
self.assign_params(params_dict, 'protection')
|
self.assign_params(params_dict, 'protection')
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'roi'):
|
if HyperoptTools.has_space(self.config, 'roi'):
|
||||||
self.backtesting.strategy.minimal_roi = ( # type: ignore
|
self.backtesting.strategy.minimal_roi = (
|
||||||
self.custom_hyperopt.generate_roi_table(params_dict))
|
self.custom_hyperopt.generate_roi_table(params_dict))
|
||||||
|
|
||||||
if HyperoptTools.has_space(self.config, 'stoploss'):
|
if HyperoptTools.has_space(self.config, 'stoploss'):
|
||||||
@ -409,6 +409,51 @@ class Hyperopt:
|
|||||||
# Store non-trimmed data - will be trimmed after signal generation.
|
# Store non-trimmed data - will be trimmed after signal generation.
|
||||||
dump(preprocessed, self.data_pickle_file)
|
dump(preprocessed, self.data_pickle_file)
|
||||||
|
|
||||||
|
def get_asked_points(self, n_points: int) -> Tuple[List[List[Any]], List[bool]]:
|
||||||
|
"""
|
||||||
|
Enforce points returned from `self.opt.ask` have not been already evaluated
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Try to get points using `self.opt.ask` first
|
||||||
|
2. Discard the points that have already been evaluated
|
||||||
|
3. Retry using `self.opt.ask` up to 3 times
|
||||||
|
4. If still some points are missing in respect to `n_points`, random sample some points
|
||||||
|
5. Repeat until at least `n_points` points in the `asked_non_tried` list
|
||||||
|
6. Return a list with length truncated at `n_points`
|
||||||
|
"""
|
||||||
|
def unique_list(a_list):
|
||||||
|
new_list = []
|
||||||
|
for item in a_list:
|
||||||
|
if item not in new_list:
|
||||||
|
new_list.append(item)
|
||||||
|
return new_list
|
||||||
|
i = 0
|
||||||
|
asked_non_tried: List[List[Any]] = []
|
||||||
|
is_random: List[bool] = []
|
||||||
|
while i < 5 and len(asked_non_tried) < n_points:
|
||||||
|
if i < 3:
|
||||||
|
self.opt.cache_ = {}
|
||||||
|
asked = unique_list(self.opt.ask(n_points=n_points * 5))
|
||||||
|
is_random = [False for _ in range(len(asked))]
|
||||||
|
else:
|
||||||
|
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
||||||
|
is_random = [True for _ in range(len(asked))]
|
||||||
|
is_random += [rand for x, rand in zip(asked, is_random)
|
||||||
|
if x not in self.opt.Xi
|
||||||
|
and x not in asked_non_tried]
|
||||||
|
asked_non_tried += [x for x in asked
|
||||||
|
if x not in self.opt.Xi
|
||||||
|
and x not in asked_non_tried]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if asked_non_tried:
|
||||||
|
return (
|
||||||
|
asked_non_tried[:min(len(asked_non_tried), n_points)],
|
||||||
|
is_random[:min(len(asked_non_tried), n_points)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
||||||
logger.info(f"Using optimizer random state: {self.random_state}")
|
logger.info(f"Using optimizer random state: {self.random_state}")
|
||||||
@ -420,9 +465,10 @@ class Hyperopt:
|
|||||||
|
|
||||||
# We don't need exchange instance anymore while running hyperopt
|
# We don't need exchange instance anymore while running hyperopt
|
||||||
self.backtesting.exchange.close()
|
self.backtesting.exchange.close()
|
||||||
self.backtesting.exchange._api = None # type: ignore
|
self.backtesting.exchange._api = None
|
||||||
self.backtesting.exchange._api_async = None # type: ignore
|
self.backtesting.exchange._api_async = None
|
||||||
self.backtesting.exchange.loop = None # type: ignore
|
self.backtesting.exchange.loop = None # type: ignore
|
||||||
|
self.backtesting.exchange._loop_lock = None # type: ignore
|
||||||
# self.backtesting.exchange = None # type: ignore
|
# self.backtesting.exchange = None # type: ignore
|
||||||
self.backtesting.pairlists = None # type: ignore
|
self.backtesting.pairlists = None # type: ignore
|
||||||
|
|
||||||
@ -473,7 +519,7 @@ class Hyperopt:
|
|||||||
n_rest = (i + 1) * jobs - self.total_epochs
|
n_rest = (i + 1) * jobs - self.total_epochs
|
||||||
current_jobs = jobs - n_rest if n_rest > 0 else jobs
|
current_jobs = jobs - n_rest if n_rest > 0 else jobs
|
||||||
|
|
||||||
asked = self.opt.ask(n_points=current_jobs)
|
asked, is_random = self.get_asked_points(n_points=current_jobs)
|
||||||
f_val = self.run_optimizer_parallel(parallel, asked, i)
|
f_val = self.run_optimizer_parallel(parallel, asked, i)
|
||||||
self.opt.tell(asked, [v['loss'] for v in f_val])
|
self.opt.tell(asked, [v['loss'] for v in f_val])
|
||||||
|
|
||||||
@ -492,6 +538,7 @@ class Hyperopt:
|
|||||||
# evaluations can take different time. Here they are aligned in the
|
# evaluations can take different time. Here they are aligned in the
|
||||||
# order they will be shown to the user.
|
# order they will be shown to the user.
|
||||||
val['is_best'] = is_best
|
val['is_best'] = is_best
|
||||||
|
val['is_random'] = is_random[j]
|
||||||
self.print_results(val)
|
self.print_results(val)
|
||||||
|
|
||||||
if is_best:
|
if is_best:
|
||||||
|
@ -41,7 +41,8 @@ class HyperoptTools():
|
|||||||
"""
|
"""
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||||
strategy_objs = StrategyResolver.search_all_objects(directory, False)
|
strategy_objs = StrategyResolver.search_all_objects(
|
||||||
|
directory, False, config.get('recursive_strategy_search', False))
|
||||||
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
|
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
|
||||||
if strategies:
|
if strategies:
|
||||||
strategy = strategies[0]
|
strategy = strategies[0]
|
||||||
@ -310,6 +311,8 @@ class HyperoptTools():
|
|||||||
if not has_drawdown:
|
if not has_drawdown:
|
||||||
# Ensure compatibility with older versions of hyperopt results
|
# Ensure compatibility with older versions of hyperopt results
|
||||||
trials['results_metrics.max_drawdown_account'] = None
|
trials['results_metrics.max_drawdown_account'] = None
|
||||||
|
if 'is_random' not in trials.columns:
|
||||||
|
trials['is_random'] = False
|
||||||
|
|
||||||
# New mode, using backtest result for metrics
|
# New mode, using backtest result for metrics
|
||||||
trials['results_metrics.winsdrawslosses'] = trials.apply(
|
trials['results_metrics.winsdrawslosses'] = trials.apply(
|
||||||
@ -322,12 +325,12 @@ class HyperoptTools():
|
|||||||
'results_metrics.profit_total', 'results_metrics.holding_avg',
|
'results_metrics.profit_total', 'results_metrics.holding_avg',
|
||||||
'results_metrics.max_drawdown',
|
'results_metrics.max_drawdown',
|
||||||
'results_metrics.max_drawdown_account', 'results_metrics.max_drawdown_abs',
|
'results_metrics.max_drawdown_account', 'results_metrics.max_drawdown_abs',
|
||||||
'loss', 'is_initial_point', 'is_best']]
|
'loss', 'is_initial_point', 'is_random', 'is_best']]
|
||||||
|
|
||||||
trials.columns = [
|
trials.columns = [
|
||||||
'Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
|
'Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
|
||||||
'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account',
|
'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account',
|
||||||
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_best'
|
'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best'
|
||||||
]
|
]
|
||||||
|
|
||||||
return trials
|
return trials
|
||||||
@ -349,9 +352,11 @@ class HyperoptTools():
|
|||||||
trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown)
|
trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown)
|
||||||
|
|
||||||
trials['is_profit'] = False
|
trials['is_profit'] = False
|
||||||
trials.loc[trials['is_initial_point'], 'Best'] = '* '
|
trials.loc[trials['is_initial_point'] | trials['is_random'], 'Best'] = '* '
|
||||||
trials.loc[trials['is_best'], 'Best'] = 'Best'
|
trials.loc[trials['is_best'], 'Best'] = 'Best'
|
||||||
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
|
trials.loc[
|
||||||
|
(trials['is_initial_point'] | trials['is_random']) & trials['is_best'],
|
||||||
|
'Best'] = '* Best'
|
||||||
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
|
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
|
||||||
trials['Trades'] = trials['Trades'].astype(str)
|
trials['Trades'] = trials['Trades'].astype(str)
|
||||||
# perc_multi = 1 if legacy_mode else 100
|
# perc_multi = 1 if legacy_mode else 100
|
||||||
@ -407,7 +412,7 @@ class HyperoptTools():
|
|||||||
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
|
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
|
||||||
str(trials.loc[i][j]), Style.RESET_ALL)
|
str(trials.loc[i][j]), Style.RESET_ALL)
|
||||||
|
|
||||||
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
|
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random'])
|
||||||
if remove_header > 0:
|
if remove_header > 0:
|
||||||
table = tabulate.tabulate(
|
table = tabulate.tabulate(
|
||||||
trials.to_dict(orient='list'), tablefmt='orgtbl',
|
trials.to_dict(orient='list'), tablefmt='orgtbl',
|
||||||
|
@ -9,10 +9,10 @@ from pandas import DataFrame, to_datetime
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
|
from freqtrade.data.btanalysis import (calculate_cagr, calculate_csum, calculate_market_change,
|
||||||
calculate_max_drawdown)
|
calculate_max_drawdown)
|
||||||
from freqtrade.misc import (decimals_per_coin, file_dump_json, get_backtest_metadata_filename,
|
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
|
||||||
round_coin_value)
|
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -45,6 +45,29 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
|||||||
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
||||||
|
|
||||||
|
|
||||||
|
def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> Path:
|
||||||
|
"""
|
||||||
|
Stores backtest trade signal candles
|
||||||
|
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||||
|
Filenames will be appended with a timestamp right before the suffix
|
||||||
|
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
|
||||||
|
as filename
|
||||||
|
:param stats: Dict containing the backtesting signal candles
|
||||||
|
"""
|
||||||
|
if recordfilename.is_dir():
|
||||||
|
filename = (recordfilename /
|
||||||
|
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl')
|
||||||
|
else:
|
||||||
|
filename = Path.joinpath(
|
||||||
|
recordfilename.parent,
|
||||||
|
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl'
|
||||||
|
)
|
||||||
|
|
||||||
|
file_dump_joblib(filename, candles)
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate floatformat (goes in line with _generate_result_line())
|
Generate floatformat (goes in line with _generate_result_line())
|
||||||
@ -241,7 +264,7 @@ def generate_edge_table(results: dict) -> str:
|
|||||||
|
|
||||||
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
return tabulate(tabular_data, headers=headers,
|
return tabulate(tabular_data, headers=headers,
|
||||||
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
|
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
|
||||||
|
|
||||||
|
|
||||||
def _get_resample_from_period(period: str) -> str:
|
def _get_resample_from_period(period: str) -> str:
|
||||||
@ -423,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str],
|
|||||||
'profit_total_abs': results['profit_abs'].sum(),
|
'profit_total_abs': results['profit_abs'].sum(),
|
||||||
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
|
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
|
||||||
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
|
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
|
||||||
|
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
|
||||||
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
'backtest_start_ts': int(min_date.timestamp() * 1000),
|
||||||
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
|
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
@ -727,6 +751,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||||
|
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
||||||
('Trades per day', strat_results['trades_per_day']),
|
('Trades per day', strat_results['trades_per_day']),
|
||||||
('Avg. daily profit %',
|
('Avg. daily profit %',
|
||||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||||
|
@ -429,12 +429,10 @@ class LocalTrade():
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
|
||||||
leverage = self.leverage or 1.0
|
|
||||||
is_short = self.is_short or False
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||||
f'is_short={is_short}, leverage={leverage}, '
|
f'is_short={self.is_short or False}, leverage={self.leverage or 1.0}, '
|
||||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -632,6 +632,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||||
IStrategy.dp = DataProvider(config, exchange)
|
IStrategy.dp = DataProvider(config, exchange)
|
||||||
|
strategy.bot_start()
|
||||||
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
|
||||||
timerange = plot_elements['timerange']
|
timerange = plot_elements['timerange']
|
||||||
trades = plot_elements['trades']
|
trades = plot_elements['trades']
|
||||||
|
@ -23,7 +23,7 @@ class HyperOptLossResolver(IResolver):
|
|||||||
object_type = IHyperOptLoss
|
object_type = IHyperOptLoss
|
||||||
object_type_str = "HyperoptLoss"
|
object_type_str = "HyperoptLoss"
|
||||||
user_subdir = USERPATH_HYPEROPTS
|
user_subdir = USERPATH_HYPEROPTS
|
||||||
initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
initial_search_path = Path(__file__).parent.parent.joinpath('optimize/hyperopt_loss').resolve()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_hyperoptloss(config: Dict) -> IHyperOptLoss:
|
def load_hyperoptloss(config: Dict) -> IHyperOptLoss:
|
||||||
|
@ -44,7 +44,7 @@ class IResolver:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None,
|
def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None,
|
||||||
extra_dir: Optional[str] = None) -> List[Path]:
|
extra_dirs: List[str] = []) -> List[Path]:
|
||||||
|
|
||||||
abs_paths: List[Path] = []
|
abs_paths: List[Path] = []
|
||||||
if cls.initial_search_path:
|
if cls.initial_search_path:
|
||||||
@ -53,9 +53,9 @@ class IResolver:
|
|||||||
if user_subdir:
|
if user_subdir:
|
||||||
abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
|
abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
|
||||||
|
|
||||||
if extra_dir:
|
# Add extra directory to the top of the search paths
|
||||||
# Add extra directory to the top of the search paths
|
for dir in extra_dirs:
|
||||||
abs_paths.insert(0, Path(extra_dir).resolve())
|
abs_paths.insert(0, Path(dir).resolve())
|
||||||
|
|
||||||
return abs_paths
|
return abs_paths
|
||||||
|
|
||||||
@ -164,9 +164,13 @@ class IResolver:
|
|||||||
:return: Object instance or None
|
:return: Object instance or None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
extra_dirs: List[str] = []
|
||||||
|
if extra_dir:
|
||||||
|
extra_dirs.append(extra_dir)
|
||||||
|
|
||||||
abs_paths = cls.build_search_paths(config,
|
abs_paths = cls.build_search_paths(config,
|
||||||
user_subdir=cls.user_subdir,
|
user_subdir=cls.user_subdir,
|
||||||
extra_dir=extra_dir)
|
extra_dirs=extra_dirs)
|
||||||
|
|
||||||
found_object = cls._load_object(paths=abs_paths, object_name=object_name,
|
found_object = cls._load_object(paths=abs_paths, object_name=object_name,
|
||||||
kwargs=kwargs)
|
kwargs=kwargs)
|
||||||
@ -178,18 +182,25 @@ class IResolver:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_all_objects(cls, directory: Path,
|
def search_all_objects(cls, directory: Path, enum_failed: bool,
|
||||||
enum_failed: bool) -> List[Dict[str, Any]]:
|
recursive: bool = False) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Searches a directory for valid objects
|
Searches a directory for valid objects
|
||||||
:param directory: Path to search
|
:param directory: Path to search
|
||||||
:param enum_failed: If True, will return None for modules which fail.
|
:param enum_failed: If True, will return None for modules which fail.
|
||||||
Otherwise, failing modules are skipped.
|
Otherwise, failing modules are skipped.
|
||||||
|
:param recursive: Recursively walk directory tree searching for strategies
|
||||||
:return: List of dicts containing 'name', 'class' and 'location' entries
|
:return: List of dicts containing 'name', 'class' and 'location' entries
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'")
|
logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'")
|
||||||
objects = []
|
objects = []
|
||||||
for entry in directory.iterdir():
|
for entry in directory.iterdir():
|
||||||
|
if (
|
||||||
|
recursive and entry.is_dir()
|
||||||
|
and not entry.name.startswith('__')
|
||||||
|
and not entry.name.startswith('.')
|
||||||
|
):
|
||||||
|
objects.extend(cls.search_all_objects(entry, enum_failed, recursive=recursive))
|
||||||
# Only consider python files
|
# Only consider python files
|
||||||
if entry.suffix != '.py':
|
if entry.suffix != '.py':
|
||||||
logger.debug('Ignoring %s', entry)
|
logger.debug('Ignoring %s', entry)
|
||||||
|
@ -7,8 +7,9 @@ import logging
|
|||||||
import tempfile
|
import tempfile
|
||||||
from base64 import urlsafe_b64decode
|
from base64 import urlsafe_b64decode
|
||||||
from inspect import getfullargspec
|
from inspect import getfullargspec
|
||||||
|
from os import walk
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
|
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
|
||||||
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
|
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
|
||||||
@ -216,15 +217,19 @@ class StrategyResolver(IResolver):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
|
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
|
||||||
|
|
||||||
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
|
_populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
|
||||||
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
_buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
||||||
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
|
_sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
|
||||||
if any(x == 2 for x in [
|
if any(x == 2 for x in [
|
||||||
strategy._populate_fun_len,
|
_populate_fun_len,
|
||||||
strategy._buy_fun_len,
|
_buy_fun_len,
|
||||||
strategy._sell_fun_len
|
_sell_fun_len
|
||||||
]):
|
]):
|
||||||
strategy.INTERFACE_VERSION = 1
|
raise OperationalException(
|
||||||
|
"Strategy Interface v1 is no longer supported. "
|
||||||
|
"Please update your strategy to implement "
|
||||||
|
"`populate_indicators`, `populate_entry_trend` and `populate_exit_trend` "
|
||||||
|
"with the metadata argument. ")
|
||||||
return strategy
|
return strategy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -237,10 +242,19 @@ class StrategyResolver(IResolver):
|
|||||||
:param extra_dir: additional directory to search for the given strategy
|
:param extra_dir: additional directory to search for the given strategy
|
||||||
:return: Strategy instance or None
|
:return: Strategy instance or None
|
||||||
"""
|
"""
|
||||||
|
if config.get('recursive_strategy_search', False):
|
||||||
|
extra_dirs: List[str] = [
|
||||||
|
path[0] for path in walk(f"{config['user_data_dir']}/{USERPATH_STRATEGIES}")
|
||||||
|
] # sub-directories
|
||||||
|
else:
|
||||||
|
extra_dirs = []
|
||||||
|
|
||||||
|
if extra_dir:
|
||||||
|
extra_dirs.append(extra_dir)
|
||||||
|
|
||||||
abs_paths = StrategyResolver.build_search_paths(config,
|
abs_paths = StrategyResolver.build_search_paths(config,
|
||||||
user_subdir=USERPATH_STRATEGIES,
|
user_subdir=USERPATH_STRATEGIES,
|
||||||
extra_dir=extra_dir)
|
extra_dirs=extra_dirs)
|
||||||
|
|
||||||
if ":" in strategy_name:
|
if ":" in strategy_name:
|
||||||
logger.info("loading base64 encoded strategy")
|
logger.info("loading base64 encoded strategy")
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||||
|
|
||||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||||
|
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
|
||||||
from freqtrade.enums import BacktestState
|
from freqtrade.enums import BacktestState
|
||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
|
||||||
|
BacktestResponse)
|
||||||
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
|
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
|
||||||
from freqtrade.rpc.api_server.webserver import ApiServer
|
from freqtrade.rpc.api_server.webserver import ApiServer
|
||||||
from freqtrade.rpc.rpc import RPCException
|
from freqtrade.rpc.rpc import RPCException
|
||||||
@ -200,3 +203,30 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
|
|||||||
"progress": 0,
|
"progress": 0,
|
||||||
"status_msg": "Backtest ended",
|
"status_msg": "Backtest ended",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], tags=['webserver', 'backtest'])
|
||||||
|
def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
|
||||||
|
# Get backtest result history, read from metadata files
|
||||||
|
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/backtest/history/result', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||||
|
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
|
||||||
|
# Get backtest result history, read from metadata files
|
||||||
|
fn = config['user_data_dir'] / 'backtest_results' / filename
|
||||||
|
results: Dict[str, Any] = {
|
||||||
|
'metadata': {},
|
||||||
|
'strategy': {},
|
||||||
|
'strategy_comparison': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
load_and_merge_backtest_result(strategy, fn, results)
|
||||||
|
return {
|
||||||
|
"status": "ended",
|
||||||
|
"running": False,
|
||||||
|
"step": "",
|
||||||
|
"progress": 1,
|
||||||
|
"status_msg": "Historic result",
|
||||||
|
"backtest_result": results,
|
||||||
|
}
|
||||||
|
@ -421,6 +421,13 @@ class BacktestResponse(BaseModel):
|
|||||||
backtest_result: Optional[Dict[str, Any]]
|
backtest_result: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class BacktestHistoryEntry(BaseModel):
|
||||||
|
filename: str
|
||||||
|
strategy: str
|
||||||
|
run_id: str
|
||||||
|
backtest_start_time: int
|
||||||
|
|
||||||
|
|
||||||
class SysInfo(BaseModel):
|
class SysInfo(BaseModel):
|
||||||
cpu_pct: List[float]
|
cpu_pct: List[float]
|
||||||
ram_pct: float
|
ram_pct: float
|
||||||
|
@ -35,7 +35,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 1.13: forcebuy supports stake_amount
|
# 1.13: forcebuy supports stake_amount
|
||||||
# versions 2.xx -> futures/short branch
|
# versions 2.xx -> futures/short branch
|
||||||
# 2.14: Add entry/exit orders to trade response
|
# 2.14: Add entry/exit orders to trade response
|
||||||
API_VERSION = 2.14
|
# 2.15: Add backtest history endpoints
|
||||||
|
API_VERSION = 2.15
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@ -252,7 +253,8 @@ def list_strategies(config=Depends(get_config)):
|
|||||||
directory = Path(config.get(
|
directory = Path(config.get(
|
||||||
'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
strategies = StrategyResolver.search_all_objects(directory, False)
|
strategies = StrategyResolver.search_all_objects(
|
||||||
|
directory, False, config.get('recursive_strategy_search', False))
|
||||||
strategies = sorted(strategies, key=lambda x: x['name'])
|
strategies = sorted(strategies, key=lambda x: x['name'])
|
||||||
|
|
||||||
return {'strategies': [x['name'] for x in strategies]}
|
return {'strategies': [x['name'] for x in strategies]}
|
||||||
|
@ -2,7 +2,7 @@ import logging
|
|||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
import rapidjson
|
import orjson
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import Depends, FastAPI
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
@ -24,7 +24,7 @@ class FTJSONResponse(JSONResponse):
|
|||||||
Use rapidjson for responses
|
Use rapidjson for responses
|
||||||
Handles NaN and Inf / -Inf in a javascript way by default.
|
Handles NaN and Inf / -Inf in a javascript way by default.
|
||||||
"""
|
"""
|
||||||
return rapidjson.dumps(content).encode("utf-8")
|
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
|
||||||
|
|
||||||
|
|
||||||
class ApiServer(RPCHandler):
|
class ApiServer(RPCHandler):
|
||||||
|
@ -387,7 +387,7 @@ class Telegram(RPCHandler):
|
|||||||
else:
|
else:
|
||||||
return "\N{CROSS MARK}"
|
return "\N{CROSS MARK}"
|
||||||
|
|
||||||
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
|
def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
|
||||||
"""
|
"""
|
||||||
Prepare details of trade with entry adjustment enabled
|
Prepare details of trade with entry adjustment enabled
|
||||||
"""
|
"""
|
||||||
@ -405,7 +405,7 @@ class Telegram(RPCHandler):
|
|||||||
if x == 0:
|
if x == 0:
|
||||||
lines.append(f"*Entry #{x+1}:*")
|
lines.append(f"*Entry #{x+1}:*")
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||||
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
||||||
else:
|
else:
|
||||||
sumA = 0
|
sumA = 0
|
||||||
@ -429,7 +429,7 @@ class Telegram(RPCHandler):
|
|||||||
lines.append("({})".format(cur_entry_datetime
|
lines.append("({})".format(cur_entry_datetime
|
||||||
.humanize(granularity=["day", "hour", "minute"])))
|
.humanize(granularity=["day", "hour", "minute"])))
|
||||||
lines.append(
|
lines.append(
|
||||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||||
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
||||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||||
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||||
@ -472,7 +472,7 @@ class Telegram(RPCHandler):
|
|||||||
"*Current Pair:* {pair}",
|
"*Current Pair:* {pair}",
|
||||||
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
||||||
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
||||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
"*Amount:* `{amount} ({stake_amount} {quote_currency})`",
|
||||||
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
||||||
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
|
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
|
||||||
]
|
]
|
||||||
@ -943,7 +943,7 @@ class Telegram(RPCHandler):
|
|||||||
else:
|
else:
|
||||||
fiat_currency = self._config.get('fiat_display_currency', '')
|
fiat_currency = self._config.get('fiat_display_currency', '')
|
||||||
try:
|
try:
|
||||||
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
|
statlist, _, _ = self._rpc._rpc_status_table(
|
||||||
self._config['stake_currency'], fiat_currency)
|
self._config['stake_currency'], fiat_currency)
|
||||||
except RPCException:
|
except RPCException:
|
||||||
self._send_msg(msg='No open trade found.')
|
self._send_msg(msg='No open trade found.')
|
||||||
|
@ -23,7 +23,7 @@ class InformativeData:
|
|||||||
def informative(timeframe: str, asset: str = '',
|
def informative(timeframe: str, asset: str = '',
|
||||||
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
|
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
|
||||||
*,
|
*,
|
||||||
candle_type: Optional[CandleType] = None,
|
candle_type: Optional[Union[CandleType, str]] = None,
|
||||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
"""
|
"""
|
||||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
@ -3,7 +3,6 @@ IStrategy interface
|
|||||||
This module defines the interface to apply for strategies
|
This module defines the interface to apply for strategies
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
@ -44,14 +43,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
# Strategy interface version
|
# Strategy interface version
|
||||||
# Default to version 2
|
# Default to version 2
|
||||||
# Version 1 is the initial interface without metadata dict
|
# Version 1 is the initial interface without metadata dict - deprecated and no longer supported.
|
||||||
# Version 2 populate_* include metadata dict
|
# Version 2 populate_* include metadata dict
|
||||||
# Version 3 - First version with short and leverage support
|
# Version 3 - First version with short and leverage support
|
||||||
INTERFACE_VERSION: int = 3
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
_populate_fun_len: int = 0
|
|
||||||
_buy_fun_len: int = 0
|
|
||||||
_sell_fun_len: int = 0
|
|
||||||
_ft_params_from_file: Dict
|
_ft_params_from_file: Dict
|
||||||
# associated minimal roi
|
# associated minimal roi
|
||||||
minimal_roi: Dict = {}
|
minimal_roi: Dict = {}
|
||||||
@ -114,7 +110,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# Class level variables (intentional) containing
|
# Class level variables (intentional) containing
|
||||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||||
# and wallets - access to the current balance.
|
# and wallets - access to the current balance.
|
||||||
dp: Optional[DataProvider]
|
dp: DataProvider
|
||||||
wallets: Optional[Wallets] = None
|
wallets: Optional[Wallets] = None
|
||||||
# Filled from configuration
|
# Filled from configuration
|
||||||
stake_currency: str
|
stake_currency: str
|
||||||
@ -197,6 +193,13 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return self.populate_sell_trend(dataframe, metadata)
|
return self.populate_sell_trend(dataframe, metadata)
|
||||||
|
|
||||||
|
def bot_start(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Called only once after bot instantiation.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def bot_loop_start(self, **kwargs) -> None:
|
def bot_loop_start(self, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Called at the start of the bot iteration (one loop).
|
Called at the start of the bot iteration (one loop).
|
||||||
@ -206,18 +209,18 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_buy_timeout(self, pair: str, trade: Trade, order: Order,
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
DEPRECATED: Please use `check_entry_timeout` instead.
|
DEPRECATED: Please use `check_entry_timeout` instead.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_entry_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Check entry timeout function callback.
|
Check entry timeout function callback.
|
||||||
This method can be used to override the enter-timeout.
|
This method can be used to override the entry-timeout.
|
||||||
It is called whenever a limit entry order has been created,
|
It is called whenever a limit entry order has been created,
|
||||||
and is not yet fully filled.
|
and is not yet fully filled.
|
||||||
Configuration options in `unfilledtimeout` will be verified before this,
|
Configuration options in `unfilledtimeout` will be verified before this,
|
||||||
@ -225,8 +228,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
When not implemented by a strategy, this simply returns False.
|
When not implemented by a strategy, this simply returns False.
|
||||||
:param pair: Pair the trade is for
|
:param pair: Pair the trade is for
|
||||||
:param trade: trade object.
|
:param trade: Trade object.
|
||||||
:param order: Order dictionary as returned from CCXT.
|
:param order: Order object.
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return bool: When True is returned, then the entry order is cancelled.
|
:return bool: When True is returned, then the entry order is cancelled.
|
||||||
@ -234,30 +237,30 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
return self.check_buy_timeout(
|
return self.check_buy_timeout(
|
||||||
pair=pair, trade=trade, order=order, current_time=current_time)
|
pair=pair, trade=trade, order=order, current_time=current_time)
|
||||||
|
|
||||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_sell_timeout(self, pair: str, trade: Trade, order: Order,
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
DEPRECATED: Please use `check_exit_timeout` instead.
|
DEPRECATED: Please use `check_exit_timeout` instead.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
|
def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
|
||||||
current_time: datetime, **kwargs) -> bool:
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Check sell timeout function callback.
|
Check exit timeout function callback.
|
||||||
This method can be used to override the exit-timeout.
|
This method can be used to override the exit-timeout.
|
||||||
It is called whenever a (long) limit sell order or (short) limit buy
|
It is called whenever a limit exit order has been created,
|
||||||
has been created, and is not yet fully filled.
|
and is not yet fully filled.
|
||||||
Configuration options in `unfilledtimeout` will be verified before this,
|
Configuration options in `unfilledtimeout` will be verified before this,
|
||||||
so ensure to set these timeouts high enough.
|
so ensure to set these timeouts high enough.
|
||||||
|
|
||||||
When not implemented by a strategy, this simply returns False.
|
When not implemented by a strategy, this simply returns False.
|
||||||
:param pair: Pair the trade is for
|
:param pair: Pair the trade is for
|
||||||
:param trade: trade object.
|
:param trade: Trade object.
|
||||||
:param order: Order dictionary as returned from CCXT.
|
:param order: Order object
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled.
|
:return bool: When True is returned, then the exit-order is cancelled.
|
||||||
"""
|
"""
|
||||||
return self.check_sell_timeout(
|
return self.check_sell_timeout(
|
||||||
pair=pair, trade=trade, order=order, current_time=current_time)
|
pair=pair, trade=trade, order=order, current_time=current_time)
|
||||||
@ -359,7 +362,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
def custom_exit_price(self, pair: str, trade: Trade,
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
current_time: datetime, proposed_rate: float,
|
current_time: datetime, proposed_rate: float,
|
||||||
current_profit: float, **kwargs) -> float:
|
current_profit: float, exit_tag: Optional[str], **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Custom exit price logic, returning the new exit price.
|
Custom exit price logic, returning the new exit price.
|
||||||
|
|
||||||
@ -372,6 +375,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param exit_tag: Exit reason.
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return float: New exit price value if provided
|
:return float: New exit price value if provided
|
||||||
"""
|
"""
|
||||||
@ -1090,12 +1094,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
dataframe = _create_and_merge_informative_pair(
|
dataframe = _create_and_merge_informative_pair(
|
||||||
self, dataframe, metadata, inf_data, populate_fn)
|
self, dataframe, metadata, inf_data, populate_fn)
|
||||||
|
|
||||||
if self._populate_fun_len == 2:
|
return self.populate_indicators(dataframe, metadata)
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
|
||||||
"the current function headers!", DeprecationWarning)
|
|
||||||
return self.populate_indicators(dataframe) # type: ignore
|
|
||||||
else:
|
|
||||||
return self.populate_indicators(dataframe, metadata)
|
|
||||||
|
|
||||||
def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -1109,12 +1108,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
logger.debug(f"Populating enter signals for pair {metadata.get('pair')}.")
|
logger.debug(f"Populating enter signals for pair {metadata.get('pair')}.")
|
||||||
|
|
||||||
if self._buy_fun_len == 2:
|
df = self.populate_entry_trend(dataframe, metadata)
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
|
||||||
"the current function headers!", DeprecationWarning)
|
|
||||||
df = self.populate_buy_trend(dataframe) # type: ignore
|
|
||||||
else:
|
|
||||||
df = self.populate_entry_trend(dataframe, metadata)
|
|
||||||
if 'enter_long' not in df.columns:
|
if 'enter_long' not in df.columns:
|
||||||
df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns')
|
df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns')
|
||||||
|
|
||||||
@ -1129,14 +1123,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
currently traded pair
|
currently traded pair
|
||||||
:return: DataFrame with exit column
|
:return: DataFrame with exit column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.")
|
logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.")
|
||||||
if self._sell_fun_len == 2:
|
df = self.populate_exit_trend(dataframe, metadata)
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
|
||||||
"the current function headers!", DeprecationWarning)
|
|
||||||
df = self.populate_sell_trend(dataframe) # type: ignore
|
|
||||||
else:
|
|
||||||
df = self.populate_exit_trend(dataframe, metadata)
|
|
||||||
if 'exit_long' not in df.columns:
|
if 'exit_long' not in df.columns:
|
||||||
df = df.rename({'sell': 'exit_long'}, axis='columns')
|
df = df.rename({'sell': 'exit_long'}, axis='columns')
|
||||||
return df
|
return df
|
||||||
|
@ -56,12 +56,18 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
|
|
||||||
# Combine the 2 dataframes
|
# Combine the 2 dataframes
|
||||||
# all indicators on the informative sample MUST be calculated before this point
|
# all indicators on the informative sample MUST be calculated before this point
|
||||||
dataframe = pd.merge(dataframe, informative, left_on='date',
|
if ffill:
|
||||||
right_on=date_merge, how='left')
|
# https://pandas.pydata.org/docs/user_guide/merging.html#timeseries-friendly-merging
|
||||||
|
# merge_ordered - ffill method is 2.5x faster than seperate ffill()
|
||||||
|
dataframe = pd.merge_ordered(dataframe, informative, fill_method="ffill", left_on='date',
|
||||||
|
right_on=date_merge, how='left')
|
||||||
|
else:
|
||||||
|
dataframe = pd.merge(dataframe, informative, left_on='date',
|
||||||
|
right_on=date_merge, how='left')
|
||||||
dataframe = dataframe.drop(date_merge, axis=1)
|
dataframe = dataframe.drop(date_merge, axis=1)
|
||||||
|
|
||||||
if ffill:
|
# if ffill:
|
||||||
dataframe = dataframe.ffill()
|
# dataframe = dataframe.ffill()
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ def custom_entry_price(self, pair: str, current_time: 'datetime', proposed_rate:
|
|||||||
|
|
||||||
def custom_exit_price(self, pair: str, trade: 'Trade',
|
def custom_exit_price(self, pair: str, trade: 'Trade',
|
||||||
current_time: 'datetime', proposed_rate: float,
|
current_time: 'datetime', proposed_rate: float,
|
||||||
current_profit: float, **kwargs) -> float:
|
current_profit: float, exit_tag: Optional[str], **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Custom exit price logic, returning the new exit price.
|
Custom exit price logic, returning the new exit price.
|
||||||
|
|
||||||
@ -45,6 +45,7 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
|
|||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param exit_tag: Exit reason.
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return float: New exit price value if provided
|
:return float: New exit price value if provided
|
||||||
"""
|
"""
|
||||||
@ -170,7 +171,8 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Check entry timeout function callback.
|
Check entry timeout function callback.
|
||||||
This method can be used to override the entry-timeout.
|
This method can be used to override the entry-timeout.
|
||||||
@ -183,14 +185,16 @@ def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs)
|
|||||||
|
|
||||||
When not implemented by a strategy, this simply returns False.
|
When not implemented by a strategy, this simply returns False.
|
||||||
:param pair: Pair the trade is for
|
:param pair: Pair the trade is for
|
||||||
:param trade: trade object.
|
:param trade: Trade object.
|
||||||
:param order: Order dictionary as returned from CCXT.
|
:param order: Order object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return bool: When True is returned, then the buy-order is cancelled.
|
:return bool: When True is returned, then the entry order is cancelled.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
|
def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Check exit timeout function callback.
|
Check exit timeout function callback.
|
||||||
This method can be used to override the exit-timeout.
|
This method can be used to override the exit-timeout.
|
||||||
@ -203,8 +207,9 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
|
|||||||
|
|
||||||
When not implemented by a strategy, this simply returns False.
|
When not implemented by a strategy, this simply returns False.
|
||||||
:param pair: Pair the trade is for
|
:param pair: Pair the trade is for
|
||||||
:param trade: trade object.
|
:param trade: Trade object.
|
||||||
:param order: Order dictionary as returned from CCXT.
|
:param order: Order object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
:return bool: When True is returned, then the exit-order is cancelled.
|
:return bool: When True is returned, then the exit-order is cancelled.
|
||||||
"""
|
"""
|
||||||
|
@ -29,6 +29,7 @@ nav:
|
|||||||
- Data Analysis:
|
- Data Analysis:
|
||||||
- Jupyter Notebooks: data-analysis.md
|
- Jupyter Notebooks: data-analysis.md
|
||||||
- Strategy analysis: strategy_analysis_example.md
|
- Strategy analysis: strategy_analysis_example.md
|
||||||
|
- Backtest analysis: advanced-backtesting.md
|
||||||
- Advanced Topics:
|
- Advanced Topics:
|
||||||
- Advanced Post-installation Tasks: advanced-setup.md
|
- Advanced Post-installation Tasks: advanced-setup.md
|
||||||
- Edge Positioning: edge.md
|
- Edge Positioning: edge.md
|
||||||
|
@ -23,7 +23,7 @@ exclude = '''
|
|||||||
line_length = 100
|
line_length = 100
|
||||||
multi_line_output=0
|
multi_line_output=0
|
||||||
lines_after_imports=2
|
lines_after_imports=2
|
||||||
skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*"]
|
skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
@ -9,7 +9,7 @@ flake8==4.0.1
|
|||||||
flake8-tidy-imports==4.6.0
|
flake8-tidy-imports==4.6.0
|
||||||
mypy==0.942
|
mypy==0.942
|
||||||
pre-commit==2.18.1
|
pre-commit==2.18.1
|
||||||
pytest==7.1.1
|
pytest==7.1.2
|
||||||
pytest-asyncio==0.18.3
|
pytest-asyncio==0.18.3
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-mock==3.7.0
|
pytest-mock==3.7.0
|
||||||
@ -19,13 +19,11 @@ isort==5.10.1
|
|||||||
time-machine==2.6.0
|
time-machine==2.6.0
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==6.4.5
|
nbconvert==6.5.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.0.0
|
types-cachetools==5.0.1
|
||||||
types-filelock==3.2.5
|
types-filelock==3.2.5
|
||||||
types-requests==2.27.16
|
types-requests==2.27.20
|
||||||
types-tabulate==0.8.6
|
types-tabulate==0.8.7
|
||||||
|
types-python-dateutil==2.8.12
|
||||||
# Extensions to datetime library
|
|
||||||
types-python-dateutil==2.8.10
|
|
||||||
|
@ -6,5 +6,4 @@ scipy==1.8.0
|
|||||||
scikit-learn==1.0.2
|
scikit-learn==1.0.2
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.6.0
|
filelock==3.6.0
|
||||||
joblib==1.1.0
|
|
||||||
progressbar2==4.0.0
|
progressbar2==4.0.0
|
||||||
|
@ -2,4 +2,3 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.7.0
|
plotly==5.7.0
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ numpy==1.22.3
|
|||||||
pandas==1.4.2
|
pandas==1.4.2
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.78.62
|
ccxt==1.80.61
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==36.0.2
|
cryptography==36.0.2
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
@ -20,18 +20,21 @@ pycoingecko==2.2.0
|
|||||||
jinja2==3.1.1
|
jinja2==3.1.1
|
||||||
tables==3.7.0
|
tables==3.7.0
|
||||||
blosc==1.10.6
|
blosc==1.10.6
|
||||||
|
joblib==1.1.0
|
||||||
|
|
||||||
# find first, C search in arrays
|
# find first, C search in arrays
|
||||||
py_find_1st==1.1.5
|
py_find_1st==1.1.5
|
||||||
|
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.6
|
python-rapidjson==1.6
|
||||||
|
# Properly format api responses
|
||||||
|
orjson==3.6.8
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.75.1
|
fastapi==0.75.2
|
||||||
uvicorn==0.17.6
|
uvicorn==0.17.6
|
||||||
pyjwt==2.3.0
|
pyjwt==2.3.0
|
||||||
aiofiles==0.8.0
|
aiofiles==0.8.0
|
||||||
|
@ -52,6 +52,11 @@ exclude =
|
|||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
warn_unused_ignores = True
|
||||||
|
exclude = (?x)(
|
||||||
|
^build_helpers\.py$
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
[mypy-tests.*]
|
[mypy-tests.*]
|
||||||
ignore_errors = True
|
ignore_errors = True
|
||||||
|
3
setup.py
3
setup.py
@ -42,7 +42,7 @@ setup(
|
|||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=1.77.29',
|
'ccxt>=1.79.69',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
@ -57,6 +57,7 @@ setup(
|
|||||||
'pycoingecko',
|
'pycoingecko',
|
||||||
'py_find_1st',
|
'py_find_1st',
|
||||||
'python-rapidjson',
|
'python-rapidjson',
|
||||||
|
'orjson',
|
||||||
'sdnotify',
|
'sdnotify',
|
||||||
'colorama',
|
'colorama',
|
||||||
'jinja2',
|
'jinja2',
|
||||||
|
2
setup.sh
2
setup.sh
@ -90,7 +90,7 @@ function updateenv() {
|
|||||||
echo "pip install completed"
|
echo "pip install completed"
|
||||||
echo
|
echo
|
||||||
if [[ $dev =~ ^[Yy]$ ]]; then
|
if [[ $dev =~ ^[Yy]$ ]]; then
|
||||||
${PYTHON} -m pre-commit install
|
${PYTHON} -m pre_commit install
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Failed installing pre-commit"
|
echo "Failed installing pre-commit"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -847,7 +847,7 @@ def test_start_convert_trades(mocker, caplog):
|
|||||||
assert convert_mock.call_count == 1
|
assert convert_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_list_strategies(mocker, caplog, capsys):
|
def test_start_list_strategies(capsys):
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
"list-strategies",
|
"list-strategies",
|
||||||
@ -859,8 +859,8 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
|||||||
# pargs['config'] = None
|
# pargs['config'] = None
|
||||||
start_list_strategies(pargs)
|
start_list_strategies(pargs)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "TestStrategyLegacyV1" in captured.out
|
assert "StrategyTestV2" in captured.out
|
||||||
assert "legacy_strategy_v1.py" not in captured.out
|
assert "strategy_test_v2.py" not in captured.out
|
||||||
assert CURRENT_TEST_STRATEGY in captured.out
|
assert CURRENT_TEST_STRATEGY in captured.out
|
||||||
|
|
||||||
# Test regular output
|
# Test regular output
|
||||||
@ -874,8 +874,8 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
|||||||
# pargs['config'] = None
|
# pargs['config'] = None
|
||||||
start_list_strategies(pargs)
|
start_list_strategies(pargs)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "TestStrategyLegacyV1" in captured.out
|
assert "StrategyTestV2" in captured.out
|
||||||
assert "legacy_strategy_v1.py" in captured.out
|
assert "strategy_test_v2.py" in captured.out
|
||||||
assert CURRENT_TEST_STRATEGY in captured.out
|
assert CURRENT_TEST_STRATEGY in captured.out
|
||||||
|
|
||||||
# Test color output
|
# Test color output
|
||||||
@ -888,10 +888,30 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
|||||||
# pargs['config'] = None
|
# pargs['config'] = None
|
||||||
start_list_strategies(pargs)
|
start_list_strategies(pargs)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "TestStrategyLegacyV1" in captured.out
|
assert "StrategyTestV2" in captured.out
|
||||||
assert "legacy_strategy_v1.py" in captured.out
|
assert "strategy_test_v2.py" in captured.out
|
||||||
assert CURRENT_TEST_STRATEGY in captured.out
|
assert CURRENT_TEST_STRATEGY in captured.out
|
||||||
assert "LOAD FAILED" in captured.out
|
assert "LOAD FAILED" in captured.out
|
||||||
|
# Recursive
|
||||||
|
assert "TestStrategyNoImplements" not in captured.out
|
||||||
|
|
||||||
|
# Test recursive
|
||||||
|
args = [
|
||||||
|
"list-strategies",
|
||||||
|
"--strategy-path",
|
||||||
|
str(Path(__file__).parent.parent / "strategy" / "strats"),
|
||||||
|
'--no-color',
|
||||||
|
'--recursive-strategy-search'
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
# pargs['config'] = None
|
||||||
|
start_list_strategies(pargs)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "StrategyTestV2" in captured.out
|
||||||
|
assert "strategy_test_v2.py" in captured.out
|
||||||
|
assert "StrategyTestV2" in captured.out
|
||||||
|
assert "TestStrategyNoImplements" in captured.out
|
||||||
|
assert str(Path("broken_strats/broken_futures_strategies.py")) in captured.out
|
||||||
|
|
||||||
|
|
||||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||||
@ -1429,7 +1449,7 @@ def test_backtesting_show(mocker, testdatadir, capsys):
|
|||||||
args = [
|
args = [
|
||||||
"backtesting-show",
|
"backtesting-show",
|
||||||
"--export-filename",
|
"--export-filename",
|
||||||
f"{testdatadir / 'backtest-result_new.json'}",
|
f"{testdatadir / 'backtest_results/backtest-result_new.json'}",
|
||||||
"--show-pair-list"
|
"--show-pair-list"
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
|
@ -1632,40 +1632,6 @@ def limit_buy_order(limit_buy_order_open):
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def market_buy_order():
|
|
||||||
return {
|
|
||||||
'id': 'mocked_market_buy',
|
|
||||||
'type': 'market',
|
|
||||||
'side': 'buy',
|
|
||||||
'symbol': 'mocked',
|
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
|
||||||
'price': 0.00004099,
|
|
||||||
'amount': 91.99181073,
|
|
||||||
'filled': 91.99181073,
|
|
||||||
'remaining': 0.0,
|
|
||||||
'status': 'closed'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def market_sell_order():
|
|
||||||
return {
|
|
||||||
'id': 'mocked_limit_sell',
|
|
||||||
'type': 'market',
|
|
||||||
'side': 'sell',
|
|
||||||
'symbol': 'mocked',
|
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
|
||||||
'price': 0.00004173,
|
|
||||||
'amount': 91.99181073,
|
|
||||||
'filled': 91.99181073,
|
|
||||||
'remaining': 0.0,
|
|
||||||
'status': 'closed'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def limit_buy_order_old():
|
def limit_buy_order_old():
|
||||||
return {
|
return {
|
||||||
@ -2672,6 +2638,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.00125625,
|
'total_profit': -0.00125625,
|
||||||
'current_epoch': 1,
|
'current_epoch': 1,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': True,
|
'is_best': True,
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
@ -2688,6 +2655,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': 6.185e-05,
|
'total_profit': 6.185e-05,
|
||||||
'current_epoch': 2,
|
'current_epoch': 2,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': 14.241196856510731,
|
'loss': 14.241196856510731,
|
||||||
@ -2698,6 +2666,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.13639474,
|
'total_profit': -0.13639474,
|
||||||
'current_epoch': 3,
|
'current_epoch': 3,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': 100000,
|
'loss': 100000,
|
||||||
@ -2705,7 +2674,7 @@ def saved_hyperopt_results():
|
|||||||
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
|
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
|
||||||
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
|
'results_metrics': {'total_trades': 0, 'wins': 0, 'draws': 0, 'losses': 0, 'profit_mean': None, 'profit_median': None, 'profit_total': 0, 'profit': 0.0, 'holding_avg': timedelta()}, # noqa: E501
|
||||||
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
|
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
|
||||||
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False
|
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_random': False, 'is_best': False # noqa: E501
|
||||||
}, {
|
}, {
|
||||||
'loss': 0.22195522184191518,
|
'loss': 0.22195522184191518,
|
||||||
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
|
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
|
||||||
@ -2715,6 +2684,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.002480140000000001,
|
'total_profit': -0.002480140000000001,
|
||||||
'current_epoch': 5,
|
'current_epoch': 5,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': True
|
'is_best': True
|
||||||
}, {
|
}, {
|
||||||
'loss': 0.545315889154162,
|
'loss': 0.545315889154162,
|
||||||
@ -2725,6 +2695,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.0041773,
|
'total_profit': -0.0041773,
|
||||||
'current_epoch': 6,
|
'current_epoch': 6,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': 4.713497421432944,
|
'loss': 4.713497421432944,
|
||||||
@ -2737,6 +2708,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.06339929,
|
'total_profit': -0.06339929,
|
||||||
'current_epoch': 7,
|
'current_epoch': 7,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': 20.0, # noqa: E501
|
'loss': 20.0, # noqa: E501
|
||||||
@ -2747,6 +2719,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': 0.0,
|
'total_profit': 0.0,
|
||||||
'current_epoch': 8,
|
'current_epoch': 8,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': 2.4731817780991223,
|
'loss': 2.4731817780991223,
|
||||||
@ -2757,6 +2730,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.044050070000000004, # noqa: E501
|
'total_profit': -0.044050070000000004, # noqa: E501
|
||||||
'current_epoch': 9,
|
'current_epoch': 9,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': -0.2604606005845212, # noqa: E501
|
'loss': -0.2604606005845212, # noqa: E501
|
||||||
@ -2767,6 +2741,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': 0.00021629,
|
'total_profit': 0.00021629,
|
||||||
'current_epoch': 10,
|
'current_epoch': 10,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': True
|
'is_best': True
|
||||||
}, {
|
}, {
|
||||||
'loss': 4.876465945994304, # noqa: E501
|
'loss': 4.876465945994304, # noqa: E501
|
||||||
@ -2778,6 +2753,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': -0.07436117,
|
'total_profit': -0.07436117,
|
||||||
'current_epoch': 11,
|
'current_epoch': 11,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}, {
|
}, {
|
||||||
'loss': 100000,
|
'loss': 100000,
|
||||||
@ -2788,6 +2764,7 @@ def saved_hyperopt_results():
|
|||||||
'total_profit': 0,
|
'total_profit': 0,
|
||||||
'current_epoch': 12,
|
'current_epoch': 12,
|
||||||
'is_initial_point': True,
|
'is_initial_point': True,
|
||||||
|
'is_random': False,
|
||||||
'is_best': False
|
'is_best': False
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -2935,14 +2912,6 @@ def limit_order(limit_buy_order_usdt, limit_sell_order_usdt):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def market_order(market_buy_order_usdt, market_sell_order_usdt):
|
|
||||||
return {
|
|
||||||
'buy': market_buy_order_usdt,
|
|
||||||
'sell': market_sell_order_usdt
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
|
def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
|
||||||
return {
|
return {
|
||||||
|
@ -8,13 +8,13 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime
|
|||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||||
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_csum,
|
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_cagr,
|
||||||
calculate_market_change, calculate_max_drawdown,
|
calculate_csum, calculate_market_change,
|
||||||
calculate_underwater, combine_dataframes_with_mean,
|
calculate_max_drawdown, calculate_underwater,
|
||||||
create_cum_profit, extract_trades_of_period,
|
combine_dataframes_with_mean, create_cum_profit,
|
||||||
get_latest_backtest_filename, get_latest_hyperopt_file,
|
extract_trades_of_period, get_latest_backtest_filename,
|
||||||
load_backtest_data, load_backtest_metadata, load_trades,
|
get_latest_hyperopt_file, load_backtest_data,
|
||||||
load_trades_from_db)
|
load_backtest_metadata, load_trades, load_trades_from_db)
|
||||||
from freqtrade.data.history import load_data, load_pair_history
|
from freqtrade.data.history import load_data, load_pair_history
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
|
||||||
@ -27,18 +27,19 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
|
|||||||
|
|
||||||
with pytest.raises(ValueError,
|
with pytest.raises(ValueError,
|
||||||
match=r"Directory .* does not seem to contain .*"):
|
match=r"Directory .* does not seem to contain .*"):
|
||||||
get_latest_backtest_filename(testdatadir.parent)
|
get_latest_backtest_filename(testdatadir)
|
||||||
|
|
||||||
res = get_latest_backtest_filename(testdatadir)
|
testdir_bt = testdatadir / "backtest_results"
|
||||||
|
res = get_latest_backtest_filename(testdir_bt)
|
||||||
assert res == 'backtest-result_new.json'
|
assert res == 'backtest-result_new.json'
|
||||||
|
|
||||||
res = get_latest_backtest_filename(str(testdatadir))
|
res = get_latest_backtest_filename(str(testdir_bt))
|
||||||
assert res == 'backtest-result_new.json'
|
assert res == 'backtest-result_new.json'
|
||||||
|
|
||||||
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
|
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r"Invalid '.last_result.json' format."):
|
with pytest.raises(ValueError, match=r"Invalid '.last_result.json' format."):
|
||||||
get_latest_backtest_filename(testdatadir)
|
get_latest_backtest_filename(testdir_bt)
|
||||||
|
|
||||||
|
|
||||||
def test_get_latest_hyperopt_file(testdatadir):
|
def test_get_latest_hyperopt_file(testdatadir):
|
||||||
@ -81,7 +82,7 @@ def test_load_backtest_data_old_format(testdatadir, mocker):
|
|||||||
|
|
||||||
def test_load_backtest_data_new_format(testdatadir):
|
def test_load_backtest_data_new_format(testdatadir):
|
||||||
|
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
assert isinstance(bt_data, DataFrame)
|
assert isinstance(bt_data, DataFrame)
|
||||||
assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
|
assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
|
||||||
@ -92,19 +93,19 @@ def test_load_backtest_data_new_format(testdatadir):
|
|||||||
assert bt_data.equals(bt_data2)
|
assert bt_data.equals(bt_data2)
|
||||||
|
|
||||||
# Test loading from folder (must yield same result)
|
# Test loading from folder (must yield same result)
|
||||||
bt_data3 = load_backtest_data(testdatadir)
|
bt_data3 = load_backtest_data(testdatadir / "backtest_results")
|
||||||
assert bt_data.equals(bt_data3)
|
assert bt_data.equals(bt_data3)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r"File .* does not exist\."):
|
with pytest.raises(ValueError, match=r"File .* does not exist\."):
|
||||||
load_backtest_data(str("filename") + "nofile")
|
load_backtest_data(str("filename") + "nofile")
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r"Unknown dataformat."):
|
with pytest.raises(ValueError, match=r"Unknown dataformat."):
|
||||||
load_backtest_data(testdatadir / LAST_BT_RESULT_FN)
|
load_backtest_data(testdatadir / "backtest_results" / LAST_BT_RESULT_FN)
|
||||||
|
|
||||||
|
|
||||||
def test_load_backtest_data_multi(testdatadir):
|
def test_load_backtest_data_multi(testdatadir):
|
||||||
|
|
||||||
filename = testdatadir / "backtest-result_multistrat.json"
|
filename = testdatadir / "backtest_results/backtest-result_multistrat.json"
|
||||||
for strategy in ('StrategyTestV2', 'TestStrategy'):
|
for strategy in ('StrategyTestV2', 'TestStrategy'):
|
||||||
bt_data = load_backtest_data(filename, strategy=strategy)
|
bt_data = load_backtest_data(filename, strategy=strategy)
|
||||||
assert isinstance(bt_data, DataFrame)
|
assert isinstance(bt_data, DataFrame)
|
||||||
@ -182,7 +183,7 @@ def test_extract_trades_of_period(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_analyze_trade_parallelism(testdatadir):
|
def test_analyze_trade_parallelism(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
res = analyze_trade_parallelism(bt_data, "5m")
|
res = analyze_trade_parallelism(bt_data, "5m")
|
||||||
@ -256,7 +257,7 @@ def test_combine_dataframes_with_mean_no_data(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_create_cum_profit(testdatadir):
|
def test_create_cum_profit(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
|
|
||||||
@ -272,7 +273,7 @@ def test_create_cum_profit(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_create_cum_profit1(testdatadir):
|
def test_create_cum_profit1(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
# Move close-time to "off" the candle, to make sure the logic still works
|
# Move close-time to "off" the candle, to make sure the logic still works
|
||||||
bt_data.loc[:, 'close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
|
bt_data.loc[:, 'close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
|
||||||
@ -294,7 +295,7 @@ def test_create_cum_profit1(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_calculate_max_drawdown(testdatadir):
|
def test_calculate_max_drawdown(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
|
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
|
||||||
bt_data, value_col="profit_abs")
|
bt_data, value_col="profit_abs")
|
||||||
@ -318,7 +319,7 @@ def test_calculate_max_drawdown(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_calculate_csum(testdatadir):
|
def test_calculate_csum(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
csum_min, csum_max = calculate_csum(bt_data)
|
csum_min, csum_max = calculate_csum(bt_data)
|
||||||
|
|
||||||
@ -335,6 +336,19 @@ def test_calculate_csum(testdatadir):
|
|||||||
csum_min, csum_max = calculate_csum(DataFrame())
|
csum_min, csum_max = calculate_csum(DataFrame())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('start,end,days, expected', [
|
||||||
|
(64900, 176000, 3 * 365, 0.3945),
|
||||||
|
(64900, 176000, 365, 1.7119),
|
||||||
|
(1000, 1000, 365, 0.0),
|
||||||
|
(1000, 1500, 365, 0.5),
|
||||||
|
(1000, 1500, 100, 3.3927), # sub year
|
||||||
|
(0.01000000, 0.01762792, 120, 4.6087), # sub year BTC values
|
||||||
|
])
|
||||||
|
def test_calculate_cagr(start, end, days, expected):
|
||||||
|
|
||||||
|
assert round(calculate_cagr(days, start, end), 4) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_calculate_max_drawdown2():
|
def test_calculate_max_drawdown2():
|
||||||
values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
|
values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
|
||||||
-0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,
|
-0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,
|
||||||
|
@ -8,7 +8,7 @@ from unittest.mock import MagicMock
|
|||||||
import arrow
|
import arrow
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||||
from freqtrade.edge import Edge, PairInfo
|
from freqtrade.edge import Edge, PairInfo
|
||||||
@ -30,49 +30,6 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
|||||||
tests_start_time = arrow.get(2018, 10, 3)
|
tests_start_time = arrow.get(2018, 10, 3)
|
||||||
timeframe_in_minute = 60
|
timeframe_in_minute = 60
|
||||||
|
|
||||||
# Helpers for this test file
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_ohlc(buy_ohlc_sell_matrice):
|
|
||||||
for index, ohlc in enumerate(buy_ohlc_sell_matrice):
|
|
||||||
# if not high < open < low or not high < close < low
|
|
||||||
if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]:
|
|
||||||
raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _build_dataframe(buy_ohlc_sell_matrice):
|
|
||||||
_validate_ohlc(buy_ohlc_sell_matrice)
|
|
||||||
data = []
|
|
||||||
for ohlc in buy_ohlc_sell_matrice:
|
|
||||||
d = {
|
|
||||||
'date': tests_start_time.shift(
|
|
||||||
minutes=(
|
|
||||||
ohlc[0] *
|
|
||||||
timeframe_in_minute)).int_timestamp *
|
|
||||||
1000,
|
|
||||||
'buy': ohlc[1],
|
|
||||||
'open': ohlc[2],
|
|
||||||
'high': ohlc[3],
|
|
||||||
'low': ohlc[4],
|
|
||||||
'close': ohlc[5],
|
|
||||||
'sell': ohlc[6]}
|
|
||||||
data.append(d)
|
|
||||||
|
|
||||||
frame = DataFrame(data)
|
|
||||||
frame['date'] = to_datetime(frame['date'],
|
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
|
|
||||||
return frame
|
|
||||||
|
|
||||||
|
|
||||||
def _time_on_candle(number):
|
|
||||||
return np.datetime64(tests_start_time.shift(
|
|
||||||
minutes=(number * timeframe_in_minute)).int_timestamp * 1000, 'ms')
|
|
||||||
|
|
||||||
|
|
||||||
# End helper functions
|
# End helper functions
|
||||||
# Open trade should be removed from the end
|
# Open trade should be removed from the end
|
||||||
tc0 = BTContainer(data=[
|
tc0 = BTContainer(data=[
|
||||||
|
@ -169,90 +169,90 @@ def test_fill_leverage_tiers_binance(default_conf, mocker):
|
|||||||
'ADA/BUSD': [
|
'ADA/BUSD': [
|
||||||
{
|
{
|
||||||
"tier": 1,
|
"tier": 1,
|
||||||
"notionalFloor": 0,
|
"minNotional": 0,
|
||||||
"notionalCap": 100000,
|
"maxNotional": 100000,
|
||||||
"maintenanceMarginRate": 0.025,
|
"maintenanceMarginRate": 0.025,
|
||||||
"maxLeverage": 20,
|
"maxLeverage": 20,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "1",
|
"bracket": "1",
|
||||||
"initialLeverage": "20",
|
"initialLeverage": "20",
|
||||||
"notionalCap": "100000",
|
"maxNotional": "100000",
|
||||||
"notionalFloor": "0",
|
"minNotional": "0",
|
||||||
"maintMarginRatio": "0.025",
|
"maintMarginRatio": "0.025",
|
||||||
"cum": "0.0"
|
"cum": "0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 2,
|
"tier": 2,
|
||||||
"notionalFloor": 100000,
|
"minNotional": 100000,
|
||||||
"notionalCap": 500000,
|
"maxNotional": 500000,
|
||||||
"maintenanceMarginRate": 0.05,
|
"maintenanceMarginRate": 0.05,
|
||||||
"maxLeverage": 10,
|
"maxLeverage": 10,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "2",
|
"bracket": "2",
|
||||||
"initialLeverage": "10",
|
"initialLeverage": "10",
|
||||||
"notionalCap": "500000",
|
"maxNotional": "500000",
|
||||||
"notionalFloor": "100000",
|
"minNotional": "100000",
|
||||||
"maintMarginRatio": "0.05",
|
"maintMarginRatio": "0.05",
|
||||||
"cum": "2500.0"
|
"cum": "2500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 3,
|
"tier": 3,
|
||||||
"notionalFloor": 500000,
|
"minNotional": 500000,
|
||||||
"notionalCap": 1000000,
|
"maxNotional": 1000000,
|
||||||
"maintenanceMarginRate": 0.1,
|
"maintenanceMarginRate": 0.1,
|
||||||
"maxLeverage": 5,
|
"maxLeverage": 5,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "3",
|
"bracket": "3",
|
||||||
"initialLeverage": "5",
|
"initialLeverage": "5",
|
||||||
"notionalCap": "1000000",
|
"maxNotional": "1000000",
|
||||||
"notionalFloor": "500000",
|
"minNotional": "500000",
|
||||||
"maintMarginRatio": "0.1",
|
"maintMarginRatio": "0.1",
|
||||||
"cum": "27500.0"
|
"cum": "27500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 4,
|
"tier": 4,
|
||||||
"notionalFloor": 1000000,
|
"minNotional": 1000000,
|
||||||
"notionalCap": 2000000,
|
"maxNotional": 2000000,
|
||||||
"maintenanceMarginRate": 0.15,
|
"maintenanceMarginRate": 0.15,
|
||||||
"maxLeverage": 3,
|
"maxLeverage": 3,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "4",
|
"bracket": "4",
|
||||||
"initialLeverage": "3",
|
"initialLeverage": "3",
|
||||||
"notionalCap": "2000000",
|
"maxNotional": "2000000",
|
||||||
"notionalFloor": "1000000",
|
"minNotional": "1000000",
|
||||||
"maintMarginRatio": "0.15",
|
"maintMarginRatio": "0.15",
|
||||||
"cum": "77500.0"
|
"cum": "77500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 5,
|
"tier": 5,
|
||||||
"notionalFloor": 2000000,
|
"minNotional": 2000000,
|
||||||
"notionalCap": 5000000,
|
"maxNotional": 5000000,
|
||||||
"maintenanceMarginRate": 0.25,
|
"maintenanceMarginRate": 0.25,
|
||||||
"maxLeverage": 2,
|
"maxLeverage": 2,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "5",
|
"bracket": "5",
|
||||||
"initialLeverage": "2",
|
"initialLeverage": "2",
|
||||||
"notionalCap": "5000000",
|
"maxNotional": "5000000",
|
||||||
"notionalFloor": "2000000",
|
"minNotional": "2000000",
|
||||||
"maintMarginRatio": "0.25",
|
"maintMarginRatio": "0.25",
|
||||||
"cum": "277500.0"
|
"cum": "277500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 6,
|
"tier": 6,
|
||||||
"notionalFloor": 5000000,
|
"minNotional": 5000000,
|
||||||
"notionalCap": 30000000,
|
"maxNotional": 30000000,
|
||||||
"maintenanceMarginRate": 0.5,
|
"maintenanceMarginRate": 0.5,
|
||||||
"maxLeverage": 1,
|
"maxLeverage": 1,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "6",
|
"bracket": "6",
|
||||||
"initialLeverage": "1",
|
"initialLeverage": "1",
|
||||||
"notionalCap": "30000000",
|
"maxNotional": "30000000",
|
||||||
"notionalFloor": "5000000",
|
"minNotional": "5000000",
|
||||||
"maintMarginRatio": "0.5",
|
"maintMarginRatio": "0.5",
|
||||||
"cum": "1527500.0"
|
"cum": "1527500.0"
|
||||||
}
|
}
|
||||||
@ -261,105 +261,105 @@ def test_fill_leverage_tiers_binance(default_conf, mocker):
|
|||||||
"ZEC/USDT": [
|
"ZEC/USDT": [
|
||||||
{
|
{
|
||||||
"tier": 1,
|
"tier": 1,
|
||||||
"notionalFloor": 0,
|
"minNotional": 0,
|
||||||
"notionalCap": 50000,
|
"maxNotional": 50000,
|
||||||
"maintenanceMarginRate": 0.01,
|
"maintenanceMarginRate": 0.01,
|
||||||
"maxLeverage": 50,
|
"maxLeverage": 50,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "1",
|
"bracket": "1",
|
||||||
"initialLeverage": "50",
|
"initialLeverage": "50",
|
||||||
"notionalCap": "50000",
|
"maxNotional": "50000",
|
||||||
"notionalFloor": "0",
|
"minNotional": "0",
|
||||||
"maintMarginRatio": "0.01",
|
"maintMarginRatio": "0.01",
|
||||||
"cum": "0.0"
|
"cum": "0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 2,
|
"tier": 2,
|
||||||
"notionalFloor": 50000,
|
"minNotional": 50000,
|
||||||
"notionalCap": 150000,
|
"maxNotional": 150000,
|
||||||
"maintenanceMarginRate": 0.025,
|
"maintenanceMarginRate": 0.025,
|
||||||
"maxLeverage": 20,
|
"maxLeverage": 20,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "2",
|
"bracket": "2",
|
||||||
"initialLeverage": "20",
|
"initialLeverage": "20",
|
||||||
"notionalCap": "150000",
|
"maxNotional": "150000",
|
||||||
"notionalFloor": "50000",
|
"minNotional": "50000",
|
||||||
"maintMarginRatio": "0.025",
|
"maintMarginRatio": "0.025",
|
||||||
"cum": "750.0"
|
"cum": "750.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 3,
|
"tier": 3,
|
||||||
"notionalFloor": 150000,
|
"minNotional": 150000,
|
||||||
"notionalCap": 250000,
|
"maxNotional": 250000,
|
||||||
"maintenanceMarginRate": 0.05,
|
"maintenanceMarginRate": 0.05,
|
||||||
"maxLeverage": 10,
|
"maxLeverage": 10,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "3",
|
"bracket": "3",
|
||||||
"initialLeverage": "10",
|
"initialLeverage": "10",
|
||||||
"notionalCap": "250000",
|
"maxNotional": "250000",
|
||||||
"notionalFloor": "150000",
|
"minNotional": "150000",
|
||||||
"maintMarginRatio": "0.05",
|
"maintMarginRatio": "0.05",
|
||||||
"cum": "4500.0"
|
"cum": "4500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 4,
|
"tier": 4,
|
||||||
"notionalFloor": 250000,
|
"minNotional": 250000,
|
||||||
"notionalCap": 500000,
|
"maxNotional": 500000,
|
||||||
"maintenanceMarginRate": 0.1,
|
"maintenanceMarginRate": 0.1,
|
||||||
"maxLeverage": 5,
|
"maxLeverage": 5,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "4",
|
"bracket": "4",
|
||||||
"initialLeverage": "5",
|
"initialLeverage": "5",
|
||||||
"notionalCap": "500000",
|
"maxNotional": "500000",
|
||||||
"notionalFloor": "250000",
|
"minNotional": "250000",
|
||||||
"maintMarginRatio": "0.1",
|
"maintMarginRatio": "0.1",
|
||||||
"cum": "17000.0"
|
"cum": "17000.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 5,
|
"tier": 5,
|
||||||
"notionalFloor": 500000,
|
"minNotional": 500000,
|
||||||
"notionalCap": 1000000,
|
"maxNotional": 1000000,
|
||||||
"maintenanceMarginRate": 0.125,
|
"maintenanceMarginRate": 0.125,
|
||||||
"maxLeverage": 4,
|
"maxLeverage": 4,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "5",
|
"bracket": "5",
|
||||||
"initialLeverage": "4",
|
"initialLeverage": "4",
|
||||||
"notionalCap": "1000000",
|
"maxNotional": "1000000",
|
||||||
"notionalFloor": "500000",
|
"minNotional": "500000",
|
||||||
"maintMarginRatio": "0.125",
|
"maintMarginRatio": "0.125",
|
||||||
"cum": "29500.0"
|
"cum": "29500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 6,
|
"tier": 6,
|
||||||
"notionalFloor": 1000000,
|
"minNotional": 1000000,
|
||||||
"notionalCap": 2000000,
|
"maxNotional": 2000000,
|
||||||
"maintenanceMarginRate": 0.25,
|
"maintenanceMarginRate": 0.25,
|
||||||
"maxLeverage": 2,
|
"maxLeverage": 2,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "6",
|
"bracket": "6",
|
||||||
"initialLeverage": "2",
|
"initialLeverage": "2",
|
||||||
"notionalCap": "2000000",
|
"maxNotional": "2000000",
|
||||||
"notionalFloor": "1000000",
|
"minNotional": "1000000",
|
||||||
"maintMarginRatio": "0.25",
|
"maintMarginRatio": "0.25",
|
||||||
"cum": "154500.0"
|
"cum": "154500.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tier": 7,
|
"tier": 7,
|
||||||
"notionalFloor": 2000000,
|
"minNotional": 2000000,
|
||||||
"notionalCap": 30000000,
|
"maxNotional": 30000000,
|
||||||
"maintenanceMarginRate": 0.5,
|
"maintenanceMarginRate": 0.5,
|
||||||
"maxLeverage": 1,
|
"maxLeverage": 1,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "7",
|
"bracket": "7",
|
||||||
"initialLeverage": "1",
|
"initialLeverage": "1",
|
||||||
"notionalCap": "30000000",
|
"maxNotional": "30000000",
|
||||||
"notionalFloor": "2000000",
|
"minNotional": "2000000",
|
||||||
"maintMarginRatio": "0.5",
|
"maintMarginRatio": "0.5",
|
||||||
"cum": "654500.0"
|
"cum": "654500.0"
|
||||||
}
|
}
|
||||||
|
@ -369,25 +369,25 @@ class TestCCXTExchange():
|
|||||||
pair_tiers = leverage_tiers[futures_pair]
|
pair_tiers = leverage_tiers[futures_pair]
|
||||||
assert len(pair_tiers) > 0
|
assert len(pair_tiers) > 0
|
||||||
oldLeverage = float('inf')
|
oldLeverage = float('inf')
|
||||||
oldMaintenanceMarginRate = oldNotionalFloor = oldNotionalCap = -1
|
oldMaintenanceMarginRate = oldminNotional = oldmaxNotional = -1
|
||||||
for tier in pair_tiers:
|
for tier in pair_tiers:
|
||||||
for key in [
|
for key in [
|
||||||
'maintenanceMarginRate',
|
'maintenanceMarginRate',
|
||||||
'notionalFloor',
|
'minNotional',
|
||||||
'notionalCap',
|
'maxNotional',
|
||||||
'maxLeverage'
|
'maxLeverage'
|
||||||
]:
|
]:
|
||||||
assert key in tier
|
assert key in tier
|
||||||
assert tier[key] >= 0.0
|
assert tier[key] >= 0.0
|
||||||
assert tier['notionalCap'] > tier['notionalFloor']
|
assert tier['maxNotional'] > tier['minNotional']
|
||||||
assert tier['maxLeverage'] <= oldLeverage
|
assert tier['maxLeverage'] <= oldLeverage
|
||||||
assert tier['maintenanceMarginRate'] >= oldMaintenanceMarginRate
|
assert tier['maintenanceMarginRate'] >= oldMaintenanceMarginRate
|
||||||
assert tier['notionalFloor'] > oldNotionalFloor
|
assert tier['minNotional'] > oldminNotional
|
||||||
assert tier['notionalCap'] > oldNotionalCap
|
assert tier['maxNotional'] > oldmaxNotional
|
||||||
oldLeverage = tier['maxLeverage']
|
oldLeverage = tier['maxLeverage']
|
||||||
oldMaintenanceMarginRate = tier['maintenanceMarginRate']
|
oldMaintenanceMarginRate = tier['maintenanceMarginRate']
|
||||||
oldNotionalFloor = tier['notionalFloor']
|
oldminNotional = tier['minNotional']
|
||||||
oldNotionalCap = tier['notionalCap']
|
oldmaxNotional = tier['maxNotional']
|
||||||
|
|
||||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
|
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
|
@ -231,6 +231,10 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
|||||||
(2.34559, 2, 3, 1, 2.345, 'spot'),
|
(2.34559, 2, 3, 1, 2.345, 'spot'),
|
||||||
(2.9999, 2, 3, 1, 2.999, 'spot'),
|
(2.9999, 2, 3, 1, 2.999, 'spot'),
|
||||||
(2.9909, 2, 3, 1, 2.990, 'spot'),
|
(2.9909, 2, 3, 1, 2.990, 'spot'),
|
||||||
|
(2.9909, 2, 0, 1, 2, 'spot'),
|
||||||
|
(29991.5555, 2, 0, 1, 29991, 'spot'),
|
||||||
|
(29991.5555, 2, -1, 1, 29990, 'spot'),
|
||||||
|
(29991.5555, 2, -2, 1, 29900, 'spot'),
|
||||||
# Tests for Tick-size
|
# Tests for Tick-size
|
||||||
(2.34559, 4, 0.0001, 1, 2.3455, 'spot'),
|
(2.34559, 4, 0.0001, 1, 2.3455, 'spot'),
|
||||||
(2.34559, 4, 0.00001, 1, 2.34559, 'spot'),
|
(2.34559, 4, 0.00001, 1, 2.34559, 'spot'),
|
||||||
@ -905,7 +909,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'The ccxt library does not provide the list of timeframes '
|
match=r'The ccxt library does not provide the list of timeframes '
|
||||||
r'for the exchange ".*" and this exchange '
|
r'for the exchange .* and this exchange '
|
||||||
r'is therefore not supported. *'):
|
r'is therefore not supported. *'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
@ -926,7 +930,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'The ccxt library does not provide the list of timeframes '
|
match=r'The ccxt library does not provide the list of timeframes '
|
||||||
r'for the exchange ".*" and this exchange '
|
r'for the exchange .* and this exchange '
|
||||||
r'is therefore not supported. *'):
|
r'is therefore not supported. *'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
@ -4503,8 +4507,8 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name
|
|||||||
'ADA/USDT:USDT': [
|
'ADA/USDT:USDT': [
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 500,
|
'maxNotional': 500,
|
||||||
'maintenanceMarginRate': 0.02,
|
'maintenanceMarginRate': 0.02,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
@ -4544,8 +4548,8 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name
|
|||||||
'ADA/USDT:USDT': [
|
'ADA/USDT:USDT': [
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 500,
|
'maxNotional': 500,
|
||||||
'maintenanceMarginRate': 0.02,
|
'maintenanceMarginRate': 0.02,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
@ -4580,15 +4584,15 @@ def test_parse_leverage_tier(mocker, default_conf):
|
|||||||
|
|
||||||
tier = {
|
tier = {
|
||||||
"tier": 1,
|
"tier": 1,
|
||||||
"notionalFloor": 0,
|
"minNotional": 0,
|
||||||
"notionalCap": 100000,
|
"maxNotional": 100000,
|
||||||
"maintenanceMarginRate": 0.025,
|
"maintenanceMarginRate": 0.025,
|
||||||
"maxLeverage": 20,
|
"maxLeverage": 20,
|
||||||
"info": {
|
"info": {
|
||||||
"bracket": "1",
|
"bracket": "1",
|
||||||
"initialLeverage": "20",
|
"initialLeverage": "20",
|
||||||
"notionalCap": "100000",
|
"maxNotional": "100000",
|
||||||
"notionalFloor": "0",
|
"minNotional": "0",
|
||||||
"maintMarginRatio": "0.025",
|
"maintMarginRatio": "0.025",
|
||||||
"cum": "0.0"
|
"cum": "0.0"
|
||||||
}
|
}
|
||||||
@ -4604,8 +4608,8 @@ def test_parse_leverage_tier(mocker, default_conf):
|
|||||||
|
|
||||||
tier2 = {
|
tier2 = {
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 2000,
|
'maxNotional': 2000,
|
||||||
'maintenanceMarginRate': 0.01,
|
'maintenanceMarginRate': 0.01,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
|
@ -19,8 +19,8 @@ def test_get_maintenance_ratio_and_amt_okx(
|
|||||||
'ETH/USDT:USDT': [
|
'ETH/USDT:USDT': [
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 2000,
|
'maxNotional': 2000,
|
||||||
'maintenanceMarginRate': 0.01,
|
'maintenanceMarginRate': 0.01,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
@ -39,8 +39,8 @@ def test_get_maintenance_ratio_and_amt_okx(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 2,
|
'tier': 2,
|
||||||
'notionalFloor': 2001,
|
'minNotional': 2001,
|
||||||
'notionalCap': 4000,
|
'maxNotional': 4000,
|
||||||
'maintenanceMarginRate': 0.015,
|
'maintenanceMarginRate': 0.015,
|
||||||
'maxLeverage': 50,
|
'maxLeverage': 50,
|
||||||
'info': {
|
'info': {
|
||||||
@ -59,8 +59,8 @@ def test_get_maintenance_ratio_and_amt_okx(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 3,
|
'tier': 3,
|
||||||
'notionalFloor': 4001,
|
'minNotional': 4001,
|
||||||
'notionalCap': 8000,
|
'maxNotional': 8000,
|
||||||
'maintenanceMarginRate': 0.02,
|
'maintenanceMarginRate': 0.02,
|
||||||
'maxLeverage': 20,
|
'maxLeverage': 20,
|
||||||
'info': {
|
'info': {
|
||||||
@ -81,8 +81,8 @@ def test_get_maintenance_ratio_and_amt_okx(
|
|||||||
'ADA/USDT:USDT': [
|
'ADA/USDT:USDT': [
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 500,
|
'maxNotional': 500,
|
||||||
'maintenanceMarginRate': 0.02,
|
'maintenanceMarginRate': 0.02,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
@ -101,8 +101,8 @@ def test_get_maintenance_ratio_and_amt_okx(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 2,
|
'tier': 2,
|
||||||
'notionalFloor': 501,
|
'minNotional': 501,
|
||||||
'notionalCap': 1000,
|
'maxNotional': 1000,
|
||||||
'maintenanceMarginRate': 0.025,
|
'maintenanceMarginRate': 0.025,
|
||||||
'maxLeverage': 50,
|
'maxLeverage': 50,
|
||||||
'info': {
|
'info': {
|
||||||
@ -121,8 +121,8 @@ def test_get_maintenance_ratio_and_amt_okx(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 3,
|
'tier': 3,
|
||||||
'notionalFloor': 1001,
|
'minNotional': 1001,
|
||||||
'notionalCap': 2000,
|
'maxNotional': 2000,
|
||||||
'maintenanceMarginRate': 0.03,
|
'maintenanceMarginRate': 0.03,
|
||||||
'maxLeverage': 20,
|
'maxLeverage': 20,
|
||||||
'info': {
|
'info': {
|
||||||
@ -180,8 +180,8 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 500,
|
'maxNotional': 500,
|
||||||
'maintenanceMarginRate': 0.02,
|
'maintenanceMarginRate': 0.02,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
@ -200,8 +200,8 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 2,
|
'tier': 2,
|
||||||
'notionalFloor': 501,
|
'minNotional': 501,
|
||||||
'notionalCap': 1000,
|
'maxNotional': 1000,
|
||||||
'maintenanceMarginRate': 0.025,
|
'maintenanceMarginRate': 0.025,
|
||||||
'maxLeverage': 50,
|
'maxLeverage': 50,
|
||||||
'info': {
|
'info': {
|
||||||
@ -220,8 +220,8 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 3,
|
'tier': 3,
|
||||||
'notionalFloor': 1001,
|
'minNotional': 1001,
|
||||||
'notionalCap': 2000,
|
'maxNotional': 2000,
|
||||||
'maintenanceMarginRate': 0.03,
|
'maintenanceMarginRate': 0.03,
|
||||||
'maxLeverage': 20,
|
'maxLeverage': 20,
|
||||||
'info': {
|
'info': {
|
||||||
@ -242,8 +242,8 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
'notionalFloor': 0,
|
'minNotional': 0,
|
||||||
'notionalCap': 2000,
|
'maxNotional': 2000,
|
||||||
'maintenanceMarginRate': 0.01,
|
'maintenanceMarginRate': 0.01,
|
||||||
'maxLeverage': 75,
|
'maxLeverage': 75,
|
||||||
'info': {
|
'info': {
|
||||||
@ -262,8 +262,8 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 2,
|
'tier': 2,
|
||||||
'notionalFloor': 2001,
|
'minNotional': 2001,
|
||||||
'notionalCap': 4000,
|
'maxNotional': 4000,
|
||||||
'maintenanceMarginRate': 0.015,
|
'maintenanceMarginRate': 0.015,
|
||||||
'maxLeverage': 50,
|
'maxLeverage': 50,
|
||||||
'info': {
|
'info': {
|
||||||
@ -282,8 +282,8 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'tier': 3,
|
'tier': 3,
|
||||||
'notionalFloor': 4001,
|
'minNotional': 4001,
|
||||||
'notionalCap': 8000,
|
'maxNotional': 8000,
|
||||||
'maintenanceMarginRate': 0.02,
|
'maintenanceMarginRate': 0.02,
|
||||||
'maxLeverage': 20,
|
'maxLeverage': 20,
|
||||||
'info': {
|
'info': {
|
||||||
|
@ -22,7 +22,7 @@ from freqtrade.data.history import get_timerange
|
|||||||
from freqtrade.enums import ExitType, RunMode
|
from freqtrade.enums import 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.exchange import timeframe_to_next_date
|
||||||
from freqtrade.misc import get_strategy_run_id
|
from freqtrade.optimize.backtest_caching import get_strategy_run_id
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.persistence import LocalTrade
|
from freqtrade.persistence import LocalTrade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
@ -312,6 +312,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
|
|||||||
get_fee.assert_called()
|
get_fee.assert_called()
|
||||||
assert backtesting.fee == 0.5
|
assert backtesting.fee == 0.5
|
||||||
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
||||||
|
assert backtesting.strategy.bot_started is True
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None:
|
||||||
@ -384,14 +385,16 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
|
||||||
sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats')
|
sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats')
|
||||||
|
sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_signal_candles')
|
||||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
PropertyMock(return_value=['UNITTEST/BTC']))
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
|
|
||||||
default_conf['timeframe'] = '1m'
|
default_conf['timeframe'] = '1m'
|
||||||
default_conf['datadir'] = testdatadir
|
default_conf['datadir'] = testdatadir
|
||||||
default_conf['export'] = 'trades'
|
default_conf['export'] = 'signals'
|
||||||
default_conf['exportfilename'] = 'export.txt'
|
default_conf['exportfilename'] = 'export.txt'
|
||||||
default_conf['timerange'] = '-1510694220'
|
default_conf['timerange'] = '-1510694220'
|
||||||
|
default_conf['runmode'] = RunMode.BACKTEST
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
@ -407,6 +410,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
|||||||
assert backtesting.strategy.dp._pairlists is not None
|
assert backtesting.strategy.dp._pairlists is not None
|
||||||
assert backtesting.strategy.bot_loop_start.call_count == 1
|
assert backtesting.strategy.bot_loop_start.call_count == 1
|
||||||
assert sbs.call_count == 1
|
assert sbs.call_count == 1
|
||||||
|
assert sbc.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
|
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
|
||||||
@ -497,7 +501,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
|||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
|
|
||||||
# Multiple strategies
|
# Multiple strategies
|
||||||
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1']
|
default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'StrategyTestV2']
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match='PrecisionFilter not allowed for backtesting multiple strategies.'):
|
match='PrecisionFilter not allowed for backtesting multiple strategies.'):
|
||||||
Backtesting(default_conf)
|
Backtesting(default_conf)
|
||||||
@ -711,7 +715,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# No data available.
|
# No data available.
|
||||||
res = backtesting._get_sell_trade_entry(trade, row_sell)
|
res = backtesting._get_exit_trade_entry(trade, row_sell)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
assert res.exit_reason == ExitType.ROI.value
|
||||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
|
||||||
@ -724,13 +728,13 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
|
||||||
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
|
||||||
|
|
||||||
res = backtesting._get_sell_trade_entry(trade, row)
|
res = backtesting._get_exit_trade_entry(trade, row)
|
||||||
assert res is None
|
assert res is None
|
||||||
|
|
||||||
# Assign backtest-detail data
|
# Assign backtest-detail data
|
||||||
backtesting.detail_data[pair] = row_detail
|
backtesting.detail_data[pair] = row_detail
|
||||||
|
|
||||||
res = backtesting._get_sell_trade_entry(trade, row_sell)
|
res = backtesting._get_exit_trade_entry(trade, row_sell)
|
||||||
assert res is not None
|
assert res is not None
|
||||||
assert res.exit_reason == ExitType.ROI.value
|
assert res.exit_reason == ExitType.ROI.value
|
||||||
# Sell at minute 3 (not available above!)
|
# Sell at minute 3 (not available above!)
|
||||||
@ -1195,7 +1199,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
'--disable-max-market-positions',
|
'--disable-max-market-positions',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
CURRENT_TEST_STRATEGY,
|
CURRENT_TEST_STRATEGY,
|
||||||
'TestStrategyLegacyV1',
|
'StrategyTestV2',
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
@ -1218,14 +1222,13 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
'up to 2017-11-14 22:58:00 (0 days).',
|
'up to 2017-11-14 22:58:00 (0 days).',
|
||||||
'Parameter --enable-position-stacking detected ...',
|
'Parameter --enable-position-stacking detected ...',
|
||||||
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
||||||
'Running backtesting for Strategy TestStrategyLegacyV1',
|
'Running backtesting for Strategy StrategyTestV2',
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert log_has(line, caplog)
|
assert log_has(line, caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
||||||
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
|
def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdatadir, capsys):
|
||||||
default_conf.update({
|
default_conf.update({
|
||||||
"use_exit_signal": True,
|
"use_exit_signal": True,
|
||||||
@ -1307,7 +1310,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'--breakdown', 'day',
|
'--breakdown', 'day',
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
CURRENT_TEST_STRATEGY,
|
CURRENT_TEST_STRATEGY,
|
||||||
'TestStrategyLegacyV1',
|
'StrategyTestV2',
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
@ -1324,7 +1327,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'up to 2017-11-14 22:58:00 (0 days).',
|
'up to 2017-11-14 22:58:00 (0 days).',
|
||||||
'Parameter --enable-position-stacking detected ...',
|
'Parameter --enable-position-stacking detected ...',
|
||||||
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
|
||||||
'Running backtesting for Strategy TestStrategyLegacyV1',
|
'Running backtesting for Strategy StrategyTestV2',
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
@ -1339,6 +1342,39 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
assert 'STRATEGY SUMMARY' in captured.out
|
assert 'STRATEGY SUMMARY' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
|
def test_backtest_start_futures_noliq(default_conf_usdt, mocker,
|
||||||
|
caplog, testdatadir, capsys):
|
||||||
|
# Tests detail-data loading
|
||||||
|
default_conf_usdt.update({
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated",
|
||||||
|
"use_exit_signal": True,
|
||||||
|
"exit_profit_only": False,
|
||||||
|
"exit_profit_offset": 0.0,
|
||||||
|
"ignore_roi_if_entry_signal": False,
|
||||||
|
"strategy": CURRENT_TEST_STRATEGY,
|
||||||
|
})
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
|
||||||
|
# mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
|
|
||||||
|
patched_configuration_load_config_file(mocker, default_conf_usdt)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'backtesting',
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--datadir', str(testdatadir),
|
||||||
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
||||||
|
'--timeframe', '1h',
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
with pytest.raises(OperationalException, match=r"Pairs .* got no leverage tiers available\."):
|
||||||
|
start_backtesting(args)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
|
||||||
caplog, testdatadir, capsys):
|
caplog, testdatadir, capsys):
|
||||||
@ -1589,7 +1625,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
|||||||
min_backtest_date = now - timedelta(weeks=4)
|
min_backtest_date = now - timedelta(weeks=4)
|
||||||
load_backtest_metadata = MagicMock(return_value={
|
load_backtest_metadata = MagicMock(return_value={
|
||||||
'StrategyTestV2': {'run_id': '1', 'backtest_start_time': now.timestamp()},
|
'StrategyTestV2': {'run_id': '1', 'backtest_start_time': now.timestamp()},
|
||||||
'TestStrategyLegacyV1': {'run_id': run_id, 'backtest_start_time': start_time.timestamp()}
|
'StrategyTestV3': {'run_id': run_id, 'backtest_start_time': start_time.timestamp()}
|
||||||
})
|
})
|
||||||
load_backtest_stats = MagicMock(side_effect=[
|
load_backtest_stats = MagicMock(side_effect=[
|
||||||
{
|
{
|
||||||
@ -1598,9 +1634,9 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
|||||||
'strategy_comparison': [{'key': 'StrategyTestV2'}]
|
'strategy_comparison': [{'key': 'StrategyTestV2'}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'metadata': {'TestStrategyLegacyV1': {'run_id': '2'}},
|
'metadata': {'StrategyTestV3': {'run_id': '2'}},
|
||||||
'strategy': {'TestStrategyLegacyV1': {}},
|
'strategy': {'StrategyTestV3': {}},
|
||||||
'strategy_comparison': [{'key': 'TestStrategyLegacyV1'}]
|
'strategy_comparison': [{'key': 'StrategyTestV3'}]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
mocker.patch('pathlib.Path.glob', return_value=[
|
mocker.patch('pathlib.Path.glob', return_value=[
|
||||||
@ -1624,7 +1660,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
|||||||
'--cache', cache,
|
'--cache', cache,
|
||||||
'--strategy-list',
|
'--strategy-list',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'TestStrategyLegacyV1',
|
'StrategyTestV3',
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
@ -1646,7 +1682,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
|||||||
assert backtestmock.call_count == 2
|
assert backtestmock.call_count == 2
|
||||||
exists = [
|
exists = [
|
||||||
'Running backtesting for Strategy StrategyTestV2',
|
'Running backtesting for Strategy StrategyTestV2',
|
||||||
'Running backtesting for Strategy TestStrategyLegacyV1',
|
'Running backtesting for Strategy StrategyTestV3',
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).',
|
'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).',
|
||||||
]
|
]
|
||||||
@ -1654,12 +1690,12 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
|||||||
assert backtestmock.call_count == 0
|
assert backtestmock.call_count == 0
|
||||||
exists = [
|
exists = [
|
||||||
'Reusing result of previous backtest for StrategyTestV2',
|
'Reusing result of previous backtest for StrategyTestV2',
|
||||||
'Reusing result of previous backtest for TestStrategyLegacyV1',
|
'Reusing result of previous backtest for StrategyTestV3',
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
exists = [
|
exists = [
|
||||||
'Reusing result of previous backtest for StrategyTestV2',
|
'Reusing result of previous backtest for StrategyTestV2',
|
||||||
'Running backtesting for Strategy TestStrategyLegacyV1',
|
'Running backtesting for Strategy StrategyTestV3',
|
||||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).',
|
'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).',
|
||||||
]
|
]
|
||||||
|
@ -94,6 +94,7 @@ def test_edge_init(mocker, edge_conf) -> None:
|
|||||||
assert edge_cli.config == edge_conf
|
assert edge_cli.config == edge_conf
|
||||||
assert edge_cli.config['stake_amount'] == 'unlimited'
|
assert edge_cli.config['stake_amount'] == 'unlimited'
|
||||||
assert callable(edge_cli.edge.calculate)
|
assert callable(edge_cli.edge.calculate)
|
||||||
|
assert edge_cli.strategy.bot_started is True
|
||||||
|
|
||||||
|
|
||||||
def test_edge_init_fee(mocker, edge_conf) -> None:
|
def test_edge_init_fee(mocker, edge_conf) -> None:
|
||||||
|
@ -41,6 +41,7 @@ def generate_result_metrics():
|
|||||||
'max_drawdown_abs': 0.001,
|
'max_drawdown_abs': 0.001,
|
||||||
'loss': 0.001,
|
'loss': 0.001,
|
||||||
'is_initial_point': 0.001,
|
'is_initial_point': 0.001,
|
||||||
|
'is_random': False,
|
||||||
'is_best': 1,
|
'is_best': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +248,7 @@ def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
|||||||
'total_profit': 0,
|
'total_profit': 0,
|
||||||
'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
|
'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
|
||||||
'is_initial_point': False,
|
'is_initial_point': False,
|
||||||
|
'is_random': False,
|
||||||
'is_best': True
|
'is_best': True
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.optimize.hyperopt_loss_short_trade_dur import ShortTradeDurHyperOptLoss
|
from freqtrade.optimize.hyperopt_loss.hyperopt_loss_short_trade_dur import ShortTradeDurHyperOptLoss
|
||||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import re
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import joblib
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
@ -19,6 +20,7 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene
|
|||||||
generate_periodic_breakdown_stats,
|
generate_periodic_breakdown_stats,
|
||||||
generate_strategy_comparison,
|
generate_strategy_comparison,
|
||||||
generate_trading_stats, show_sorted_pairlist,
|
generate_trading_stats, show_sorted_pairlist,
|
||||||
|
store_backtest_signal_candles,
|
||||||
store_backtest_stats, text_table_bt_results,
|
store_backtest_stats, text_table_bt_results,
|
||||||
text_table_exit_reason, text_table_strategy)
|
text_table_exit_reason, text_table_strategy)
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
@ -201,6 +203,62 @@ def test_store_backtest_stats(testdatadir, mocker):
|
|||||||
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult'))
|
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_backtest_candles(testdatadir, mocker):
|
||||||
|
|
||||||
|
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_joblib')
|
||||||
|
|
||||||
|
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
||||||
|
|
||||||
|
# mock directory exporting
|
||||||
|
store_backtest_signal_candles(testdatadir, candle_dict)
|
||||||
|
|
||||||
|
assert dump_mock.call_count == 1
|
||||||
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
|
assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl'))
|
||||||
|
|
||||||
|
dump_mock.reset_mock()
|
||||||
|
# mock file exporting
|
||||||
|
filename = Path(testdatadir / 'testresult')
|
||||||
|
store_backtest_signal_candles(filename, candle_dict)
|
||||||
|
assert dump_mock.call_count == 1
|
||||||
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
|
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
||||||
|
assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl'))
|
||||||
|
dump_mock.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_read_backtest_candles(tmpdir):
|
||||||
|
|
||||||
|
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
||||||
|
|
||||||
|
# test directory exporting
|
||||||
|
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict)
|
||||||
|
scp = open(stored_file, "rb")
|
||||||
|
pickled_signal_candles = joblib.load(scp)
|
||||||
|
scp.close()
|
||||||
|
|
||||||
|
assert pickled_signal_candles.keys() == candle_dict.keys()
|
||||||
|
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
||||||
|
assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \
|
||||||
|
.equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC'])
|
||||||
|
|
||||||
|
_clean_test_file(stored_file)
|
||||||
|
|
||||||
|
# test file exporting
|
||||||
|
filename = Path(tmpdir / 'testresult')
|
||||||
|
stored_file = store_backtest_signal_candles(filename, candle_dict)
|
||||||
|
scp = open(stored_file, "rb")
|
||||||
|
pickled_signal_candles = joblib.load(scp)
|
||||||
|
scp.close()
|
||||||
|
|
||||||
|
assert pickled_signal_candles.keys() == candle_dict.keys()
|
||||||
|
assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys()
|
||||||
|
assert pickled_signal_candles['DefStrat']['UNITTEST/BTC'] \
|
||||||
|
.equals(pickled_signal_candles['DefStrat']['UNITTEST/BTC'])
|
||||||
|
|
||||||
|
_clean_test_file(stored_file)
|
||||||
|
|
||||||
|
|
||||||
def test_generate_pair_metrics():
|
def test_generate_pair_metrics():
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
@ -228,7 +286,7 @@ def test_generate_pair_metrics():
|
|||||||
|
|
||||||
def test_generate_daily_stats(testdatadir):
|
def test_generate_daily_stats(testdatadir):
|
||||||
|
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
res = generate_daily_stats(bt_data)
|
res = generate_daily_stats(bt_data)
|
||||||
assert isinstance(res, dict)
|
assert isinstance(res, dict)
|
||||||
@ -248,7 +306,7 @@ def test_generate_daily_stats(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_trading_stats(testdatadir):
|
def test_generate_trading_stats(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
res = generate_trading_stats(bt_data)
|
res = generate_trading_stats(bt_data)
|
||||||
assert isinstance(res, dict)
|
assert isinstance(res, dict)
|
||||||
@ -332,7 +390,7 @@ def test_generate_sell_reason_stats():
|
|||||||
|
|
||||||
|
|
||||||
def test_text_table_strategy(testdatadir):
|
def test_text_table_strategy(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_multistrat.json"
|
filename = testdatadir / "backtest_results/backtest-result_multistrat.json"
|
||||||
bt_res_data = load_backtest_stats(filename)
|
bt_res_data = load_backtest_stats(filename)
|
||||||
|
|
||||||
bt_res_data_comparison = bt_res_data.pop('strategy_comparison')
|
bt_res_data_comparison = bt_res_data.pop('strategy_comparison')
|
||||||
@ -364,7 +422,7 @@ def test_generate_edge_table():
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_periodic_breakdown_stats(testdatadir):
|
def test_generate_periodic_breakdown_stats(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename).to_dict(orient='records')
|
bt_data = load_backtest_data(filename).to_dict(orient='records')
|
||||||
|
|
||||||
res = generate_periodic_breakdown_stats(bt_data, 'day')
|
res = generate_periodic_breakdown_stats(bt_data, 'day')
|
||||||
@ -392,7 +450,7 @@ def test__get_resample_from_period():
|
|||||||
|
|
||||||
|
|
||||||
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
|
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_stats(filename)
|
bt_data = load_backtest_stats(filename)
|
||||||
default_conf['backtest_show_pair_list'] = True
|
default_conf['backtest_show_pair_list'] = True
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ import uvicorn
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.exceptions import HTTPException
|
from fastapi.exceptions import HTTPException
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from numpy import isnan
|
|
||||||
from requests.auth import _basic_auth_str
|
from requests.auth import _basic_auth_str
|
||||||
|
|
||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
@ -985,7 +984,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
resp_values = rc.json()
|
resp_values = rc.json()
|
||||||
assert len(resp_values) == 4
|
assert len(resp_values) == 4
|
||||||
assert isnan(resp_values[0]['profit_abs'])
|
assert resp_values[0]['profit_abs'] is None
|
||||||
|
|
||||||
|
|
||||||
def test_api_version(botclient):
|
def test_api_version(botclient):
|
||||||
@ -1389,7 +1388,6 @@ def test_api_strategies(botclient):
|
|||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'StrategyTestV3',
|
'StrategyTestV3',
|
||||||
'StrategyTestV3Futures',
|
'StrategyTestV3Futures',
|
||||||
'TestStrategyLegacyV1',
|
|
||||||
]}
|
]}
|
||||||
|
|
||||||
|
|
||||||
@ -1581,6 +1579,38 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
|||||||
assert result['status_msg'] == 'Backtest reset'
|
assert result['status_msg'] == 'Backtest reset'
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_backtest_history(botclient, mocker, testdatadir):
|
||||||
|
ftbot, client = botclient
|
||||||
|
mocker.patch('freqtrade.data.btanalysis._get_backtest_files',
|
||||||
|
return_value=[
|
||||||
|
testdatadir / 'backtest_results/backtest-result_multistrat.json',
|
||||||
|
testdatadir / 'backtest_results/backtest-result_new.json'
|
||||||
|
])
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/backtest/history")
|
||||||
|
assert_response(rc, 502)
|
||||||
|
ftbot.config['user_data_dir'] = testdatadir
|
||||||
|
ftbot.config['runmode'] = RunMode.WEBSERVER
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/backtest/history")
|
||||||
|
assert_response(rc)
|
||||||
|
result = rc.json()
|
||||||
|
assert len(result) == 3
|
||||||
|
fn = result[0]['filename']
|
||||||
|
assert fn == "backtest-result_multistrat.json"
|
||||||
|
strategy = result[0]['strategy']
|
||||||
|
rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}")
|
||||||
|
assert_response(rc)
|
||||||
|
result2 = rc.json()
|
||||||
|
assert result2
|
||||||
|
assert result2['status'] == 'ended'
|
||||||
|
assert not result2['running']
|
||||||
|
assert result2['progress'] == 1
|
||||||
|
# Only one strategy loaded - even though we use multiresult
|
||||||
|
assert len(result2['backtest_result']['strategy']) == 1
|
||||||
|
assert result2['backtest_result']['strategy'][strategy]
|
||||||
|
|
||||||
|
|
||||||
def test_health(botclient):
|
def test_health(botclient):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
|
30
tests/strategy/strats/broken_strats/legacy_strategy_v1.py
Normal file
30
tests/strategy/strats/broken_strats/legacy_strategy_v1.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# type: ignore
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
# Dummy strategy - no longer loads but raises an exception.
|
||||||
|
class TestStrategyLegacyV1(IStrategy):
|
||||||
|
|
||||||
|
minimal_roi = {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
}
|
||||||
|
stoploss = -0.10
|
||||||
|
|
||||||
|
timeframe = '5m'
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
|
||||||
|
return dataframe
|
@ -1,85 +0,0 @@
|
|||||||
|
|
||||||
# --- Do not remove these libs ---
|
|
||||||
# Add your lib to import here
|
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.strategy import IStrategy
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------
|
|
||||||
|
|
||||||
# This class is a sample. Feel free to customize it.
|
|
||||||
class TestStrategyLegacyV1(IStrategy):
|
|
||||||
"""
|
|
||||||
This is a test strategy using the legacy function headers, which will be
|
|
||||||
removed in a future update.
|
|
||||||
Please do not use this as a template, but refer to user_data/strategy/sample_strategy.py
|
|
||||||
for a uptodate version of this template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Minimal ROI designed for the strategy.
|
|
||||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
|
||||||
minimal_roi = {
|
|
||||||
"40": 0.0,
|
|
||||||
"30": 0.01,
|
|
||||||
"20": 0.02,
|
|
||||||
"0": 0.04
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
|
||||||
# This attribute will be overridden if the config file contains "stoploss"
|
|
||||||
stoploss = -0.10
|
|
||||||
|
|
||||||
timeframe = '5m'
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Adds several different TA indicators to the given DataFrame
|
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
|
||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Momentum Indicator
|
|
||||||
# ------------------------------------
|
|
||||||
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
|
|
||||||
# TEMA - Triple Exponential Moving Average
|
|
||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
|
||||||
:param dataframe: DataFrame
|
|
||||||
:return: DataFrame with buy column
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 30) &
|
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1)) &
|
|
||||||
(dataframe['volume'] > 0)
|
|
||||||
),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
|
||||||
:param dataframe: DataFrame
|
|
||||||
:return: DataFrame with buy column
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 70) &
|
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1)) &
|
|
||||||
(dataframe['volume'] > 0)
|
|
||||||
),
|
|
||||||
'sell'] = 1
|
|
||||||
return dataframe
|
|
@ -56,19 +56,6 @@ class StrategyTestV2(IStrategy):
|
|||||||
# By default this strategy does not use Position Adjustments
|
# By default this strategy does not use Position Adjustments
|
||||||
position_adjustment_enable = False
|
position_adjustment_enable = False
|
||||||
|
|
||||||
def informative_pairs(self):
|
|
||||||
"""
|
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
|
||||||
These pair/interval combinations are non-tradeable, unless they are part
|
|
||||||
of the whitelist as well.
|
|
||||||
For more information, please consult the documentation
|
|
||||||
:return: List of tuples in the format (pair, interval)
|
|
||||||
Sample: return [("ETH/USDT", "5m"),
|
|
||||||
("BTC/USDT", "15m"),
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
@ -82,6 +82,11 @@ class StrategyTestV3(IStrategy):
|
|||||||
# })
|
# })
|
||||||
# return prot
|
# return prot
|
||||||
|
|
||||||
|
bot_started = False
|
||||||
|
|
||||||
|
def bot_start(self):
|
||||||
|
self.bot_started = True
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
@ -686,7 +686,7 @@ def test_is_pair_locked(default_conf):
|
|||||||
|
|
||||||
|
|
||||||
def test_is_informative_pairs_callback(default_conf):
|
def test_is_informative_pairs_callback(default_conf):
|
||||||
default_conf.update({'strategy': 'TestStrategyLegacyV1'})
|
default_conf.update({'strategy': 'StrategyTestV2'})
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
# Should return empty
|
# Should return empty
|
||||||
# Uses fallback to base implementation
|
# Uses fallback to base implementation
|
||||||
|
@ -68,6 +68,21 @@ def test_merge_informative_pair():
|
|||||||
assert result.iloc[7]['date_1h'] == result.iloc[4]['date']
|
assert result.iloc[7]['date_1h'] == result.iloc[4]['date']
|
||||||
assert result.iloc[8]['date_1h'] == result.iloc[4]['date']
|
assert result.iloc[8]['date_1h'] == result.iloc[4]['date']
|
||||||
|
|
||||||
|
informative = generate_test_data('1h', 40)
|
||||||
|
result = merge_informative_pair(data, informative, '15m', '1h', ffill=False)
|
||||||
|
# First 3 rows are empty
|
||||||
|
assert result.iloc[0]['date_1h'] is pd.NaT
|
||||||
|
assert result.iloc[1]['date_1h'] is pd.NaT
|
||||||
|
assert result.iloc[2]['date_1h'] is pd.NaT
|
||||||
|
# Next 4 rows contain the starting date (0:00)
|
||||||
|
assert result.iloc[3]['date_1h'] == result.iloc[0]['date']
|
||||||
|
assert result.iloc[4]['date_1h'] is pd.NaT
|
||||||
|
assert result.iloc[5]['date_1h'] is pd.NaT
|
||||||
|
assert result.iloc[6]['date_1h'] is pd.NaT
|
||||||
|
# Next 4 rows contain the next Hourly date original date row 4
|
||||||
|
assert result.iloc[7]['date_1h'] == result.iloc[4]['date']
|
||||||
|
assert result.iloc[8]['date_1h'] is pd.NaT
|
||||||
|
|
||||||
|
|
||||||
def test_merge_informative_pair_same():
|
def test_merge_informative_pair_same():
|
||||||
data = generate_test_data('15m', 40)
|
data = generate_test_data('15m', 40)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
|
||||||
from base64 import urlsafe_b64encode
|
from base64 import urlsafe_b64encode
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ def test_search_all_strategies_no_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 6
|
assert len(strategies) == 5
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
@ -43,10 +42,10 @@ def test_search_all_strategies_with_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 7
|
assert len(strategies) == 6
|
||||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||||
# and 1 which fails to load
|
# and 1 which fails to load
|
||||||
assert len([x for x in strategies if x['class'] is not None]) == 6
|
assert len([x for x in strategies if x['class'] is not None]) == 5
|
||||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -100,7 +99,7 @@ def test_load_strategy_noname(default_conf):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
@pytest.mark.parametrize('strategy_name', ['StrategyTestV2', 'TestStrategyLegacyV1'])
|
@pytest.mark.parametrize('strategy_name', ['StrategyTestV2'])
|
||||||
def test_strategy_pre_v3(result, default_conf, strategy_name):
|
def test_strategy_pre_v3(result, default_conf, strategy_name):
|
||||||
default_conf.update({'strategy': strategy_name})
|
default_conf.update({'strategy': strategy_name})
|
||||||
|
|
||||||
@ -346,40 +345,6 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
|
|||||||
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
|
||||||
def test_deprecate_populate_indicators(result, default_conf):
|
|
||||||
default_location = Path(__file__).parent / "strats"
|
|
||||||
default_conf.update({'strategy': 'TestStrategyLegacyV1',
|
|
||||||
'strategy_path': default_location})
|
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
|
||||||
with warnings.catch_warnings(record=True) as w:
|
|
||||||
# Cause all warnings to always be triggered.
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
indicators = strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
|
||||||
assert len(w) == 1
|
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
|
||||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
|
||||||
in str(w[-1].message)
|
|
||||||
|
|
||||||
with warnings.catch_warnings(record=True) as w:
|
|
||||||
# Cause all warnings to always be triggered.
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
strategy.advise_entry(indicators, {'pair': 'ETH/BTC'})
|
|
||||||
assert len(w) == 1
|
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
|
||||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
|
||||||
in str(w[-1].message)
|
|
||||||
|
|
||||||
with warnings.catch_warnings(record=True) as w:
|
|
||||||
# Cause all warnings to always be triggered.
|
|
||||||
warnings.simplefilter("always")
|
|
||||||
strategy.advise_exit(indicators, {'pair': 'ETH_BTC'})
|
|
||||||
assert len(w) == 1
|
|
||||||
assert issubclass(w[-1].category, DeprecationWarning)
|
|
||||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
|
||||||
in str(w[-1].message)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
def test_missing_implements(default_conf, caplog):
|
def test_missing_implements(default_conf, caplog):
|
||||||
|
|
||||||
@ -438,33 +403,14 @@ def test_missing_implements(default_conf, caplog):
|
|||||||
StrategyResolver.load_strategy(default_conf)
|
StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
def test_call_deprecated_function(default_conf):
|
||||||
def test_call_deprecated_function(result, default_conf, caplog):
|
default_location = Path(__file__).parent / "strats/broken_strats/"
|
||||||
default_location = Path(__file__).parent / "strats"
|
|
||||||
del default_conf['timeframe']
|
del default_conf['timeframe']
|
||||||
default_conf.update({'strategy': 'TestStrategyLegacyV1',
|
default_conf.update({'strategy': 'TestStrategyLegacyV1',
|
||||||
'strategy_path': default_location})
|
'strategy_path': default_location})
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
with pytest.raises(OperationalException,
|
||||||
metadata = {'pair': 'ETH/BTC'}
|
match=r"Strategy Interface v1 is no longer supported.*"):
|
||||||
|
StrategyResolver.load_strategy(default_conf)
|
||||||
# Make sure we are using a legacy function
|
|
||||||
assert strategy._populate_fun_len == 2
|
|
||||||
assert strategy._buy_fun_len == 2
|
|
||||||
assert strategy._sell_fun_len == 2
|
|
||||||
assert strategy.INTERFACE_VERSION == 1
|
|
||||||
assert strategy.timeframe == '5m'
|
|
||||||
|
|
||||||
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
|
||||||
assert isinstance(indicator_df, DataFrame)
|
|
||||||
assert 'adx' in indicator_df.columns
|
|
||||||
|
|
||||||
enterdf = strategy.advise_entry(result, metadata=metadata)
|
|
||||||
assert isinstance(enterdf, DataFrame)
|
|
||||||
assert 'enter_long' in enterdf.columns
|
|
||||||
|
|
||||||
exitdf = strategy.advise_exit(result, metadata=metadata)
|
|
||||||
assert isinstance(exitdf, DataFrame)
|
|
||||||
assert 'exit_long' in exitdf
|
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_interface_versioning(result, default_conf):
|
def test_strategy_interface_versioning(result, default_conf):
|
||||||
@ -472,10 +418,6 @@ def test_strategy_interface_versioning(result, default_conf):
|
|||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
metadata = {'pair': 'ETH/BTC'}
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
|
|
||||||
# Make sure we are using a legacy function
|
|
||||||
assert strategy._populate_fun_len == 3
|
|
||||||
assert strategy._buy_fun_len == 3
|
|
||||||
assert strategy._sell_fun_len == 3
|
|
||||||
assert strategy.INTERFACE_VERSION == 2
|
assert strategy.INTERFACE_VERSION == 2
|
||||||
|
|
||||||
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
indicator_df = strategy.advise_indicators(result, metadata=metadata)
|
||||||
|
@ -717,12 +717,12 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
|
|||||||
(True, 'spot', 'gateio', None, 0.0, None),
|
(True, 'spot', 'gateio', None, 0.0, None),
|
||||||
(False, 'spot', 'okx', None, 0.0, None),
|
(False, 'spot', 'okx', None, 0.0, None),
|
||||||
(True, 'spot', 'okx', None, 0.0, None),
|
(True, 'spot', 'okx', None, 0.0, None),
|
||||||
(True, 'futures', 'binance', 'isolated', 0.0, 11.89108910891089),
|
(True, 'futures', 'binance', 'isolated', 0.0, 11.88151815181518),
|
||||||
(False, 'futures', 'binance', 'isolated', 0.0, 8.070707070707071),
|
(False, 'futures', 'binance', 'isolated', 0.0, 8.080471380471382),
|
||||||
(True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621),
|
(True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621),
|
||||||
(False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207),
|
(False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207),
|
||||||
(True, 'futures', 'binance', 'isolated', 0.05, 11.796534653465345),
|
(True, 'futures', 'binance', 'isolated', 0.05, 11.7874422442244),
|
||||||
(False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717),
|
(False, 'futures', 'binance', 'isolated', 0.05, 8.17644781144781),
|
||||||
(True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
|
(True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
|
||||||
(False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
|
(False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
|
||||||
(True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621),
|
(True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621),
|
||||||
@ -845,6 +845,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
|||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert trade.open_rate == 10
|
assert trade.open_rate == 10
|
||||||
assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8)
|
assert trade.stake_amount == round(order['price'] * order['filled'] / leverage, 8)
|
||||||
|
assert pytest.approx(trade.liquidation_price) == liq_price
|
||||||
|
|
||||||
# In case of rejected or expired order and partially filled
|
# In case of rejected or expired order and partially filled
|
||||||
order['status'] = 'expired'
|
order['status'] = 'expired'
|
||||||
@ -932,8 +933,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
|||||||
assert trade.open_rate_requested == 10
|
assert trade.open_rate_requested == 10
|
||||||
|
|
||||||
# In case of custom entry price not float type
|
# In case of custom entry price not float type
|
||||||
freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
|
|
||||||
freqtrade.exchange.name = exchange_name
|
|
||||||
order['status'] = 'open'
|
order['status'] = 'open'
|
||||||
order['id'] = '5568'
|
order['id'] = '5568'
|
||||||
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
|
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
|
||||||
@ -946,7 +945,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
|||||||
trade.is_short = is_short
|
trade.is_short = is_short
|
||||||
assert trade
|
assert trade
|
||||||
assert trade.open_rate_requested == 10
|
assert trade.open_rate_requested == 10
|
||||||
assert trade.liquidation_price == liq_price
|
|
||||||
|
|
||||||
# In case of too high stake amount
|
# In case of too high stake amount
|
||||||
|
|
||||||
@ -3221,7 +3219,7 @@ def test_execute_trade_exit_custom_exit_price(
|
|||||||
freqtrade.execute_trade_exit(
|
freqtrade.execute_trade_exit(
|
||||||
trade=trade,
|
trade=trade,
|
||||||
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
|
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
|
||||||
exit_check=ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)
|
exit_check=ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL, exit_reason='foo')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sell price must be different to default bid price
|
# Sell price must be different to default bid price
|
||||||
@ -3249,8 +3247,8 @@ def test_execute_trade_exit_custom_exit_price(
|
|||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'sell_reason': ExitType.EXIT_SIGNAL.value,
|
'sell_reason': 'foo',
|
||||||
'exit_reason': ExitType.EXIT_SIGNAL.value,
|
'exit_reason': 'foo',
|
||||||
'open_date': ANY,
|
'open_date': ANY,
|
||||||
'close_date': ANY,
|
'close_date': ANY,
|
||||||
'close_rate': ANY,
|
'close_rate': ANY,
|
||||||
|
@ -157,7 +157,7 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
assert fig == fig1
|
assert fig == fig1
|
||||||
assert log_has("No trades found.", caplog)
|
assert log_has("No trades found.", caplog)
|
||||||
pair = "ADA/BTC"
|
pair = "ADA/BTC"
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
trades = trades.loc[trades['pair'] == pair]
|
trades = trades.loc[trades['pair'] == pair]
|
||||||
|
|
||||||
@ -298,7 +298,7 @@ def test_generate_plot_file(mocker, caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_add_profit(testdatadir):
|
def test_add_profit(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
|
|
||||||
@ -318,7 +318,7 @@ def test_add_profit(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_profit_graph(testdatadir):
|
def test_generate_profit_graph(testdatadir):
|
||||||
filename = testdatadir / "backtest-result_new.json"
|
filename = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
trades = load_backtest_data(filename)
|
trades = load_backtest_data(filename)
|
||||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||||
pairs = ["TRX/BTC", "XLM/BTC"]
|
pairs = ["TRX/BTC", "XLM/BTC"]
|
||||||
@ -466,7 +466,7 @@ def test_plot_profit(default_conf, mocker, testdatadir):
|
|||||||
match=r"No trades found, cannot generate Profit-plot.*"):
|
match=r"No trades found, cannot generate Profit-plot.*"):
|
||||||
plot_profit(default_conf)
|
plot_profit(default_conf)
|
||||||
|
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json"
|
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result_new.json"
|
||||||
|
|
||||||
plot_profit(default_conf)
|
plot_profit(default_conf)
|
||||||
|
|
||||||
|
10
tests/testdata/backtest_results/backtest-result_multistrat.meta.json
vendored
Normal file
10
tests/testdata/backtest_results/backtest-result_multistrat.meta.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"StrategyTestV2": {
|
||||||
|
"run_id": "430d0271075ef327edbb23088f4db4ebe51a3dbf",
|
||||||
|
"backtest_start_time": 1648904006
|
||||||
|
},
|
||||||
|
"TestStrategy": {
|
||||||
|
"run_id": "110d0271075ef327edbb23085102b4ebe51a3d55",
|
||||||
|
"backtest_start_time": 1648904006
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user