mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-14 04:03:55 +00:00
commit
2db5cc177d
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
|
@ -13,6 +13,10 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 5 * * 4'
|
- cron: '0 5 * * 4'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_linux:
|
build_linux:
|
||||||
|
|
||||||
|
@ -26,7 +30,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -62,12 +66,12 @@ jobs:
|
||||||
- name: Tests
|
- name: Tests
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||||
if: matrix.python-version != '3.9'
|
if: matrix.python-version != '3.9' || matrix.os != 'ubuntu-22.04'
|
||||||
|
|
||||||
- name: Tests incl. ccxt compatibility tests
|
- name: Tests incl. ccxt compatibility tests
|
||||||
run: |
|
run: |
|
||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun
|
||||||
if: matrix.python-version == '3.9'
|
if: matrix.python-version == '3.9' && matrix.os == 'ubuntu-22.04'
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: (runner.os == 'Linux' && matrix.python-version == '3.9')
|
if: (runner.os == 'Linux' && matrix.python-version == '3.9')
|
||||||
|
@ -123,7 +127,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -207,7 +211,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -259,7 +263,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
@ -278,7 +282,7 @@ jobs:
|
||||||
./tests/test_docs.sh
|
./tests/test_docs.sh
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
@ -296,18 +300,6 @@ jobs:
|
||||||
details: Freqtrade doc test failed!
|
details: Freqtrade doc test failed!
|
||||||
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|
||||||
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
|
|
||||||
steps:
|
|
||||||
- name: Cleanup previous runs on this branch
|
|
||||||
uses: rokroskar/workflow-run-cleanup-action@v0.3.3
|
|
||||||
if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
|
|
||||||
# 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, mypy_version_check ]
|
needs: [ build_linux, build_macos, build_windows, docs_check, mypy_version_check ]
|
||||||
|
@ -344,7 +336,7 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,11 @@ repos:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
exclude: build_helpers
|
exclude: build_helpers
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.0.1
|
- types-cachetools==5.2.1
|
||||||
- types-filelock==3.2.6
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.27.29
|
- types-requests==2.28.0
|
||||||
- types-tabulate==0.8.9
|
- types-tabulate==0.8.11
|
||||||
- types-python-dateutil==2.8.17
|
- types-python-dateutil==2.8.18
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.10.4-slim-bullseye as base
|
FROM python:3.10.5-slim-bullseye as base
|
||||||
|
|
||||||
# Setup env
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
|
@ -7,4 +7,5 @@ FROM freqtradeorg/freqtrade:develop
|
||||||
# The below dependency - pyti - serves as an example. Please use whatever you need!
|
# The below dependency - pyti - serves as an example. Please use whatever you need!
|
||||||
RUN pip install --user pyti
|
RUN pip install --user pyti
|
||||||
|
|
||||||
|
# Switch back to user (only if you required root above)
|
||||||
# USER ftuser
|
# USER ftuser
|
||||||
|
|
|
@ -22,50 +22,79 @@ DataFrame of the candles that resulted in buy signals. Depending on how many buy
|
||||||
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
|
makes, this file may get quite large, so periodically check your `user_data/backtest_results`
|
||||||
folder to delete old exports.
|
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
|
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.
|
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
|
If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
|
||||||
`user_data/backtest_results` folder.
|
`user_data/backtest_results` folder.
|
||||||
|
|
||||||
Now run the `buy_reasons.py` script, supplying a few options:
|
To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
|
||||||
|
with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`):
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
python3 scripts/buy_reasons.py -c <config.json> -s <strategy_name> -t <timerange> -g0,1,2,3,4
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 1 2 3 4
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-g` option is used to specify the various tabular outputs, ranging from the simplest (0)
|
This command will read from the last backtesting results. The `--analysis-groups` option is
|
||||||
to the most detailed per pair, per buy and per sell tag (4). More options are available by
|
used to specify the various tabular outputs showing the profit fo each group or trade,
|
||||||
running with the `-h` option.
|
ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4):
|
||||||
|
|
||||||
|
* 1: profit summaries grouped by enter_tag
|
||||||
|
* 2: profit summaries grouped by enter_tag and exit_tag
|
||||||
|
* 3: profit summaries grouped by pair and enter_tag
|
||||||
|
* 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
|
|
||||||
|
More options are available by running with the `-h` option.
|
||||||
|
|
||||||
|
### Using export-filename
|
||||||
|
|
||||||
|
Normally, `backtesting-analysis` uses the latest backtest results, but if you wanted to go
|
||||||
|
back to a previous backtest output, you need to supply the `--export-filename` option.
|
||||||
|
You can supply the same parameter to `backtest-analysis` with the name of the final backtest
|
||||||
|
output file. This allows you to keep historical versions of backtest results and re-analyse
|
||||||
|
them at a later date:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_name> --timerange=<timerange> --export=signals --export-filename=/tmp/mystrat_backtest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see some output similar to below in the logs with the name of the timestamped
|
||||||
|
filename that was exported:
|
||||||
|
|
||||||
|
```
|
||||||
|
2022-06-14 16:28:32,698 - freqtrade.misc - INFO - dumping json to "/tmp/mystrat_backtest-2022-06-14_16-28-32.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then use that filename in `backtesting-analysis`:
|
||||||
|
|
||||||
|
```
|
||||||
|
freqtrade backtesting-analysis -c <config.json> --export-filename=/tmp/mystrat_backtest-2022-06-14_16-28-32.json
|
||||||
|
```
|
||||||
|
|
||||||
### Tuning the buy tags and sell tags to display
|
### 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:
|
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"
|
--enter-reason-list : Space-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"
|
--exit-reason-list : Space-separated list of exit signals to analyse. Default: "all"
|
||||||
```
|
```
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```bash
|
```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"
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --enter-reason-list enter_tag_a enter_tag_b --exit-reason-list roi custom_exit_tag_a stop_loss
|
||||||
```
|
```
|
||||||
|
|
||||||
### Outputting signal candle indicators
|
### Outputting signal candle indicators
|
||||||
|
|
||||||
The real power of the buy_reasons.py script comes from the ability to print out the indicator
|
The real power of `freqtrade backtesting-analysis` comes from the ability to print out the indicator
|
||||||
values present on signal candles to allow fine-grained investigation and tuning of buy signal
|
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`
|
indicators. To print out a column for a given set of indicators, use the `--indicator-list`
|
||||||
option:
|
option:
|
||||||
|
|
||||||
```bash
|
```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"
|
freqtrade backtesting-analysis -c <config.json> --analysis-groups 0 2 --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
|
The indicators have to be present in your strategy's main DataFrame (either for your main
|
||||||
|
|
BIN
docs/assets/discord_notification.png
Normal file
BIN
docs/assets/discord_notification.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -300,6 +300,7 @@ A backtesting result will look like that:
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Profit factor | 1.11 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
|
@ -399,6 +400,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| CAGR % | 460.87% |
|
| CAGR % | 460.87% |
|
||||||
|
| Profit factor | 1.11 |
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 BTC |
|
| Total trade volume | 0.429 BTC |
|
||||||
| | |
|
| | |
|
||||||
|
@ -444,6 +446,8 @@ It contains some useful key metrics about performance of your strategy on backte
|
||||||
- `Final balance`: Final balance - starting balance + absolute profit.
|
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||||
- `Absolute profit`: Profit made in stake currency.
|
- `Absolute profit`: Profit made in stake currency.
|
||||||
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
|
||||||
|
- `CAGR %`: Compound annual growth rate.
|
||||||
|
- `Profit factor`: profit / loss.
|
||||||
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
|
||||||
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
|
||||||
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
|
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
|
||||||
|
|
|
@ -20,7 +20,9 @@ All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt /
|
||||||
## Bot execution logic
|
## Bot execution logic
|
||||||
|
|
||||||
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
||||||
By default, loop runs every few seconds (`internals.process_throttle_secs`) and does roughly the following in the following sequence:
|
This will also run the `bot_start()` callback.
|
||||||
|
|
||||||
|
By default, the bot loop runs every few seconds (`internals.process_throttle_secs`) and performs the following actions:
|
||||||
|
|
||||||
* Fetch open trades from persistence.
|
* Fetch open trades from persistence.
|
||||||
* Calculate current list of tradable pairs.
|
* Calculate current list of tradable pairs.
|
||||||
|
@ -54,6 +56,7 @@ This loop will be repeated again and again until the bot is stopped.
|
||||||
[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated.
|
[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated.
|
||||||
|
|
||||||
* Load historic data for configured pairlist.
|
* Load historic data for configured pairlist.
|
||||||
|
* Calls `bot_start()` once.
|
||||||
* Calls `bot_loop_start()` once.
|
* Calls `bot_loop_start()` once.
|
||||||
* Calculate indicators (calls `populate_indicators()` once per pair).
|
* Calculate indicators (calls `populate_indicators()` once per pair).
|
||||||
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
|
||||||
|
|
|
@ -64,7 +64,10 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade
|
||||||
|
|
||||||
### Margin mode
|
### Margin mode
|
||||||
|
|
||||||
The possible values are: `isolated`, or `cross`(*currently unavailable*)
|
On top of `trading_mode` - you will also have to configure your `margin_mode`.
|
||||||
|
While freqtrade currently only supports one margin mode, this will change, and by configuring it now you're all set for future updates.
|
||||||
|
|
||||||
|
The possible values are: `isolated`, or `cross`(*currently unavailable*).
|
||||||
|
|
||||||
#### Isolated margin mode
|
#### Isolated margin mode
|
||||||
|
|
||||||
|
@ -82,6 +85,16 @@ One account is used to share collateral between markets (trading pairs). Margin
|
||||||
"margin_mode": "cross"
|
"margin_mode": "cross"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Set leverage to use
|
||||||
|
|
||||||
|
Different strategies and risk profiles will require different levels of leverage.
|
||||||
|
While you could configure one static leverage value - freqtrade offers you the flexibility to adjust this via [strategy leverage callback](strategy-callbacks.md#leverage-callback) - which allows you to use different leverages by pair, or based on some other factor benefitting your strategy result.
|
||||||
|
|
||||||
|
If not implemented, leverage defaults to 1x (no leverage).
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Higher leverage also equals higher risk - be sure you fully understand the implications of using leverage!
|
||||||
|
|
||||||
## Understand `liquidation_buffer`
|
## Understand `liquidation_buffer`
|
||||||
|
|
||||||
*Defaults to `0.05`*
|
*Defaults to `0.05`*
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mkdocs==1.3.0
|
mkdocs==1.3.0
|
||||||
mkdocs-material==8.2.16
|
mkdocs-material==8.3.8
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.4
|
pymdown-extensions==9.5
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
|
|
|
@ -89,11 +89,12 @@ WHERE id=31;
|
||||||
|
|
||||||
If you'd still like to remove a trade from the database directly, you can use the below query.
|
If you'd still like to remove a trade from the database directly, you can use the below query.
|
||||||
|
|
||||||
```sql
|
!!! Danger
|
||||||
DELETE FROM trades WHERE id = <tradeid>;
|
Some systems (Ubuntu) disable foreign keys in their sqlite3 packaging. When using sqlite - please ensure that foreign keys are on by running `PRAGMA foreign_keys = ON` before the above query.
|
||||||
```
|
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
DELETE FROM trades WHERE id = <tradeid>;
|
||||||
|
|
||||||
DELETE FROM trades WHERE id = 31;
|
DELETE FROM trades WHERE id = 31;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -102,13 +103,20 @@ DELETE FROM trades WHERE id = 31;
|
||||||
|
|
||||||
## Use a different database system
|
## Use a different database system
|
||||||
|
|
||||||
|
Freqtrade is using SQLAlchemy, which supports multiple different database systems. As such, a multitude of database systems should be supported.
|
||||||
|
Freqtrade does not depend or install any additional database driver. Please refer to the [SQLAlchemy docs](https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls) on installation instructions for the respective database systems.
|
||||||
|
|
||||||
|
The following systems have been tested and are known to work with freqtrade:
|
||||||
|
|
||||||
|
* sqlite (default)
|
||||||
|
* PostgreSQL)
|
||||||
|
* MariaDB
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
By using one of the below database systems, you acknowledge that you know how to manage such a system. Freqtrade will not provide any support with setup or maintenance (or backups) of the below database systems.
|
By using one of the below database systems, you acknowledge that you know how to manage such a system. The freqtrade team will not provide any support with setup or maintenance (or backups) of the below database systems.
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL
|
||||||
|
|
||||||
Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems.
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
`pip install psycopg2-binary`
|
`pip install psycopg2-binary`
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ In summary: The stoploss will be adjusted to be always be -10% of the highest ob
|
||||||
|
|
||||||
### Trailing stop loss, custom positive loss
|
### Trailing stop loss, custom positive loss
|
||||||
|
|
||||||
It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit positive result the system will utilize a new stop loss, which can have a different value.
|
You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, which can have a different value.
|
||||||
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
|
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -142,6 +142,8 @@ Both values require `trailing_stop` to be set to true and `trailing_stop_positiv
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
trailing_stop = True
|
trailing_stop = True
|
||||||
trailing_stop_positive = 0.02
|
trailing_stop_positive = 0.02
|
||||||
|
trailing_stop_positive_offset = 0.0
|
||||||
|
trailing_only_offset_is_reached = False # Default - not necessary for this example
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, simplified math:
|
For example, simplified math:
|
||||||
|
@ -156,11 +158,31 @@ For example, simplified math:
|
||||||
The 0.02 would translate to a -2% stop loss.
|
The 0.02 would translate to a -2% stop loss.
|
||||||
Before this, `stoploss` is used for the trailing stoploss.
|
Before this, `stoploss` is used for the trailing stoploss.
|
||||||
|
|
||||||
|
!!! Tip "Use an offset to change your stoploss"
|
||||||
|
Use `trailing_stop_positive_offset` to ensure that your new trailing stoploss will be in profit by setting `trailing_stop_positive_offset` higher than `trailing_stop_positive`. Your first new stoploss value will then already have locked in profits.
|
||||||
|
|
||||||
|
Example with simplified math:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
stoploss = -0.10
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.02
|
||||||
|
trailing_stop_positive_offset = 0.03
|
||||||
|
```
|
||||||
|
|
||||||
|
* the bot buys an asset at a price of 100$
|
||||||
|
* the stop loss is defined at -10%, so the stop loss would get triggered once the asset drops below 90$
|
||||||
|
* assuming the asset now increases to 102$
|
||||||
|
* the stoploss will now be at 91.8$ - 10% below the highest observed rate
|
||||||
|
* assuming the asset now increases to 103.5$ (above the offset configured)
|
||||||
|
* the stop loss will now be -2% of 103$ = 101.42$
|
||||||
|
* now the asset drops in value to 102\$, the stop loss will still be 101.42$ and would trigger once price breaks below 101.42$
|
||||||
|
|
||||||
### Trailing stop loss only once the trade has reached a certain offset
|
### Trailing stop loss only once the trade has reached a certain offset
|
||||||
|
|
||||||
It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
|
You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
|
||||||
|
|
||||||
If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
|
||||||
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
@ -191,6 +213,18 @@ For example, simplified math:
|
||||||
!!! Tip
|
!!! Tip
|
||||||
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
|
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
|
||||||
|
|
||||||
|
## Stoploss and Leverage
|
||||||
|
|
||||||
|
Stoploss should be thought of as "risk on this trade" - so a stoploss of 10% on a 100$ trade means you are willing to lose 10$ (10%) on this trade - which would trigger if the price moves 10% to the downside.
|
||||||
|
|
||||||
|
When using leverage, the same principle is applied - with stoploss defining the risk on the trade (the amount you are willing to lose).
|
||||||
|
|
||||||
|
Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move.
|
||||||
|
If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage).
|
||||||
|
If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case.
|
||||||
|
|
||||||
|
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
|
||||||
|
|
||||||
## Changing stoploss on open trades
|
## Changing stoploss on open trades
|
||||||
|
|
||||||
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).
|
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).
|
||||||
|
|
|
@ -551,6 +551,7 @@ class AwesomeStrategy(IStrategy):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in target (base) currency that's going to be traded.
|
:param amount: Amount in target (base) currency that's going to be traded.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
or current rate for market orders.
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
|
@ -600,6 +601,7 @@ class AwesomeStrategy(IStrategy):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in base currency.
|
:param amount: Amount in base currency.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
or current rate for market orders.
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param exit_reason: Exit reason.
|
:param exit_reason: Exit reason.
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
@ -804,19 +806,23 @@ For markets / exchanges that don't support leverage, this method is ignored.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
|
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_leverage: float, max_leverage: float, side: str,
|
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||||
**kwargs) -> float:
|
**kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Customize leverage for each new trade.
|
Customize leverage for each new trade. This method is only called in futures mode.
|
||||||
|
|
||||||
:param pair: Pair that's currently analyzed
|
:param pair: Pair that's currently analyzed
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
:param proposed_leverage: A leverage proposed by the bot.
|
:param proposed_leverage: A leverage proposed by the bot.
|
||||||
:param max_leverage: Max leverage allowed on this pair
|
:param max_leverage: Max leverage allowed on this pair
|
||||||
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||||
"""
|
"""
|
||||||
return 1.0
|
return 1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
|
||||||
|
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.
|
||||||
|
|
|
@ -171,8 +171,8 @@ official commands. You can ask at any moment for help with `/help`.
|
||||||
| `/locks` | Show currently locked pairs.
|
| `/locks` | Show currently locked pairs.
|
||||||
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
|
||||||
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
|
||||||
| `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
| `/forceexit <trade_id> | /fx <tradeid>` | Instantly exits the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
| `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`).
|
||||||
| `/fx` | alias for `/forceexit`
|
| `/fx` | alias for `/forceexit`
|
||||||
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True)
|
||||||
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True)
|
||||||
|
@ -270,10 +270,15 @@ Return a summary of your profit/loss and performance.
|
||||||
> **Latest Trade opened:** `2 minutes ago`
|
> **Latest Trade opened:** `2 minutes ago`
|
||||||
> **Avg. Duration:** `2:33:45`
|
> **Avg. Duration:** `2:33:45`
|
||||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||||
|
> **Trading volume:** `0.5 BTC`
|
||||||
|
> **Profit factor:** `1.04`
|
||||||
|
> **Max Drawdown:** `9.23% (0.01255 BTC)`
|
||||||
|
|
||||||
The relative profit of `1.2%` is the average profit per trade.
|
The relative profit of `1.2%` is the average profit per trade.
|
||||||
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`.
|
||||||
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits.
|
||||||
|
Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy.
|
||||||
|
Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`.
|
||||||
|
|
||||||
### /forceexit <trade_id>
|
### /forceexit <trade_id>
|
||||||
|
|
||||||
|
@ -281,6 +286,7 @@ Starting capital is either taken from the `available_capital` setting, or calcul
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
|
You can get a list of all open trades by calling `/forceexit` without parameter, which will show a list of buttons to simply exit a trade.
|
||||||
|
This command has an alias in `/fx` - which has the same capabilities, but is faster to type in "emergency" situations.
|
||||||
|
|
||||||
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
|
||||||
|
|
||||||
|
@ -328,11 +334,11 @@ Per default `/daily` will return the 7 last days. The example below if for `/dai
|
||||||
|
|
||||||
> **Daily Profit over the last 3 days:**
|
> **Daily Profit over the last 3 days:**
|
||||||
```
|
```
|
||||||
Day Profit BTC Profit USD
|
Day (count) USDT USD Profit %
|
||||||
---------- -------------- ------------
|
-------------- ------------ ---------- ----------
|
||||||
2018-01-03 0.00224175 BTC 29,142 USD
|
2022-06-11 (1) -0.746 USDT -0.75 USD -0.08%
|
||||||
2018-01-02 0.00033131 BTC 4,307 USD
|
2022-06-10 (0) 0 USDT 0.00 USD 0.00%
|
||||||
2018-01-01 0.00269130 BTC 34.986 USD
|
2022-06-09 (5) 20 USDT 20.10 USD 5.00%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /weekly <n>
|
### /weekly <n>
|
||||||
|
@ -342,11 +348,11 @@ from Monday. The example below if for `/weekly 3`:
|
||||||
|
|
||||||
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
> **Weekly Profit over the last 3 weeks (starting from Monday):**
|
||||||
```
|
```
|
||||||
Monday Profit BTC Profit USD
|
Monday (count) Profit BTC Profit USD Profit %
|
||||||
---------- -------------- ------------
|
------------- -------------- ------------ ----------
|
||||||
2018-01-03 0.00224175 BTC 29,142 USD
|
2018-01-03 (5) 0.00224175 BTC 29,142 USD 4.98%
|
||||||
2017-12-27 0.00033131 BTC 4,307 USD
|
2017-12-27 (1) 0.00033131 BTC 4,307 USD 0.00%
|
||||||
2017-12-20 0.00269130 BTC 34.986 USD
|
2017-12-20 (4) 0.00269130 BTC 34.986 USD 5.12%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /monthly <n>
|
### /monthly <n>
|
||||||
|
@ -356,11 +362,11 @@ if for `/monthly 3`:
|
||||||
|
|
||||||
> **Monthly Profit over the last 3 months:**
|
> **Monthly Profit over the last 3 months:**
|
||||||
```
|
```
|
||||||
Month Profit BTC Profit USD
|
Month (count) Profit BTC Profit USD Profit %
|
||||||
---------- -------------- ------------
|
------------- -------------- ------------ ----------
|
||||||
2018-01 0.00224175 BTC 29,142 USD
|
2018-01 (20) 0.00224175 BTC 29,142 USD 4.98%
|
||||||
2017-12 0.00033131 BTC 4,307 USD
|
2017-12 (5) 0.00033131 BTC 4,307 USD 0.00%
|
||||||
2017-11 0.00269130 BTC 34.986 USD
|
2017-11 (10) 0.00269130 BTC 34.986 USD 5.10%
|
||||||
```
|
```
|
||||||
|
|
||||||
### /whitelist
|
### /whitelist
|
||||||
|
|
|
@ -32,4 +32,8 @@ Please ensure that you're also updating dependencies - otherwise things might br
|
||||||
``` bash
|
``` bash
|
||||||
git pull
|
git pull
|
||||||
pip install -U -r requirements.txt
|
pip install -U -r requirements.txt
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Ensure freqUI is at the latest version
|
||||||
|
freqtrade install-ui
|
||||||
```
|
```
|
||||||
|
|
|
@ -651,6 +651,61 @@ Common arguments:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Detailed backtest analysis
|
||||||
|
|
||||||
|
Advanced backtest result analysis.
|
||||||
|
|
||||||
|
More details in the [Backtesting analysis](advanced-backtesting.md#analyze-the-buyentry-and-sellexit-tags) Section.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V]
|
||||||
|
[-c PATH] [-d PATH] [--userdir PATH]
|
||||||
|
[--export-filename PATH]
|
||||||
|
[--analysis-groups {0,1,2,3,4} [{0,1,2,3,4} ...]]
|
||||||
|
[--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]]
|
||||||
|
[--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]]
|
||||||
|
[--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--export-filename PATH, --backtest-filename PATH
|
||||||
|
Use this filename for backtest results.Requires
|
||||||
|
`--export` to be set as well. Example: `--export-filen
|
||||||
|
ame=user_data/backtest_results/backtest_today.json`
|
||||||
|
--analysis-groups {0,1,2,3,4} [{0,1,2,3,4} ...]
|
||||||
|
grouping output - 0: simple wins/losses by enter tag,
|
||||||
|
1: by enter_tag, 2: by enter_tag and exit_tag, 3: by
|
||||||
|
pair and enter_tag, 4: by pair, enter_ and exit_tag
|
||||||
|
(this can get quite large)
|
||||||
|
--enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...]
|
||||||
|
Comma separated list of entry signals to analyse.
|
||||||
|
Default: all. e.g. 'entry_tag_a,entry_tag_b'
|
||||||
|
--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]
|
||||||
|
Comma separated list of exit signals to analyse.
|
||||||
|
Default: all. e.g.
|
||||||
|
'exit_tag_a,roi,stop_loss,trailing_stop_loss'
|
||||||
|
--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]
|
||||||
|
Comma separated list of indicators to analyse. e.g.
|
||||||
|
'close,rsi,bb_lowerband,profit_abs'
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default:
|
||||||
|
`userdir/config.json` or `config.json` whichever
|
||||||
|
exists). Multiple --config options may be used. Can be
|
||||||
|
set to `-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## List Hyperopt results
|
## List Hyperopt results
|
||||||
|
|
||||||
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command.
|
||||||
|
|
|
@ -239,3 +239,52 @@ Possible parameters are:
|
||||||
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||||
|
|
||||||
The only possible value here is `{status}`.
|
The only possible value here is `{status}`.
|
||||||
|
|
||||||
|
## Discord
|
||||||
|
|
||||||
|
A special form of webhooks is available for discord.
|
||||||
|
You can configure this as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"webhook_url": "https://discord.com/api/webhooks/<Your webhook URL ...>",
|
||||||
|
"exit_fill": [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Close rate": "{close_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Profit": "{profit_amount} {stake_currency}"},
|
||||||
|
{"Profitability": "{profit_ratio:.2%}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Exit Reason": "{exit_reason}"},
|
||||||
|
{"Strategy": "{strategy}"},
|
||||||
|
{"Timeframe": "{timeframe}"},
|
||||||
|
],
|
||||||
|
"entry_fill": [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Strategy": "{strategy} {timeframe}"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible.
|
||||||
|
|
||||||
|
Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections.
|
||||||
|
|
||||||
|
The notifications will look as follows by default.
|
||||||
|
|
||||||
|
![discord-notification](assets/discord_notification.png)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.5.1'
|
__version__ = '2022.6'
|
||||||
|
|
||||||
if 'dev' in __version__:
|
if 'dev' in __version__:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -6,6 +6,7 @@ Contains all start-commands, subcommands and CLI Interface creation.
|
||||||
Note: Be careful with file-scoped imports in these subfiles.
|
Note: Be careful with file-scoped imports in these subfiles.
|
||||||
as they are parsed on startup, nothing containing optional modules should be loaded.
|
as they are parsed on startup, nothing containing optional modules should be loaded.
|
||||||
"""
|
"""
|
||||||
|
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
|
||||||
from freqtrade.commands.arguments import Arguments
|
from freqtrade.commands.arguments import Arguments
|
||||||
from freqtrade.commands.build_config_commands import start_new_config
|
from freqtrade.commands.build_config_commands import start_new_config
|
||||||
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
||||||
|
|
69
freqtrade/commands/analyze_commands.py
Executable file
69
freqtrade/commands/analyze_commands.py
Executable file
|
@ -0,0 +1,69 @@
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Prepare the configuration for the entry/exit reason analysis module
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:param method: Bot running mode
|
||||||
|
:return: Configuration
|
||||||
|
"""
|
||||||
|
config = setup_utils_configuration(args, method)
|
||||||
|
|
||||||
|
no_unlimited_runmodes = {
|
||||||
|
RunMode.BACKTEST: 'backtesting',
|
||||||
|
}
|
||||||
|
if method in no_unlimited_runmodes.keys():
|
||||||
|
from freqtrade.data.btanalysis import get_latest_backtest_filename
|
||||||
|
|
||||||
|
if 'exportfilename' in config:
|
||||||
|
if config['exportfilename'].is_dir():
|
||||||
|
btfile = Path(get_latest_backtest_filename(config['exportfilename']))
|
||||||
|
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
|
||||||
|
else:
|
||||||
|
if config['exportfilename'].exists():
|
||||||
|
btfile = Path(config['exportfilename'])
|
||||||
|
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
|
||||||
|
else:
|
||||||
|
raise OperationalException(f"{config['exportfilename']} does not exist.")
|
||||||
|
else:
|
||||||
|
raise OperationalException('exportfilename not in config.')
|
||||||
|
|
||||||
|
if (not Path(signals_file).exists()):
|
||||||
|
raise OperationalException(
|
||||||
|
(f"Cannot find latest backtest signals file: {signals_file}."
|
||||||
|
"Run backtesting with `--export signals`.")
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Start analysis script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
|
||||||
|
|
||||||
|
# Initialize configuration
|
||||||
|
config = setup_analyze_configuration(args, RunMode.BACKTEST)
|
||||||
|
|
||||||
|
logger.info('Starting freqtrade in analysis mode')
|
||||||
|
|
||||||
|
process_entry_exit_reasons(config['exportfilename'],
|
||||||
|
config['exchange']['pair_whitelist'],
|
||||||
|
config['analysis_groups'],
|
||||||
|
config['enter_reason_list'],
|
||||||
|
config['exit_reason_list'],
|
||||||
|
config['indicator_list']
|
||||||
|
)
|
|
@ -101,6 +101,9 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
||||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
||||||
"disableparamexport", "backtest_breakdown"]
|
"disableparamexport", "backtest_breakdown"]
|
||||||
|
|
||||||
|
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||||
|
"exit_reason_list", "indicator_list"]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||||
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||||
|
@ -182,8 +185,9 @@ class Arguments:
|
||||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||||
self._build_args(optionlist=['version'], parser=self.parser)
|
self._build_args(optionlist=['version'], parser=self.parser)
|
||||||
|
|
||||||
from freqtrade.commands import (start_backtesting, start_backtesting_show,
|
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting,
|
||||||
start_convert_data, start_convert_db, start_convert_trades,
|
start_backtesting_show, start_convert_data,
|
||||||
|
start_convert_db, start_convert_trades,
|
||||||
start_create_userdir, start_download_data, start_edge,
|
start_create_userdir, start_download_data, start_edge,
|
||||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||||
start_install_ui, start_list_data, start_list_exchanges,
|
start_install_ui, start_list_data, start_list_exchanges,
|
||||||
|
@ -283,6 +287,13 @@ class Arguments:
|
||||||
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||||
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||||
|
|
||||||
|
# Add backtesting analysis subcommand
|
||||||
|
analysis_cmd = subparsers.add_parser('backtesting-analysis',
|
||||||
|
help='Backtest Analysis module.',
|
||||||
|
parents=[_common_parser])
|
||||||
|
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
|
||||||
|
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
|
||||||
|
|
||||||
# Add edge subcommand
|
# Add edge subcommand
|
||||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
||||||
parents=[_common_parser, _strategy_parser])
|
parents=[_common_parser, _strategy_parser])
|
||||||
|
|
|
@ -614,4 +614,37 @@ AVAILABLE_CLI_OPTIONS = {
|
||||||
"that do not contain any parameters."),
|
"that do not contain any parameters."),
|
||||||
action="store_true",
|
action="store_true",
|
||||||
),
|
),
|
||||||
|
"analysis_groups": Arg(
|
||||||
|
"--analysis-groups",
|
||||||
|
help=("grouping output - "
|
||||||
|
"0: simple wins/losses by enter tag, "
|
||||||
|
"1: by enter_tag, "
|
||||||
|
"2: by enter_tag and exit_tag, "
|
||||||
|
"3: by pair and enter_tag, "
|
||||||
|
"4: by pair, enter_ and exit_tag (this can get quite large)"),
|
||||||
|
nargs='+',
|
||||||
|
default=['0', '1', '2'],
|
||||||
|
choices=['0', '1', '2', '3', '4'],
|
||||||
|
),
|
||||||
|
"enter_reason_list": Arg(
|
||||||
|
"--enter-reason-list",
|
||||||
|
help=("Comma separated list of entry signals to analyse. Default: all. "
|
||||||
|
"e.g. 'entry_tag_a,entry_tag_b'"),
|
||||||
|
nargs='+',
|
||||||
|
default=['all'],
|
||||||
|
),
|
||||||
|
"exit_reason_list": Arg(
|
||||||
|
"--exit-reason-list",
|
||||||
|
help=("Comma separated list of exit signals to analyse. Default: all. "
|
||||||
|
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
|
||||||
|
nargs='+',
|
||||||
|
default=['all'],
|
||||||
|
),
|
||||||
|
"indicator_list": Arg(
|
||||||
|
"--indicator-list",
|
||||||
|
help=("Comma separated list of indicators to analyse. "
|
||||||
|
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
|
||||||
|
nargs='+',
|
||||||
|
default=[],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
print_colorized = config.get('print_colorized', False)
|
print_colorized = config.get('print_colorized', False)
|
||||||
print_json = config.get('print_json', False)
|
print_json = config.get('print_json', False)
|
||||||
export_csv = config.get('export_csv', None)
|
export_csv = config.get('export_csv')
|
||||||
no_details = config.get('hyperopt_list_no_details', False)
|
no_details = config.get('hyperopt_list_no_details', False)
|
||||||
no_header = False
|
no_header = False
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,8 @@ class Configuration:
|
||||||
|
|
||||||
self._process_data_options(config)
|
self._process_data_options(config)
|
||||||
|
|
||||||
|
self._process_analyze_options(config)
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||||
|
|
||||||
|
@ -127,7 +129,7 @@ class Configuration:
|
||||||
# Default to in-memory db for dry_run if not specified
|
# Default to in-memory db for dry_run if not specified
|
||||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||||
else:
|
else:
|
||||||
if not config.get('db_url', None):
|
if not config.get('db_url'):
|
||||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
logger.info('Dry run is disabled')
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
|
@ -180,7 +182,7 @@ class Configuration:
|
||||||
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
||||||
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
||||||
|
|
||||||
config.update({'datadir': create_datadir(config, self.args.get('datadir', None))})
|
config.update({'datadir': create_datadir(config, self.args.get('datadir'))})
|
||||||
logger.info('Using data directory: %s ...', config.get('datadir'))
|
logger.info('Using data directory: %s ...', config.get('datadir'))
|
||||||
|
|
||||||
if self.args.get('exportfilename'):
|
if self.args.get('exportfilename'):
|
||||||
|
@ -219,7 +221,7 @@ class Configuration:
|
||||||
if config.get('max_open_trades') == -1:
|
if config.get('max_open_trades') == -1:
|
||||||
config['max_open_trades'] = float('inf')
|
config['max_open_trades'] = float('inf')
|
||||||
|
|
||||||
if self.args.get('stake_amount', None):
|
if self.args.get('stake_amount'):
|
||||||
# Convert explicitly to float to support CLI argument for both unlimited and value
|
# Convert explicitly to float to support CLI argument for both unlimited and value
|
||||||
try:
|
try:
|
||||||
self.args['stake_amount'] = float(self.args['stake_amount'])
|
self.args['stake_amount'] = float(self.args['stake_amount'])
|
||||||
|
@ -433,6 +435,19 @@ class Configuration:
|
||||||
self._args_to_config(config, argname='candle_types',
|
self._args_to_config(config, argname='candle_types',
|
||||||
logstring='Detected --candle-types: {}')
|
logstring='Detected --candle-types: {}')
|
||||||
|
|
||||||
|
def _process_analyze_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
self._args_to_config(config, argname='analysis_groups',
|
||||||
|
logstring='Analysis reason groups: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='enter_reason_list',
|
||||||
|
logstring='Analysis enter tag list: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='exit_reason_list',
|
||||||
|
logstring='Analysis exit tag list: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='indicator_list',
|
||||||
|
logstring='Analysis indicator list: {}')
|
||||||
|
|
||||||
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='dry_run',
|
self._args_to_config(config, argname='dry_run',
|
||||||
|
|
|
@ -336,6 +336,47 @@ CONF_SCHEMA = {
|
||||||
'webhookstatus': {'type': 'object'},
|
'webhookstatus': {'type': 'object'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'discord': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'webhook_url': {'type': 'string'},
|
||||||
|
"exit_fill": {
|
||||||
|
'type': 'array', 'items': {'type': 'object'},
|
||||||
|
'default': [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Close rate": "{close_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Close date": "{close_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Profit": "{profit_amount} {stake_currency}"},
|
||||||
|
{"Profitability": "{profit_ratio:.2%}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Exit Reason": "{exit_reason}"},
|
||||||
|
{"Strategy": "{strategy}"},
|
||||||
|
{"Timeframe": "{timeframe}"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"entry_fill": {
|
||||||
|
'type': 'array', 'items': {'type': 'object'},
|
||||||
|
'default': [
|
||||||
|
{"Trade ID": "{trade_id}"},
|
||||||
|
{"Exchange": "{exchange}"},
|
||||||
|
{"Pair": "{pair}"},
|
||||||
|
{"Direction": "{direction}"},
|
||||||
|
{"Open rate": "{open_rate}"},
|
||||||
|
{"Amount": "{amount}"},
|
||||||
|
{"Open date": "{open_date:%Y-%m-%d %H:%M:%S}"},
|
||||||
|
{"Enter tag": "{enter_tag}"},
|
||||||
|
{"Strategy": "{strategy} {timeframe}"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
'api_server': {
|
'api_server': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
|
|
@ -26,7 +26,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
|
||||||
'profit_ratio', 'profit_abs', 'exit_reason',
|
'profit_ratio', 'profit_abs', 'exit_reason',
|
||||||
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
|
||||||
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
|
||||||
'is_short'
|
'is_short', 'open_timestamp', 'close_timestamp', 'orders'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -283,6 +283,8 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
|
||||||
if 'enter_tag' not in df.columns:
|
if 'enter_tag' not in df.columns:
|
||||||
df['enter_tag'] = df['buy_tag']
|
df['enter_tag'] = df['buy_tag']
|
||||||
df = df.drop(['buy_tag'], axis=1)
|
df = df.drop(['buy_tag'], axis=1)
|
||||||
|
if 'orders' not in df.columns:
|
||||||
|
df.loc[:, 'orders'] = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# old format - only with lists.
|
# old format - only with lists.
|
||||||
|
@ -337,7 +339,7 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
|
||||||
:param trades: List of trade objects
|
:param trades: List of trade objects
|
||||||
:return: Dataframe with BT_DATA_COLUMNS
|
:return: Dataframe with BT_DATA_COLUMNS
|
||||||
"""
|
"""
|
||||||
df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS)
|
df = pd.DataFrame.from_records([t.to_json(True) for t in trades], columns=BT_DATA_COLUMNS)
|
||||||
if len(df) > 0:
|
if len(df) > 0:
|
||||||
df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True)
|
||||||
df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True)
|
||||||
|
|
227
freqtrade/data/entryexitanalysis.py
Executable file
227
freqtrade/data/entryexitanalysis.py
Executable file
|
@ -0,0 +1,227 @@
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import joblib
|
||||||
|
import pandas as pd
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
||||||
|
load_backtest_stats)
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_signal_candles(backtest_dir: Path):
|
||||||
|
if backtest_dir.is_dir():
|
||||||
|
scpf = Path(backtest_dir,
|
||||||
|
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
||||||
|
|
||||||
|
try:
|
||||||
|
scp = open(scpf, "rb")
|
||||||
|
signal_candles = joblib.load(scp)
|
||||||
|
logger.info(f"Loaded signal candles: {str(scpf)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Cannot load signal candles from pickled results: ", e)
|
||||||
|
|
||||||
|
return signal_candles
|
||||||
|
|
||||||
|
|
||||||
|
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||||
|
analysed_trades_dict = {}
|
||||||
|
analysed_trades_dict[strategy_name] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
|
||||||
|
|
||||||
|
for pair in pairlist:
|
||||||
|
if pair in signal_candles[strategy_name]:
|
||||||
|
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||||
|
pair,
|
||||||
|
trades,
|
||||||
|
signal_candles[strategy_name][pair])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||||
|
|
||||||
|
return analysed_trades_dict
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_candles_and_indicators(pair, trades, signal_candles):
|
||||||
|
buyf = signal_candles
|
||||||
|
|
||||||
|
if len(buyf) > 0:
|
||||||
|
buyf = buyf.set_index('date', drop=False)
|
||||||
|
trades_red = trades.loc[trades['pair'] == pair].copy()
|
||||||
|
|
||||||
|
trades_inds = pd.DataFrame()
|
||||||
|
|
||||||
|
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
|
||||||
|
for t, v in trades_red.open_date.items():
|
||||||
|
allinds = buyf.loc[(buyf['date'] < v)]
|
||||||
|
if allinds.shape[0] > 0:
|
||||||
|
tmp_inds = allinds.iloc[[-1]]
|
||||||
|
|
||||||
|
trades_red.loc[t, 'signal_date'] = tmp_inds['date'].values[0]
|
||||||
|
trades_red.loc[t, 'enter_reason'] = trades_red.loc[t, 'enter_tag']
|
||||||
|
tmp_inds.index.rename('signal_date', inplace=True)
|
||||||
|
trades_inds = pd.concat([trades_inds, tmp_inds])
|
||||||
|
|
||||||
|
if 'signal_date' in trades_red:
|
||||||
|
trades_red['signal_date'] = pd.to_datetime(trades_red['signal_date'], utc=True)
|
||||||
|
trades_red.set_index('signal_date', inplace=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
trades_red = pd.merge(trades_red, trades_inds, on='signal_date', how='outer')
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
return trades_red
|
||||||
|
else:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
def _do_group_table_output(bigdf, glist):
|
||||||
|
for g in glist:
|
||||||
|
# 0: summary wins/losses grouped by enter tag
|
||||||
|
if g == "0":
|
||||||
|
group_mask = ['enter_reason']
|
||||||
|
wins = bigdf.loc[bigdf['profit_abs'] >= 0] \
|
||||||
|
.groupby(group_mask) \
|
||||||
|
.agg({'profit_abs': ['sum']})
|
||||||
|
|
||||||
|
wins.columns = ['profit_abs_wins']
|
||||||
|
loss = bigdf.loc[bigdf['profit_abs'] < 0] \
|
||||||
|
.groupby(group_mask) \
|
||||||
|
.agg({'profit_abs': ['sum']})
|
||||||
|
loss.columns = ['profit_abs_loss']
|
||||||
|
|
||||||
|
new = bigdf.groupby(group_mask).agg({'profit_abs': [
|
||||||
|
'count',
|
||||||
|
lambda x: sum(x > 0),
|
||||||
|
lambda x: sum(x <= 0)]})
|
||||||
|
new = pd.concat([new, wins, loss], axis=1).fillna(0)
|
||||||
|
|
||||||
|
new['profit_tot'] = new['profit_abs_wins'] - abs(new['profit_abs_loss'])
|
||||||
|
new['wl_ratio_pct'] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
|
||||||
|
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
|
||||||
|
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
|
||||||
|
|
||||||
|
new.columns = ['total_num_buys', 'wins', 'losses', 'profit_abs_wins', 'profit_abs_loss',
|
||||||
|
'profit_tot', 'wl_ratio_pct', 'avg_win', 'avg_loss']
|
||||||
|
|
||||||
|
sortcols = ['total_num_buys']
|
||||||
|
|
||||||
|
_print_table(new, sortcols, show_index=True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
||||||
|
'profit_ratio': ['sum', 'median', 'mean']}
|
||||||
|
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
|
||||||
|
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
|
||||||
|
'total_profit_pct']
|
||||||
|
sortcols = ['profit_abs_sum', 'enter_reason']
|
||||||
|
|
||||||
|
# 1: profit summaries grouped by enter_tag
|
||||||
|
if g == "1":
|
||||||
|
group_mask = ['enter_reason']
|
||||||
|
|
||||||
|
# 2: profit summaries grouped by enter_tag and exit_tag
|
||||||
|
if g == "2":
|
||||||
|
group_mask = ['enter_reason', 'exit_reason']
|
||||||
|
|
||||||
|
# 3: profit summaries grouped by pair and enter_tag
|
||||||
|
if g == "3":
|
||||||
|
group_mask = ['pair', 'enter_reason']
|
||||||
|
|
||||||
|
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||||
|
if g == "4":
|
||||||
|
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
||||||
|
if group_mask:
|
||||||
|
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
||||||
|
new.columns = group_mask + agg_cols
|
||||||
|
new['median_profit_pct'] = new['median_profit_pct'] * 100
|
||||||
|
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
||||||
|
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
||||||
|
|
||||||
|
_print_table(new, sortcols)
|
||||||
|
else:
|
||||||
|
logger.warning("Invalid group mask specified.")
|
||||||
|
|
||||||
|
|
||||||
|
def _print_results(analysed_trades, stratname, analysis_groups,
|
||||||
|
enter_reason_list, exit_reason_list,
|
||||||
|
indicator_list, columns=None):
|
||||||
|
if columns is None:
|
||||||
|
columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason']
|
||||||
|
|
||||||
|
bigdf = pd.DataFrame()
|
||||||
|
for pair, trades in analysed_trades[stratname].items():
|
||||||
|
bigdf = pd.concat([bigdf, trades], ignore_index=True)
|
||||||
|
|
||||||
|
if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns):
|
||||||
|
if analysis_groups:
|
||||||
|
_do_group_table_output(bigdf, analysis_groups)
|
||||||
|
|
||||||
|
if enter_reason_list and "all" not in enter_reason_list:
|
||||||
|
bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))]
|
||||||
|
|
||||||
|
if exit_reason_list and "all" not in exit_reason_list:
|
||||||
|
bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))]
|
||||||
|
|
||||||
|
if "all" in indicator_list:
|
||||||
|
print(bigdf)
|
||||||
|
elif indicator_list is not None:
|
||||||
|
available_inds = []
|
||||||
|
for ind in indicator_list:
|
||||||
|
if ind in bigdf:
|
||||||
|
available_inds.append(ind)
|
||||||
|
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||||
|
_print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False)
|
||||||
|
else:
|
||||||
|
print("\\_ No trades to show")
|
||||||
|
|
||||||
|
|
||||||
|
def _print_table(df, sortcols=None, show_index=False):
|
||||||
|
if (sortcols is not None):
|
||||||
|
data = df.sort_values(sortcols)
|
||||||
|
else:
|
||||||
|
data = df
|
||||||
|
|
||||||
|
print(
|
||||||
|
tabulate(
|
||||||
|
data,
|
||||||
|
headers='keys',
|
||||||
|
tablefmt='psql',
|
||||||
|
showindex=show_index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_entry_exit_reasons(backtest_dir: Path,
|
||||||
|
pairlist: List[str],
|
||||||
|
analysis_groups: Optional[List[str]] = ["0", "1", "2"],
|
||||||
|
enter_reason_list: Optional[List[str]] = ["all"],
|
||||||
|
exit_reason_list: Optional[List[str]] = ["all"],
|
||||||
|
indicator_list: Optional[List[str]] = []):
|
||||||
|
try:
|
||||||
|
backtest_stats = load_backtest_stats(backtest_dir)
|
||||||
|
for strategy_name, results in backtest_stats['strategy'].items():
|
||||||
|
trades = load_backtest_data(backtest_dir, strategy_name)
|
||||||
|
|
||||||
|
if not trades.empty:
|
||||||
|
signal_candles = _load_signal_candles(backtest_dir)
|
||||||
|
analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name,
|
||||||
|
trades, signal_candles)
|
||||||
|
_print_results(analysed_trades_dict,
|
||||||
|
strategy_name,
|
||||||
|
analysis_groups,
|
||||||
|
enter_reason_list,
|
||||||
|
exit_reason_list,
|
||||||
|
indicator_list)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise OperationalException(e) from e
|
|
@ -221,7 +221,7 @@ def _download_pair_history(pair: str, *,
|
||||||
prepend=prepend)
|
prepend=prepend)
|
||||||
|
|
||||||
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, '
|
logger.info(f'({process}) - Download history data for "{pair}", {timeframe}, '
|
||||||
f'{candle_type} and store in {datadir}.'
|
f'{candle_type} and store in {datadir}. '
|
||||||
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
|
f'From {format_ms_time(since_ms) if since_ms else "start"} to '
|
||||||
f'{format_ms_time(until_ms) if until_ms else "now"}'
|
f'{format_ms_time(until_ms) if until_ms else "now"}'
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,10 +52,15 @@ class Binance(Exchange):
|
||||||
|
|
||||||
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
|
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
|
||||||
|
|
||||||
return order['type'] == ordertype and (
|
return (
|
||||||
|
order.get('stopPrice', None) is None
|
||||||
|
or (
|
||||||
|
order['type'] == ordertype
|
||||||
|
and (
|
||||||
(side == "sell" and stop_loss > float(order['stopPrice'])) or
|
(side == "sell" and stop_loss > float(order['stopPrice'])) or
|
||||||
(side == "buy" and stop_loss < float(order['stopPrice']))
|
(side == "buy" and stop_loss < float(order['stopPrice']))
|
||||||
)
|
)
|
||||||
|
))
|
||||||
|
|
||||||
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
|
||||||
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
tickers = super().get_tickers(symbols=symbols, cached=cached)
|
||||||
|
|
|
@ -93,7 +93,7 @@ class Exchange:
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._api: ccxt.Exchange
|
self._api: ccxt.Exchange
|
||||||
self._api_async: ccxt_async.Exchange
|
self._api_async: ccxt_async.Exchange = None
|
||||||
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]] = {}
|
||||||
|
@ -387,7 +387,7 @@ class Exchange:
|
||||||
and market.get('base', None) is not None
|
and market.get('base', None) is not None
|
||||||
and (self.precisionMode != TICK_SIZE
|
and (self.precisionMode != TICK_SIZE
|
||||||
# Too low precision will falsify calculations
|
# Too low precision will falsify calculations
|
||||||
or market.get('precision', {}).get('price', None) > 1e-11)
|
or market.get('precision', {}).get('price') > 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)))
|
||||||
|
@ -537,7 +537,7 @@ class Exchange:
|
||||||
# The internal info array is different for each particular market,
|
# The internal info array is different for each particular market,
|
||||||
# its contents depend on the exchange.
|
# its contents depend on the exchange.
|
||||||
# It can also be a string or similar ... so we need to verify that first.
|
# It can also be a string or similar ... so we need to verify that first.
|
||||||
elif (isinstance(self.markets[pair].get('info', None), dict)
|
elif (isinstance(self.markets[pair].get('info'), dict)
|
||||||
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
|
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
|
||||||
# Warn users about restricted pairs in whitelist.
|
# Warn users about restricted pairs in whitelist.
|
||||||
# We cannot determine reliably if Users are affected.
|
# We cannot determine reliably if Users are affected.
|
||||||
|
@ -2131,10 +2131,11 @@ class Exchange:
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier_async
|
||||||
def get_market_leverage_tiers(self, symbol) -> List[Dict]:
|
async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
|
||||||
try:
|
try:
|
||||||
return self._api.fetch_market_leverage_tiers(symbol)
|
tier = await self._api_async.fetch_market_leverage_tiers(symbol)
|
||||||
|
return symbol, tier
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
@ -2168,8 +2169,14 @@ class Exchange:
|
||||||
f"Initializing leverage_tiers for {len(symbols)} markets. "
|
f"Initializing leverage_tiers for {len(symbols)} markets. "
|
||||||
"This will take about a minute.")
|
"This will take about a minute.")
|
||||||
|
|
||||||
for symbol in sorted(symbols):
|
coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)]
|
||||||
tiers[symbol] = self.get_market_leverage_tiers(symbol)
|
|
||||||
|
for input_coro in chunks(coros, 100):
|
||||||
|
|
||||||
|
results = self.loop.run_until_complete(
|
||||||
|
asyncio.gather(*input_coro, return_exceptions=True))
|
||||||
|
for symbol, res in results:
|
||||||
|
tiers[symbol] = res
|
||||||
|
|
||||||
logger.info(f"Done initializing {len(symbols)} markets.")
|
logger.info(f"Done initializing {len(symbols)} markets.")
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
@ -24,6 +25,8 @@ class Gateio(Exchange):
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_volume_currency": "quote",
|
"ohlcv_volume_currency": "quote",
|
||||||
|
"time_in_force_parameter": "timeInForce",
|
||||||
|
"order_time_in_force": ['gtc', 'ioc'],
|
||||||
"stoploss_order_types": {"limit": "limit"},
|
"stoploss_order_types": {"limit": "limit"},
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
}
|
}
|
||||||
|
@ -40,13 +43,33 @@ class Gateio(Exchange):
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||||
super().validate_ordertypes(order_types)
|
|
||||||
|
|
||||||
if self.trading_mode != TradingMode.FUTURES:
|
if self.trading_mode != TradingMode.FUTURES:
|
||||||
if any(v == 'market' for k, v in order_types.items()):
|
if any(v == 'market' for k, v in order_types.items()):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
|
||||||
|
def _get_params(
|
||||||
|
self,
|
||||||
|
side: BuySell,
|
||||||
|
ordertype: str,
|
||||||
|
leverage: float,
|
||||||
|
reduceOnly: bool,
|
||||||
|
time_in_force: str = 'gtc',
|
||||||
|
) -> Dict:
|
||||||
|
params = super()._get_params(
|
||||||
|
side=side,
|
||||||
|
ordertype=ordertype,
|
||||||
|
leverage=leverage,
|
||||||
|
reduceOnly=reduceOnly,
|
||||||
|
time_in_force=time_in_force,
|
||||||
|
)
|
||||||
|
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES:
|
||||||
|
params['type'] = 'market'
|
||||||
|
param = self._ft_has.get('time_in_force_parameter', '')
|
||||||
|
params.update({param: 'ioc'})
|
||||||
|
return params
|
||||||
|
|
||||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
|
||||||
params: Optional[Dict] = None) -> List:
|
params: Optional[Dict] = None) -> List:
|
||||||
trades = super().get_trades_for_order(order_id, pair, since, params)
|
trades = super().get_trades_for_order(order_id, pair, since, params)
|
||||||
|
@ -61,7 +84,8 @@ class Gateio(Exchange):
|
||||||
pair_fees = self._trading_fees.get(pair, {})
|
pair_fees = self._trading_fees.get(pair, {})
|
||||||
if pair_fees:
|
if pair_fees:
|
||||||
for idx, trade in enumerate(trades):
|
for idx, trade in enumerate(trades):
|
||||||
if trade.get('fee', {}).get('cost') is None:
|
fee = trade.get('fee', {})
|
||||||
|
if fee and fee.get('cost') is None:
|
||||||
takerOrMaker = trade.get('takerOrMaker', 'taker')
|
takerOrMaker = trade.get('takerOrMaker', 'taker')
|
||||||
if pair_fees.get(takerOrMaker) is not None:
|
if pair_fees.get(takerOrMaker) is not None:
|
||||||
trades[idx]['fee'] = {
|
trades[idx]['fee'] = {
|
||||||
|
@ -90,5 +114,7 @@ class Gateio(Exchange):
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
"""
|
"""
|
||||||
return ((side == "sell" and stop_loss > float(order['stopPrice'])) or
|
return (order.get('stopPrice', None) is None or (
|
||||||
(side == "buy" and stop_loss < float(order['stopPrice'])))
|
side == "sell" and stop_loss > float(order['stopPrice'])) or
|
||||||
|
(side == "buy" and stop_loss < float(order['stopPrice']))
|
||||||
|
)
|
||||||
|
|
|
@ -27,7 +27,13 @@ class Huobi(Exchange):
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
"""
|
"""
|
||||||
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
return (
|
||||||
|
order.get('stopPrice', None) is None
|
||||||
|
or (
|
||||||
|
order['type'] == 'stop'
|
||||||
|
and stop_loss > float(order['stopPrice'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,10 @@ class Kucoin(Exchange):
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
"""
|
"""
|
||||||
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
return (
|
||||||
|
order.get('stopPrice', None) is None
|
||||||
|
or stop_loss > float(order['stopPrice'])
|
||||||
|
)
|
||||||
|
|
||||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime, time, timezone
|
from datetime import datetime, time, timedelta, timezone
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
@ -67,14 +67,12 @@ class FreqtradeBot(LoggingMixin):
|
||||||
|
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
|
|
||||||
init_db(self.config.get('db_url', None))
|
init_db(self.config['db_url'])
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
|
|
||||||
PairLocks.timeframe = self.config['timeframe']
|
PairLocks.timeframe = self.config['timeframe']
|
||||||
|
|
||||||
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
|
||||||
|
|
||||||
# RPC runs in separate threads, can start handling external commands just after
|
# RPC runs in separate threads, can start handling external commands just after
|
||||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||||
# so anything in the Freqtradebot instance should be ready (initialized), including
|
# so anything in the Freqtradebot instance should be ready (initialized), including
|
||||||
|
@ -124,6 +122,8 @@ class FreqtradeBot(LoggingMixin):
|
||||||
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||||
|
|
||||||
self.strategy.ft_bot_start()
|
self.strategy.ft_bot_start()
|
||||||
|
# Initialize protections AFTER bot start - otherwise parameters are not loaded.
|
||||||
|
self.protections = ProtectionManager(self.config, self.strategy.protections)
|
||||||
|
|
||||||
def notify_status(self, msg: str) -> None:
|
def notify_status(self, msg: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -227,7 +227,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
Notify the user when the bot is stopped (not reloaded)
|
Notify the user when the bot is stopped (not reloaded)
|
||||||
and there are still open trades active.
|
and there are still open trades active.
|
||||||
"""
|
"""
|
||||||
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
open_trades = Trade.get_open_trades()
|
||||||
|
|
||||||
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
||||||
msg = {
|
msg = {
|
||||||
|
@ -302,6 +302,15 @@ class FreqtradeBot(LoggingMixin):
|
||||||
self.update_trade_state(order.trade, order.order_id, fo,
|
self.update_trade_state(order.trade, order.order_id, fo,
|
||||||
stoploss_order=(order.ft_order_side == 'stoploss'))
|
stoploss_order=(order.ft_order_side == 'stoploss'))
|
||||||
|
|
||||||
|
except InvalidOrderException as e:
|
||||||
|
logger.warning(f"Error updating Order {order.order_id} due to {e}.")
|
||||||
|
if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
|
||||||
|
logger.warning(
|
||||||
|
"Order is older than 5 days. Assuming order was fully cancelled.")
|
||||||
|
fo = order.to_ccxt_object()
|
||||||
|
fo['status'] = 'canceled'
|
||||||
|
self.handle_timedout_order(fo, order.trade)
|
||||||
|
|
||||||
except ExchangeError as e:
|
except ExchangeError as e:
|
||||||
|
|
||||||
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
logger.warning(f"Error updating Order {order.order_id} due to {e}")
|
||||||
|
@ -639,7 +648,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
)
|
)
|
||||||
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
order_status = order.get('status', None)
|
order_status = order.get('status')
|
||||||
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
||||||
|
|
||||||
# we assume the order is executed at the price requested
|
# we assume the order is executed at the price requested
|
||||||
|
@ -781,7 +790,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
current_rate=enter_limit_requested,
|
current_rate=enter_limit_requested,
|
||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=max_leverage,
|
max_leverage=max_leverage,
|
||||||
side=trade_side,
|
side=trade_side, entry_tag=entry_tag,
|
||||||
) if self.trading_mode != TradingMode.SPOT else 1.0
|
) if self.trading_mode != TradingMode.SPOT else 1.0
|
||||||
# Cap leverage between 1.0 and max_leverage.
|
# Cap leverage between 1.0 and max_leverage.
|
||||||
leverage = min(max(leverage, 1.0), max_leverage)
|
leverage = min(max(leverage, 1.0), max_leverage)
|
||||||
|
@ -950,6 +959,29 @@ class FreqtradeBot(LoggingMixin):
|
||||||
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
|
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
||||||
|
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Check and execute trade exit
|
||||||
|
"""
|
||||||
|
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
||||||
|
trade,
|
||||||
|
exit_rate,
|
||||||
|
datetime.now(timezone.utc),
|
||||||
|
enter=enter,
|
||||||
|
exit_=exit_,
|
||||||
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
|
)
|
||||||
|
for should_exit in exits:
|
||||||
|
if should_exit.exit_flag:
|
||||||
|
exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
|
||||||
|
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
||||||
|
f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
|
||||||
|
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
|
||||||
|
if exited:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
|
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
|
||||||
"""
|
"""
|
||||||
Abstracts creating stoploss orders from the logic.
|
Abstracts creating stoploss orders from the logic.
|
||||||
|
@ -1101,28 +1133,6 @@ class FreqtradeBot(LoggingMixin):
|
||||||
logger.warning(f"Could not create trailing stoploss order "
|
logger.warning(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
|
|
||||||
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
|
|
||||||
"""
|
|
||||||
Check and execute trade exit
|
|
||||||
"""
|
|
||||||
exits: List[ExitCheckTuple] = self.strategy.should_exit(
|
|
||||||
trade,
|
|
||||||
exit_rate,
|
|
||||||
datetime.now(timezone.utc),
|
|
||||||
enter=enter,
|
|
||||||
exit_=exit_,
|
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
|
||||||
)
|
|
||||||
for should_exit in exits:
|
|
||||||
if should_exit.exit_flag:
|
|
||||||
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
|
|
||||||
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
|
|
||||||
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
|
|
||||||
if exited:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def manage_open_orders(self) -> None:
|
def manage_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
|
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
|
||||||
|
@ -1532,7 +1542,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.utcnow(),
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'fiat_display_currency' in self.config:
|
if 'fiat_display_currency' in self.config:
|
||||||
|
@ -1643,7 +1653,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
|
|
||||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
# If a entry order was closed, force update on stoploss on exchange
|
# If a entry order was closed, force update on stoploss on exchange
|
||||||
if order.get('side', None) == trade.entry_side:
|
if order.get('side') == trade.entry_side:
|
||||||
trade = self.cancel_stoploss_on_exchange(trade)
|
trade = self.cancel_stoploss_on_exchange(trade)
|
||||||
# TODO: Margin will need to use interest_rate as well.
|
# TODO: Margin will need to use interest_rate as well.
|
||||||
# interest_rate = self.exchange.get_interest_rate()
|
# interest_rate = self.exchange.get_interest_rate()
|
||||||
|
|
|
@ -87,7 +87,7 @@ class Backtesting:
|
||||||
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)
|
||||||
|
|
||||||
if self.config.get('strategy_list', None):
|
if self.config.get('strategy_list'):
|
||||||
for strat in list(self.config['strategy_list']):
|
for strat in list(self.config['strategy_list']):
|
||||||
stratconf = deepcopy(self.config)
|
stratconf = deepcopy(self.config)
|
||||||
stratconf['strategy'] = strat
|
stratconf['strategy'] = strat
|
||||||
|
@ -189,6 +189,7 @@ class Backtesting:
|
||||||
self.strategy.order_types['stoploss_on_exchange'] = False
|
self.strategy.order_types['stoploss_on_exchange'] = False
|
||||||
|
|
||||||
self.strategy.ft_bot_start()
|
self.strategy.ft_bot_start()
|
||||||
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||||
|
|
||||||
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):
|
||||||
|
@ -704,7 +705,7 @@ class Backtesting:
|
||||||
current_rate=row[OPEN_IDX],
|
current_rate=row[OPEN_IDX],
|
||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=max_leverage,
|
max_leverage=max_leverage,
|
||||||
side=direction,
|
side=direction, entry_tag=entry_tag,
|
||||||
) if self._can_short else 1.0
|
) if self._can_short else 1.0
|
||||||
# Cap leverage between 1.0 and max_leverage.
|
# Cap leverage between 1.0 and max_leverage.
|
||||||
leverage = min(max(leverage, 1.0), max_leverage)
|
leverage = min(max(leverage, 1.0), max_leverage)
|
||||||
|
@ -966,6 +967,7 @@ class Backtesting:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
del trade.orders[trade.orders.index(order)]
|
del trade.orders[trade.orders.index(order)]
|
||||||
|
trade.open_order_id = None
|
||||||
self.canceled_entry_orders += 1
|
self.canceled_entry_orders += 1
|
||||||
|
|
||||||
# place new order if result was not None
|
# place new order if result was not None
|
||||||
|
@ -1054,6 +1056,7 @@ class Backtesting:
|
||||||
# Close trade
|
# Close trade
|
||||||
open_trade_count -= 1
|
open_trade_count -= 1
|
||||||
open_trades[pair].remove(t)
|
open_trades[pair].remove(t)
|
||||||
|
LocalTrade.trades_open.remove(t)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# 2. Process entries.
|
# 2. Process entries.
|
||||||
|
@ -1077,6 +1080,8 @@ class Backtesting:
|
||||||
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}.")
|
||||||
open_trades[pair].append(trade)
|
open_trades[pair].append(trade)
|
||||||
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
for trade in list(open_trades[pair]):
|
for trade in list(open_trades[pair]):
|
||||||
# 3. Process entry orders.
|
# 3. Process entry orders.
|
||||||
|
@ -1084,7 +1089,6 @@ class Backtesting:
|
||||||
if order and self._get_order_filled(order.price, row):
|
if order and self._get_order_filled(order.price, row):
|
||||||
order.close_bt_order(current_time, trade)
|
order.close_bt_order(current_time, trade)
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
LocalTrade.add_bt_trade(trade)
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# 4. Create exit orders (if any)
|
# 4. Create exit orders (if any)
|
||||||
|
@ -1094,6 +1098,7 @@ class Backtesting:
|
||||||
# 5. Process exit orders.
|
# 5. Process exit orders.
|
||||||
order = trade.select_order(trade.exit_side, is_open=True)
|
order = trade.select_order(trade.exit_side, is_open=True)
|
||||||
if order and self._get_order_filled(order.price, row):
|
if order and self._get_order_filled(order.price, row):
|
||||||
|
order.close_bt_order(current_time, trade)
|
||||||
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)
|
||||||
|
@ -1136,8 +1141,6 @@ class Backtesting:
|
||||||
backtest_start_time = datetime.now(timezone.utc)
|
backtest_start_time = datetime.now(timezone.utc)
|
||||||
self._set_strategy(strat)
|
self._set_strategy(strat)
|
||||||
|
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
|
||||||
|
|
||||||
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
||||||
if self.config.get('use_max_market_positions', True):
|
if self.config.get('use_max_market_positions', True):
|
||||||
# Must come from strategy config, as the strategy may modify this setting.
|
# Must come from strategy config, as the strategy may modify this setting.
|
||||||
|
@ -1262,13 +1265,14 @@ class Backtesting:
|
||||||
self.results['strategy_comparison'].extend(results['strategy_comparison'])
|
self.results['strategy_comparison'].extend(results['strategy_comparison'])
|
||||||
else:
|
else:
|
||||||
self.results = results
|
self.results = results
|
||||||
|
dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
if self.config.get('export', 'none') in ('trades', 'signals'):
|
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, dt_appendix)
|
||||||
|
|
||||||
if (self.config.get('export', 'none') == 'signals' and
|
if (self.config.get('export', 'none') == 'signals' and
|
||||||
self.dataprovider.runmode == RunMode.BACKTEST):
|
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||||
store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs)
|
store_backtest_signal_candles(
|
||||||
|
self.config['exportfilename'], self.processed_dfs, dt_appendix)
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -429,7 +429,7 @@ class Hyperopt:
|
||||||
return new_list
|
return new_list
|
||||||
i = 0
|
i = 0
|
||||||
asked_non_tried: List[List[Any]] = []
|
asked_non_tried: List[List[Any]] = []
|
||||||
is_random: List[bool] = []
|
is_random_non_tried: List[bool] = []
|
||||||
while i < 5 and len(asked_non_tried) < n_points:
|
while i < 5 and len(asked_non_tried) < n_points:
|
||||||
if i < 3:
|
if i < 3:
|
||||||
self.opt.cache_ = {}
|
self.opt.cache_ = {}
|
||||||
|
@ -438,7 +438,7 @@ class Hyperopt:
|
||||||
else:
|
else:
|
||||||
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5))
|
||||||
is_random = [True for _ in range(len(asked))]
|
is_random = [True for _ in range(len(asked))]
|
||||||
is_random += [rand for x, rand in zip(asked, is_random)
|
is_random_non_tried += [rand for x, rand in zip(asked, is_random)
|
||||||
if x not in self.opt.Xi
|
if x not in self.opt.Xi
|
||||||
and x not in asked_non_tried]
|
and x not in asked_non_tried]
|
||||||
asked_non_tried += [x for x in asked
|
asked_non_tried += [x for x in asked
|
||||||
|
@ -449,13 +449,13 @@ class Hyperopt:
|
||||||
if asked_non_tried:
|
if asked_non_tried:
|
||||||
return (
|
return (
|
||||||
asked_non_tried[:min(len(asked_non_tried), n_points)],
|
asked_non_tried[:min(len(asked_non_tried), n_points)],
|
||||||
is_random[:min(len(asked_non_tried), n_points)]
|
is_random_non_tried[:min(len(asked_non_tried), n_points)]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
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'))
|
||||||
logger.info(f"Using optimizer random state: {self.random_state}")
|
logger.info(f"Using optimizer random state: {self.random_state}")
|
||||||
self.hyperopt_table_header = -1
|
self.hyperopt_table_header = -1
|
||||||
# Initialize spaces ...
|
# Initialize spaces ...
|
||||||
|
|
|
@ -127,14 +127,14 @@ class HyperoptTools():
|
||||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time'),
|
||||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time'),
|
||||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit'),
|
||||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit'),
|
||||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit'),
|
||||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit'),
|
||||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
'filter_min_objective': config.get('hyperopt_list_min_objective'),
|
||||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
'filter_max_objective': config.get('hyperopt_list_max_objective'),
|
||||||
}
|
}
|
||||||
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
||||||
# No file found.
|
# No file found.
|
||||||
|
|
|
@ -4,7 +4,6 @@ from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
from numpy import int64
|
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
@ -18,21 +17,21 @@ from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None:
|
def store_backtest_stats(
|
||||||
|
recordfilename: Path, stats: Dict[str, DataFrame], dtappendix: str) -> None:
|
||||||
"""
|
"""
|
||||||
Stores backtest results
|
Stores backtest results
|
||||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||||
Filenames will be appended with a timestamp right before the suffix
|
Filenames will be appended with a timestamp right before the suffix
|
||||||
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
|
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
|
||||||
:param stats: Dataframe containing the backtesting statistics
|
:param stats: Dataframe containing the backtesting statistics
|
||||||
|
:param dtappendix: Datetime to use for the filename
|
||||||
"""
|
"""
|
||||||
if recordfilename.is_dir():
|
if recordfilename.is_dir():
|
||||||
filename = (recordfilename /
|
filename = (recordfilename / f'backtest-result-{dtappendix}.json')
|
||||||
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json')
|
|
||||||
else:
|
else:
|
||||||
filename = Path.joinpath(
|
filename = Path.joinpath(
|
||||||
recordfilename.parent,
|
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}'
|
||||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
|
||||||
).with_suffix(recordfilename.suffix)
|
).with_suffix(recordfilename.suffix)
|
||||||
|
|
||||||
# Store metadata separately.
|
# Store metadata separately.
|
||||||
|
@ -45,7 +44,8 @@ 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:
|
def store_backtest_signal_candles(
|
||||||
|
recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path:
|
||||||
"""
|
"""
|
||||||
Stores backtest trade signal candles
|
Stores backtest trade signal candles
|
||||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||||
|
@ -53,14 +53,13 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]
|
||||||
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
|
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
|
||||||
as filename
|
as filename
|
||||||
:param stats: Dict containing the backtesting signal candles
|
:param stats: Dict containing the backtesting signal candles
|
||||||
|
:param dtappendix: Datetime to use for the filename
|
||||||
"""
|
"""
|
||||||
if recordfilename.is_dir():
|
if recordfilename.is_dir():
|
||||||
filename = (recordfilename /
|
filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl')
|
||||||
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl')
|
|
||||||
else:
|
else:
|
||||||
filename = Path.joinpath(
|
filename = Path.joinpath(
|
||||||
recordfilename.parent,
|
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl'
|
||||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
file_dump_joblib(filename, candles)
|
file_dump_joblib(filename, candles)
|
||||||
|
@ -417,9 +416,9 @@ def generate_strategy_stats(pairlist: List[str],
|
||||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||||
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||||
if not results.empty:
|
winning_profit = results.loc[results['profit_abs'] > 0, 'profit_abs'].sum()
|
||||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum()
|
||||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0
|
||||||
|
|
||||||
backtest_days = (max_date - min_date).days or 1
|
backtest_days = (max_date - min_date).days or 1
|
||||||
strat_stats = {
|
strat_stats = {
|
||||||
|
@ -447,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str],
|
||||||
'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']),
|
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
|
||||||
|
'profit_factor': profit_factor,
|
||||||
'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),
|
||||||
|
@ -501,8 +501,10 @@ def generate_strategy_stats(pairlist: List[str],
|
||||||
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
|
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
|
||||||
max_drawdown) = calculate_max_drawdown(
|
max_drawdown) = calculate_max_drawdown(
|
||||||
results, value_col='profit_abs', starting_balance=start_balance)
|
results, value_col='profit_abs', starting_balance=start_balance)
|
||||||
|
# max_relative_drawdown = Underwater
|
||||||
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
|
(_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown(
|
||||||
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
|
results, value_col='profit_abs', starting_balance=start_balance, relative=True)
|
||||||
|
|
||||||
strat_stats.update({
|
strat_stats.update({
|
||||||
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
|
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
|
||||||
'max_drawdown_account': max_drawdown,
|
'max_drawdown_account': max_drawdown,
|
||||||
|
@ -781,6 +783,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||||
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'),
|
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
|
||||||
|
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
|
||||||
|
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%}"),
|
||||||
|
|
|
@ -201,16 +201,18 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
|
||||||
|
|
||||||
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
|
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
|
||||||
average = get_column_def(cols_order, 'average', 'null')
|
average = get_column_def(cols_order, 'average', 'null')
|
||||||
|
stop_price = get_column_def(cols_order, 'stop_price', 'null')
|
||||||
|
|
||||||
# sqlite does not support literals for booleans
|
# sqlite does not support literals for booleans
|
||||||
with engine.begin() as connection:
|
with engine.begin() as connection:
|
||||||
connection.execute(text(f"""
|
connection.execute(text(f"""
|
||||||
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||||
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
|
||||||
order_date, order_filled_date, order_update_date, ft_fee_base)
|
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base)
|
||||||
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
|
||||||
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
|
||||||
cost, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base
|
cost, {stop_price} stop_price, order_date, order_filled_date,
|
||||||
|
order_update_date, {ft_fee_base} ft_fee_base
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
@ -247,6 +249,35 @@ def set_sqlite_to_wal(engine):
|
||||||
connection.execute(text("PRAGMA journal_mode=wal"))
|
connection.execute(text("PRAGMA journal_mode=wal"))
|
||||||
|
|
||||||
|
|
||||||
|
def fix_old_dry_orders(engine):
|
||||||
|
with engine.begin() as connection:
|
||||||
|
connection.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
update orders
|
||||||
|
set ft_is_open = 0
|
||||||
|
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
|
||||||
|
select id, stoploss_order_id from trades where stoploss_order_id is not null
|
||||||
|
) and ft_order_side = 'stoploss'
|
||||||
|
and order_id like 'dry_%'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
connection.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
update orders
|
||||||
|
set ft_is_open = 0
|
||||||
|
where ft_is_open = 1
|
||||||
|
and (ft_trade_id, order_id) not in (
|
||||||
|
select id, open_order_id from trades where open_order_id is not null
|
||||||
|
) and ft_order_side != 'stoploss'
|
||||||
|
and order_id like 'dry_%'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_migrate(engine, decl_base, previous_tables) -> None:
|
def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if migration is necessary and migrates if necessary
|
Checks if migration is necessary and migrates if necessary
|
||||||
|
@ -265,9 +296,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||||
|
|
||||||
# Check if migration necessary
|
# Check if migration necessary
|
||||||
# Migrates both trades and orders table!
|
# Migrates both trades and orders table!
|
||||||
# if ('orders' not in previous_tables
|
if not has_column(cols_orders, 'stop_price'):
|
||||||
# or not has_column(cols_orders, 'leverage')):
|
# if not has_column(cols_trades, 'base_currency'):
|
||||||
if not has_column(cols_trades, 'base_currency'):
|
|
||||||
logger.info(f"Running database migration for trades - "
|
logger.info(f"Running database migration for trades - "
|
||||||
f"backup: {table_back_name}, {order_table_bak_name}")
|
f"backup: {table_back_name}, {order_table_bak_name}")
|
||||||
migrate_trades_and_orders_table(
|
migrate_trades_and_orders_table(
|
||||||
|
@ -288,3 +318,4 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
|
||||||
"start with a fresh database.")
|
"start with a fresh database.")
|
||||||
|
|
||||||
set_sqlite_to_wal(engine)
|
set_sqlite_to_wal(engine)
|
||||||
|
fix_old_dry_orders(engine)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
|
||||||
UniqueConstraint, desc, func)
|
UniqueConstraint, desc, func)
|
||||||
from sqlalchemy.orm import Query, relationship
|
from sqlalchemy.orm import Query, lazyload, relationship
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort
|
||||||
from freqtrade.enums import ExitType, TradingMode
|
from freqtrade.enums import ExitType, TradingMode
|
||||||
|
@ -57,6 +57,7 @@ class Order(_DECL_BASE):
|
||||||
filled = Column(Float, nullable=True)
|
filled = Column(Float, nullable=True)
|
||||||
remaining = Column(Float, nullable=True)
|
remaining = Column(Float, nullable=True)
|
||||||
cost = Column(Float, nullable=True)
|
cost = Column(Float, nullable=True)
|
||||||
|
stop_price = Column(Float, nullable=True)
|
||||||
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
|
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
|
||||||
order_filled_date = Column(DateTime, nullable=True)
|
order_filled_date = Column(DateTime, nullable=True)
|
||||||
order_update_date = Column(DateTime, nullable=True)
|
order_update_date = Column(DateTime, nullable=True)
|
||||||
|
@ -74,7 +75,7 @@ class Order(_DECL_BASE):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def safe_filled(self) -> float:
|
def safe_filled(self) -> float:
|
||||||
return self.filled or self.amount or 0.0
|
return self.filled if self.filled is not None else self.amount or 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def safe_fee_base(self) -> float:
|
def safe_fee_base(self) -> float:
|
||||||
|
@ -107,6 +108,7 @@ class Order(_DECL_BASE):
|
||||||
self.average = order.get('average', self.average)
|
self.average = order.get('average', self.average)
|
||||||
self.remaining = order.get('remaining', self.remaining)
|
self.remaining = order.get('remaining', self.remaining)
|
||||||
self.cost = order.get('cost', self.cost)
|
self.cost = order.get('cost', self.cost)
|
||||||
|
self.stop_price = order.get('stopPrice', self.stop_price)
|
||||||
|
|
||||||
if 'timestamp' in order and order['timestamp'] is not None:
|
if 'timestamp' in order and order['timestamp'] is not None:
|
||||||
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
|
||||||
|
@ -130,6 +132,7 @@ class Order(_DECL_BASE):
|
||||||
'side': self.ft_order_side,
|
'side': self.ft_order_side,
|
||||||
'filled': self.filled,
|
'filled': self.filled,
|
||||||
'remaining': self.remaining,
|
'remaining': self.remaining,
|
||||||
|
'stopPrice': self.stop_price,
|
||||||
'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
|
'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'),
|
||||||
'timestamp': int(self.order_date_utc.timestamp() * 1000),
|
'timestamp': int(self.order_date_utc.timestamp() * 1000),
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
|
@ -137,17 +140,23 @@ class Order(_DECL_BASE):
|
||||||
'info': {},
|
'info': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
|
||||||
return {
|
resp = {
|
||||||
|
'amount': self.amount,
|
||||||
|
'safe_price': self.safe_price,
|
||||||
|
'ft_order_side': self.ft_order_side,
|
||||||
|
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||||
|
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||||
|
'ft_is_entry': self.ft_order_side == entry_side,
|
||||||
|
}
|
||||||
|
if not minified:
|
||||||
|
resp.update({
|
||||||
'pair': self.ft_pair,
|
'pair': self.ft_pair,
|
||||||
'order_id': self.order_id,
|
'order_id': self.order_id,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
'amount': self.amount,
|
|
||||||
'average': round(self.average, 8) if self.average else 0,
|
'average': round(self.average, 8) if self.average else 0,
|
||||||
'safe_price': self.safe_price,
|
|
||||||
'cost': self.cost if self.cost else 0,
|
'cost': self.cost if self.cost else 0,
|
||||||
'filled': self.filled,
|
'filled': self.filled,
|
||||||
'ft_order_side': self.ft_order_side,
|
|
||||||
'is_open': self.ft_is_open,
|
'is_open': self.ft_is_open,
|
||||||
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
|
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
|
||||||
if self.order_date else None,
|
if self.order_date else None,
|
||||||
|
@ -155,17 +164,16 @@ class Order(_DECL_BASE):
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
|
||||||
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
|
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
|
||||||
if self.order_filled_date else None,
|
if self.order_filled_date else None,
|
||||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
|
||||||
'order_type': self.order_type,
|
'order_type': self.order_type,
|
||||||
'price': self.price,
|
'price': self.price,
|
||||||
'ft_is_entry': self.ft_order_side == entry_side,
|
|
||||||
'remaining': self.remaining,
|
'remaining': self.remaining,
|
||||||
}
|
})
|
||||||
|
return resp
|
||||||
|
|
||||||
def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
|
def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'):
|
||||||
self.order_filled_date = close_date
|
self.order_filled_date = close_date
|
||||||
self.filled = self.amount
|
self.filled = self.amount
|
||||||
|
self.remaining = 0
|
||||||
self.status = 'closed'
|
self.status = 'closed'
|
||||||
self.ft_is_open = False
|
self.ft_is_open = False
|
||||||
if (self.ft_order_side == trade.entry_side
|
if (self.ft_order_side == trade.entry_side
|
||||||
|
@ -393,9 +401,9 @@ class LocalTrade():
|
||||||
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self, minified: bool = False) -> Dict[str, Any]:
|
||||||
filled_orders = self.select_filled_orders()
|
filled_orders = self.select_filled_or_open_orders()
|
||||||
orders = [order.to_json(self.entry_side) for order in filled_orders]
|
orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'trade_id': self.id,
|
'trade_id': self.id,
|
||||||
|
@ -619,8 +627,8 @@ class LocalTrade():
|
||||||
"""
|
"""
|
||||||
self.close_rate = rate
|
self.close_rate = rate
|
||||||
self.close_date = self.close_date or datetime.utcnow()
|
self.close_date = self.close_date or datetime.utcnow()
|
||||||
self.close_profit = self.calc_profit_ratio()
|
self.close_profit = self.calc_profit_ratio(rate)
|
||||||
self.close_profit_abs = self.calc_profit()
|
self.close_profit_abs = self.calc_profit(rate)
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
self.exit_order_status = 'closed'
|
self.exit_order_status = 'closed'
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
|
@ -688,10 +696,9 @@ class LocalTrade():
|
||||||
"""
|
"""
|
||||||
self.open_trade_value = self._calc_open_trade_value()
|
self.open_trade_value = self._calc_open_trade_value()
|
||||||
|
|
||||||
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
|
def calculate_interest(self) -> Decimal:
|
||||||
"""
|
"""
|
||||||
:param interest_rate: interest_charge for borrowing this coin(optional).
|
Calculate interest for this trade. Only applicable for Margin trading.
|
||||||
If interest_rate is not set self.interest_rate will be used
|
|
||||||
"""
|
"""
|
||||||
zero = Decimal(0.0)
|
zero = Decimal(0.0)
|
||||||
# If nothing was borrowed
|
# If nothing was borrowed
|
||||||
|
@ -704,34 +711,26 @@ class LocalTrade():
|
||||||
total_seconds = Decimal((now - open_date).total_seconds())
|
total_seconds = Decimal((now - open_date).total_seconds())
|
||||||
hours = total_seconds / sec_per_hour or zero
|
hours = total_seconds / sec_per_hour or zero
|
||||||
|
|
||||||
rate = Decimal(interest_rate or self.interest_rate)
|
rate = Decimal(self.interest_rate)
|
||||||
borrowed = Decimal(self.borrowed)
|
borrowed = Decimal(self.borrowed)
|
||||||
|
|
||||||
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
||||||
|
|
||||||
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
|
def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
|
||||||
fee: Optional[float] = None) -> Decimal:
|
|
||||||
|
|
||||||
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
|
close_trade = amount * Decimal(rate)
|
||||||
fees = close_trade * Decimal(fee or self.fee_close)
|
fees = close_trade * Decimal(fee)
|
||||||
|
|
||||||
if self.is_short:
|
if self.is_short:
|
||||||
return close_trade + fees
|
return close_trade + fees
|
||||||
else:
|
else:
|
||||||
return close_trade - fees
|
return close_trade - fees
|
||||||
|
|
||||||
def calc_close_trade_value(self, rate: Optional[float] = None,
|
def calc_close_trade_value(self, rate: float) -> float:
|
||||||
fee: Optional[float] = None,
|
|
||||||
interest_rate: Optional[float] = None) -> float:
|
|
||||||
"""
|
"""
|
||||||
Calculate the close_rate including fee
|
Calculate the Trade's close value including fees
|
||||||
:param fee: fee to use on the close rate (optional).
|
:param rate: rate to compare with.
|
||||||
If rate is not set self.fee will be used
|
:return: value in stake currency of the open trade
|
||||||
:param rate: rate to compare with (optional).
|
|
||||||
If rate is not set self.close_rate will be used
|
|
||||||
:param interest_rate: interest_charge for borrowing this coin (optional).
|
|
||||||
If interest_rate is not set self.interest_rate will be used
|
|
||||||
:return: Price in BTC of the open trade
|
|
||||||
"""
|
"""
|
||||||
if rate is None and not self.close_rate:
|
if rate is None and not self.close_rate:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
@ -740,49 +739,38 @@ class LocalTrade():
|
||||||
trading_mode = self.trading_mode or TradingMode.SPOT
|
trading_mode = self.trading_mode or TradingMode.SPOT
|
||||||
|
|
||||||
if trading_mode == TradingMode.SPOT:
|
if trading_mode == TradingMode.SPOT:
|
||||||
return float(self._calc_base_close(amount, rate, fee))
|
return float(self._calc_base_close(amount, rate, self.fee_close))
|
||||||
|
|
||||||
elif (trading_mode == TradingMode.MARGIN):
|
elif (trading_mode == TradingMode.MARGIN):
|
||||||
|
|
||||||
total_interest = self.calculate_interest(interest_rate)
|
total_interest = self.calculate_interest()
|
||||||
|
|
||||||
if self.is_short:
|
if self.is_short:
|
||||||
amount = amount + total_interest
|
amount = amount + total_interest
|
||||||
return float(self._calc_base_close(amount, rate, fee))
|
return float(self._calc_base_close(amount, rate, self.fee_close))
|
||||||
else:
|
else:
|
||||||
# Currency already owned for longs, no need to purchase
|
# Currency already owned for longs, no need to purchase
|
||||||
return float(self._calc_base_close(amount, rate, fee) - total_interest)
|
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
|
||||||
|
|
||||||
elif (trading_mode == TradingMode.FUTURES):
|
elif (trading_mode == TradingMode.FUTURES):
|
||||||
funding_fees = self.funding_fees or 0.0
|
funding_fees = self.funding_fees or 0.0
|
||||||
# Positive funding_fees -> Trade has gained from fees.
|
# Positive funding_fees -> Trade has gained from fees.
|
||||||
# Negative funding_fees -> Trade had to pay the fees.
|
# Negative funding_fees -> Trade had to pay the fees.
|
||||||
if self.is_short:
|
if self.is_short:
|
||||||
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
|
||||||
else:
|
else:
|
||||||
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
|
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
|
||||||
else:
|
else:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
||||||
|
|
||||||
def calc_profit(self, rate: Optional[float] = None,
|
def calc_profit(self, rate: float) -> float:
|
||||||
fee: Optional[float] = None,
|
|
||||||
interest_rate: Optional[float] = None) -> float:
|
|
||||||
"""
|
"""
|
||||||
Calculate the absolute profit in stake currency between Close and Open trade
|
Calculate the absolute profit in stake currency between Close and Open trade
|
||||||
:param fee: fee to use on the close rate (optional).
|
:param rate: close rate to compare with.
|
||||||
If fee is not set self.fee will be used
|
|
||||||
:param rate: close rate to compare with (optional).
|
|
||||||
If rate is not set self.close_rate will be used
|
|
||||||
:param interest_rate: interest_charge for borrowing this coin (optional).
|
|
||||||
If interest_rate is not set self.interest_rate will be used
|
|
||||||
:return: profit in stake currency as float
|
:return: profit in stake currency as float
|
||||||
"""
|
"""
|
||||||
close_trade_value = self.calc_close_trade_value(
|
close_trade_value = self.calc_close_trade_value(rate)
|
||||||
rate=(rate or self.close_rate),
|
|
||||||
fee=(fee or self.fee_close),
|
|
||||||
interest_rate=(interest_rate or self.interest_rate)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.is_short:
|
if self.is_short:
|
||||||
profit = self.open_trade_value - close_trade_value
|
profit = self.open_trade_value - close_trade_value
|
||||||
|
@ -790,23 +778,13 @@ class LocalTrade():
|
||||||
profit = close_trade_value - self.open_trade_value
|
profit = close_trade_value - self.open_trade_value
|
||||||
return float(f"{profit:.8f}")
|
return float(f"{profit:.8f}")
|
||||||
|
|
||||||
def calc_profit_ratio(self, rate: Optional[float] = None,
|
def calc_profit_ratio(self, rate: float) -> float:
|
||||||
fee: Optional[float] = None,
|
|
||||||
interest_rate: Optional[float] = None) -> float:
|
|
||||||
"""
|
"""
|
||||||
Calculates the profit as ratio (including fee).
|
Calculates the profit as ratio (including fee).
|
||||||
:param rate: rate to compare with (optional).
|
:param rate: rate to compare with.
|
||||||
If rate is not set self.close_rate will be used
|
|
||||||
:param fee: fee to use on the close rate (optional).
|
|
||||||
:param interest_rate: interest_charge for borrowing this coin (optional).
|
|
||||||
If interest_rate is not set self.interest_rate will be used
|
|
||||||
:return: profit ratio as float
|
:return: profit ratio as float
|
||||||
"""
|
"""
|
||||||
close_trade_value = self.calc_close_trade_value(
|
close_trade_value = self.calc_close_trade_value(rate)
|
||||||
rate=(rate or self.close_rate),
|
|
||||||
fee=(fee or self.fee_close),
|
|
||||||
interest_rate=(interest_rate or self.interest_rate)
|
|
||||||
)
|
|
||||||
|
|
||||||
short_close_zero = (self.is_short and close_trade_value == 0.0)
|
short_close_zero = (self.is_short and close_trade_value == 0.0)
|
||||||
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
|
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
|
||||||
|
@ -823,14 +801,6 @@ class LocalTrade():
|
||||||
return float(f"{profit_ratio:.8f}")
|
return float(f"{profit_ratio:.8f}")
|
||||||
|
|
||||||
def recalc_trade_from_orders(self):
|
def recalc_trade_from_orders(self):
|
||||||
# We need at least 2 entry orders for averaging amounts and rates.
|
|
||||||
# TODO: this condition could probably be removed
|
|
||||||
if len(self.select_filled_orders(self.entry_side)) < 2:
|
|
||||||
self.stake_amount = self.amount * self.open_rate / self.leverage
|
|
||||||
|
|
||||||
# Just in case, still recalc open trade value
|
|
||||||
self.recalc_open_trade_value()
|
|
||||||
return
|
|
||||||
|
|
||||||
total_amount = 0.0
|
total_amount = 0.0
|
||||||
total_stake = 0.0
|
total_stake = 0.0
|
||||||
|
@ -842,8 +812,6 @@ class LocalTrade():
|
||||||
|
|
||||||
tmp_amount = o.safe_amount_after_fee
|
tmp_amount = o.safe_amount_after_fee
|
||||||
tmp_price = o.average or o.price
|
tmp_price = o.average or o.price
|
||||||
if o.filled is not None:
|
|
||||||
tmp_amount = o.filled
|
|
||||||
if tmp_amount > 0.0 and tmp_price is not None:
|
if tmp_amount > 0.0 and tmp_price is not None:
|
||||||
total_amount += tmp_amount
|
total_amount += tmp_amount
|
||||||
total_stake += tmp_price * tmp_amount
|
total_stake += tmp_price * tmp_amount
|
||||||
|
@ -897,6 +865,21 @@ class LocalTrade():
|
||||||
(o.filled or 0) > 0 and
|
(o.filled or 0) > 0 and
|
||||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
o.status in NON_OPEN_EXCHANGE_STATES]
|
||||||
|
|
||||||
|
def select_filled_or_open_orders(self) -> List['Order']:
|
||||||
|
"""
|
||||||
|
Finds filled or open orders
|
||||||
|
:param order_side: Side of the order (either 'buy', 'sell', or None)
|
||||||
|
:return: array of Order objects
|
||||||
|
"""
|
||||||
|
return [o for o in self.orders if
|
||||||
|
(
|
||||||
|
o.ft_is_open is False
|
||||||
|
and (o.filled or 0) > 0
|
||||||
|
and o.status in NON_OPEN_EXCHANGE_STATES
|
||||||
|
)
|
||||||
|
or (o.ft_is_open is True and o.status is not None)
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nr_of_successful_entries(self) -> int:
|
def nr_of_successful_entries(self) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -1135,7 +1118,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_trades(trade_filter=None) -> Query:
|
def get_trades(trade_filter=None, include_orders: bool = True) -> Query:
|
||||||
"""
|
"""
|
||||||
Helper function to query Trades using filters.
|
Helper function to query Trades using filters.
|
||||||
NOTE: Not supported in Backtesting.
|
NOTE: Not supported in Backtesting.
|
||||||
|
@ -1150,9 +1133,14 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||||
if trade_filter is not None:
|
if trade_filter is not None:
|
||||||
if not isinstance(trade_filter, list):
|
if not isinstance(trade_filter, list):
|
||||||
trade_filter = [trade_filter]
|
trade_filter = [trade_filter]
|
||||||
return Trade.query.filter(*trade_filter)
|
this_query = Trade.query.filter(*trade_filter)
|
||||||
else:
|
else:
|
||||||
return Trade.query
|
this_query = Trade.query
|
||||||
|
if not include_orders:
|
||||||
|
# Don't load order relations
|
||||||
|
# Consider using noload or raiseload instead of lazyload
|
||||||
|
this_query = this_query.options(lazyload(Trade.orders))
|
||||||
|
return this_query
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_open_order_trades() -> List['Trade']:
|
def get_open_order_trades() -> List['Trade']:
|
||||||
|
@ -1372,3 +1360,18 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(desc('profit_sum')).first()
|
.order_by(desc('profit_sum')).first()
|
||||||
return best_pair
|
return best_pair
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float:
|
||||||
|
"""
|
||||||
|
Get Trade volume based on Orders
|
||||||
|
NOTE: Not supported in Backtesting.
|
||||||
|
:returns: Tuple containing (pair, profit_sum)
|
||||||
|
"""
|
||||||
|
trading_volume = Order.query.with_entities(
|
||||||
|
func.sum(Order.cost).label('volume')
|
||||||
|
).filter(
|
||||||
|
Order.order_filled_date >= start_date,
|
||||||
|
Order.status == 'closed'
|
||||||
|
).scalar()
|
||||||
|
return trading_volume
|
||||||
|
|
|
@ -30,7 +30,7 @@ class AgeFilter(IPairList):
|
||||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||||
|
|
||||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
self._max_days_listed = pairlistconfig.get('max_days_listed')
|
||||||
|
|
||||||
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
||||||
if self._min_days_listed < 1:
|
if self._min_days_listed < 1:
|
||||||
|
|
|
@ -21,7 +21,7 @@ class PerformanceFilter(IPairList):
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._minutes = pairlistconfig.get('minutes', 0)
|
self._minutes = pairlistconfig.get('minutes', 0)
|
||||||
self._min_profit = pairlistconfig.get('min_profit', None)
|
self._min_profit = pairlistconfig.get('min_profit')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
|
|
@ -27,7 +27,7 @@ class RangeStabilityFilter(IPairList):
|
||||||
|
|
||||||
self._days = pairlistconfig.get('lookback_days', 10)
|
self._days = pairlistconfig.get('lookback_days', 10)
|
||||||
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
||||||
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
|
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change')
|
||||||
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
||||||
self._def_candletype = self._config['candle_type_def']
|
self._def_candletype = self._config['candle_type_def']
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class PairListManager(LoggingMixin):
|
||||||
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||||
self._pairlist_handlers: List[IPairList] = []
|
self._pairlist_handlers: List[IPairList] = []
|
||||||
self._tickers_needed = False
|
self._tickers_needed = False
|
||||||
for pairlist_handler_config in self._config.get('pairlists', None):
|
for pairlist_handler_config in self._config.get('pairlists', []):
|
||||||
pairlist_handler = PairListResolver.load_pairlist(
|
pairlist_handler = PairListResolver.load_pairlist(
|
||||||
pairlist_handler_config['method'],
|
pairlist_handler_config['method'],
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||||
|
@ -102,7 +103,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||||
min_date=min_date, max_date=max_date)
|
min_date=min_date, max_date=max_date)
|
||||||
|
|
||||||
if btconfig.get('export', 'none') == 'trades':
|
if btconfig.get('export', 'none') == 'trades':
|
||||||
store_backtest_stats(btconfig['exportfilename'], ApiServer._bt.results)
|
store_backtest_stats(
|
||||||
|
btconfig['exportfilename'], ApiServer._bt.results,
|
||||||
|
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Backtest finished.")
|
logger.info("Backtest finished.")
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,10 @@ class Profit(BaseModel):
|
||||||
best_pair_profit_ratio: float
|
best_pair_profit_ratio: float
|
||||||
winning_trades: int
|
winning_trades: int
|
||||||
losing_trades: int
|
losing_trades: int
|
||||||
|
profit_factor: float
|
||||||
|
max_drawdown: float
|
||||||
|
max_drawdown_abs: float
|
||||||
|
trading_volume: Optional[float]
|
||||||
|
|
||||||
|
|
||||||
class SellReason(BaseModel):
|
class SellReason(BaseModel):
|
||||||
|
@ -120,6 +124,8 @@ class Stats(BaseModel):
|
||||||
class DailyRecord(BaseModel):
|
class DailyRecord(BaseModel):
|
||||||
date: date
|
date: date
|
||||||
abs_profit: float
|
abs_profit: float
|
||||||
|
rel_profit: float
|
||||||
|
starting_balance: float
|
||||||
fiat_value: float
|
fiat_value: float
|
||||||
trade_count: int
|
trade_count: int
|
||||||
|
|
||||||
|
@ -166,7 +172,7 @@ class ShowConfig(BaseModel):
|
||||||
trailing_stop_positive: Optional[float]
|
trailing_stop_positive: Optional[float]
|
||||||
trailing_stop_positive_offset: Optional[float]
|
trailing_stop_positive_offset: Optional[float]
|
||||||
trailing_only_offset_is_reached: Optional[bool]
|
trailing_only_offset_is_reached: Optional[bool]
|
||||||
unfilledtimeout: UnfilledTimeout
|
unfilledtimeout: Optional[UnfilledTimeout] # Empty in webserver mode
|
||||||
order_types: Optional[OrderTypes]
|
order_types: Optional[OrderTypes]
|
||||||
use_custom_stoploss: Optional[bool]
|
use_custom_stoploss: Optional[bool]
|
||||||
timeframe: Optional[str]
|
timeframe: Optional[str]
|
||||||
|
@ -277,6 +283,7 @@ class OpenTradeSchema(TradeSchema):
|
||||||
class TradeResponse(BaseModel):
|
class TradeResponse(BaseModel):
|
||||||
trades: List[TradeSchema]
|
trades: List[TradeSchema]
|
||||||
trades_count: int
|
trades_count: int
|
||||||
|
offset: int
|
||||||
total_trades: int
|
total_trades: int
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,8 @@ logger = logging.getLogger(__name__)
|
||||||
# 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
|
||||||
# 2.15: Add backtest history endpoints
|
# 2.15: Add backtest history endpoints
|
||||||
API_VERSION = 2.15
|
# 2.16: Additional daily metrics
|
||||||
|
API_VERSION = 2.16
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
@ -86,7 +87,7 @@ def stats(rpc: RPC = Depends(get_rpc)):
|
||||||
|
|
||||||
@router.get('/daily', response_model=Daily, tags=['info'])
|
@router.get('/daily', response_model=Daily, tags=['info'])
|
||||||
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||||
return rpc._rpc_daily_profit(timescale, config['stake_currency'],
|
return rpc._rpc_timeunit_profit(timescale, config['stake_currency'],
|
||||||
config.get('fiat_display_currency', ''))
|
config.get('fiat_display_currency', ''))
|
||||||
|
|
||||||
|
|
||||||
|
@ -281,7 +282,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
|
||||||
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
||||||
candletype: Optional[CandleType] = None, config=Depends(get_config)):
|
candletype: Optional[CandleType] = None, config=Depends(get_config)):
|
||||||
|
|
||||||
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
|
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv'))
|
||||||
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||||
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
|
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
|
||||||
|
|
||||||
|
|
59
freqtrade/rpc/discord.py
Normal file
59
freqtrade/rpc/discord.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.enums.rpcmessagetype import RPCMessageType
|
||||||
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.webhook import Webhook
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Discord(Webhook):
|
||||||
|
def __init__(self, rpc: 'RPC', config: Dict[str, Any]):
|
||||||
|
# super().__init__(rpc, config)
|
||||||
|
self.rpc = rpc
|
||||||
|
self.config = config
|
||||||
|
self.strategy = config.get('strategy', '')
|
||||||
|
self.timeframe = config.get('timeframe', '')
|
||||||
|
|
||||||
|
self._url = self.config['discord']['webhook_url']
|
||||||
|
self._format = 'json'
|
||||||
|
self._retries = 1
|
||||||
|
self._retry_delay = 0.1
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Cleanup pending module resources.
|
||||||
|
This will do nothing for webhooks, they will simply not be called anymore
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_msg(self, msg) -> None:
|
||||||
|
logger.info(f"Sending discord message: {msg}")
|
||||||
|
|
||||||
|
if msg['type'].value in self.config['discord']:
|
||||||
|
|
||||||
|
msg['strategy'] = self.strategy
|
||||||
|
msg['timeframe'] = self.timeframe
|
||||||
|
fields = self.config['discord'].get(msg['type'].value)
|
||||||
|
color = 0x0000FF
|
||||||
|
if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL):
|
||||||
|
profit_ratio = msg.get('profit_ratio')
|
||||||
|
color = (0x00FF00 if profit_ratio > 0 else 0xFF0000)
|
||||||
|
|
||||||
|
embeds = [{
|
||||||
|
'title': f"Trade: {msg['pair']} {msg['type'].value}",
|
||||||
|
'color': color,
|
||||||
|
'fields': [],
|
||||||
|
|
||||||
|
}]
|
||||||
|
for f in fields:
|
||||||
|
for k, v in f.items():
|
||||||
|
v = v.format(**msg)
|
||||||
|
embeds[0]['fields'].append( # type: ignore
|
||||||
|
{'name': k, 'value': v, 'inline': True})
|
||||||
|
|
||||||
|
# Send the message to discord channel
|
||||||
|
payload = {'embeds': embeds}
|
||||||
|
self._send_msg(payload)
|
|
@ -18,6 +18,7 @@ from freqtrade import __version__
|
||||||
from freqtrade.configuration.timerange import TimeRange
|
from freqtrade.configuration.timerange import TimeRange
|
||||||
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
|
from freqtrade.data.metrics import calculate_max_drawdown
|
||||||
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
|
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
|
||||||
TradingMode)
|
TradingMode)
|
||||||
from freqtrade.exceptions import ExchangeError, PricingError
|
from freqtrade.exceptions import ExchangeError, PricingError
|
||||||
|
@ -96,7 +97,7 @@ class RPC:
|
||||||
"""
|
"""
|
||||||
self._freqtrade = freqtrade
|
self._freqtrade = freqtrade
|
||||||
self._config: Dict[str, Any] = freqtrade.config
|
self._config: Dict[str, Any] = freqtrade.config
|
||||||
if self._config.get('fiat_display_currency', None):
|
if self._config.get('fiat_display_currency'):
|
||||||
self._fiat_converter = CryptoToFiatConverter()
|
self._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -283,33 +284,57 @@ class RPC:
|
||||||
columns.append('# Entries')
|
columns.append('# Entries')
|
||||||
return trades_list, columns, fiat_profit_sum
|
return trades_list, columns, fiat_profit_sum
|
||||||
|
|
||||||
def _rpc_daily_profit(
|
def _rpc_timeunit_profit(
|
||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
stake_currency: str, fiat_display_currency: str,
|
||||||
today = datetime.now(timezone.utc).date()
|
timeunit: str = 'days') -> Dict[str, Any]:
|
||||||
profit_days: Dict[date, Dict] = {}
|
"""
|
||||||
|
:param timeunit: Valid entries are 'days', 'weeks', 'months'
|
||||||
|
"""
|
||||||
|
start_date = datetime.now(timezone.utc).date()
|
||||||
|
if timeunit == 'weeks':
|
||||||
|
# weekly
|
||||||
|
start_date = start_date - timedelta(days=start_date.weekday()) # Monday
|
||||||
|
if timeunit == 'months':
|
||||||
|
start_date = start_date.replace(day=1)
|
||||||
|
|
||||||
|
def time_offset(step: int):
|
||||||
|
if timeunit == 'months':
|
||||||
|
return relativedelta(months=step)
|
||||||
|
return timedelta(**{timeunit: step})
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
raise RPCException('timescale must be an integer greater than 0')
|
raise RPCException('timescale must be an integer greater than 0')
|
||||||
|
|
||||||
|
profit_units: Dict[date, Dict] = {}
|
||||||
|
daily_stake = self._freqtrade.wallets.get_total_stake_amount()
|
||||||
|
|
||||||
for day in range(0, timescale):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = start_date - time_offset(day)
|
||||||
trades = Trade.get_trades(trade_filter=[
|
# Only query for necessary columns for performance reasons.
|
||||||
|
trades = Trade.query.session.query(Trade.close_profit_abs).filter(
|
||||||
Trade.is_open.is_(False),
|
Trade.is_open.is_(False),
|
||||||
Trade.close_date >= profitday,
|
Trade.close_date >= profitday,
|
||||||
Trade.close_date < (profitday + timedelta(days=1))
|
Trade.close_date < (profitday + time_offset(1))
|
||||||
]).order_by(Trade.close_date).all()
|
).order_by(Trade.close_date).all()
|
||||||
|
|
||||||
curdayprofit = sum(
|
curdayprofit = sum(
|
||||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||||
profit_days[profitday] = {
|
# Calculate this periods starting balance
|
||||||
|
daily_stake = daily_stake - curdayprofit
|
||||||
|
profit_units[profitday] = {
|
||||||
'amount': curdayprofit,
|
'amount': curdayprofit,
|
||||||
'trades': len(trades)
|
'daily_stake': daily_stake,
|
||||||
|
'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0,
|
||||||
|
'trades': len(trades),
|
||||||
}
|
}
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
'date': key,
|
'date': f"{key.year}-{key.month:02d}" if timeunit == 'months' else key,
|
||||||
'abs_profit': value["amount"],
|
'abs_profit': value["amount"],
|
||||||
|
'starting_balance': value["daily_stake"],
|
||||||
|
'rel_profit': value["rel_profit"],
|
||||||
'fiat_value': self._fiat_converter.convert_amount(
|
'fiat_value': self._fiat_converter.convert_amount(
|
||||||
value['amount'],
|
value['amount'],
|
||||||
stake_currency,
|
stake_currency,
|
||||||
|
@ -317,92 +342,7 @@ class RPC:
|
||||||
) if self._fiat_converter else 0,
|
) if self._fiat_converter else 0,
|
||||||
'trade_count': value["trades"],
|
'trade_count': value["trades"],
|
||||||
}
|
}
|
||||||
for key, value in profit_days.items()
|
for key, value in profit_units.items()
|
||||||
]
|
|
||||||
return {
|
|
||||||
'stake_currency': stake_currency,
|
|
||||||
'fiat_display_currency': fiat_display_currency,
|
|
||||||
'data': data
|
|
||||||
}
|
|
||||||
|
|
||||||
def _rpc_weekly_profit(
|
|
||||||
self, timescale: int,
|
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
|
||||||
today = datetime.now(timezone.utc).date()
|
|
||||||
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
|
|
||||||
profit_weeks: Dict[date, Dict] = {}
|
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
|
||||||
raise RPCException('timescale must be an integer greater than 0')
|
|
||||||
|
|
||||||
for week in range(0, timescale):
|
|
||||||
profitweek = first_iso_day_of_week - timedelta(weeks=week)
|
|
||||||
trades = Trade.get_trades(trade_filter=[
|
|
||||||
Trade.is_open.is_(False),
|
|
||||||
Trade.close_date >= profitweek,
|
|
||||||
Trade.close_date < (profitweek + timedelta(weeks=1))
|
|
||||||
]).order_by(Trade.close_date).all()
|
|
||||||
curweekprofit = sum(
|
|
||||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
|
||||||
profit_weeks[profitweek] = {
|
|
||||||
'amount': curweekprofit,
|
|
||||||
'trades': len(trades)
|
|
||||||
}
|
|
||||||
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
'date': key,
|
|
||||||
'abs_profit': value["amount"],
|
|
||||||
'fiat_value': self._fiat_converter.convert_amount(
|
|
||||||
value['amount'],
|
|
||||||
stake_currency,
|
|
||||||
fiat_display_currency
|
|
||||||
) if self._fiat_converter else 0,
|
|
||||||
'trade_count': value["trades"],
|
|
||||||
}
|
|
||||||
for key, value in profit_weeks.items()
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
'stake_currency': stake_currency,
|
|
||||||
'fiat_display_currency': fiat_display_currency,
|
|
||||||
'data': data
|
|
||||||
}
|
|
||||||
|
|
||||||
def _rpc_monthly_profit(
|
|
||||||
self, timescale: int,
|
|
||||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
|
||||||
first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
|
|
||||||
profit_months: Dict[date, Dict] = {}
|
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
|
||||||
raise RPCException('timescale must be an integer greater than 0')
|
|
||||||
|
|
||||||
for month in range(0, timescale):
|
|
||||||
profitmonth = first_day_of_month - relativedelta(months=month)
|
|
||||||
trades = Trade.get_trades(trade_filter=[
|
|
||||||
Trade.is_open.is_(False),
|
|
||||||
Trade.close_date >= profitmonth,
|
|
||||||
Trade.close_date < (profitmonth + relativedelta(months=1))
|
|
||||||
]).order_by(Trade.close_date).all()
|
|
||||||
curmonthprofit = sum(
|
|
||||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
|
||||||
profit_months[profitmonth] = {
|
|
||||||
'amount': curmonthprofit,
|
|
||||||
'trades': len(trades)
|
|
||||||
}
|
|
||||||
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
'date': f"{key.year}-{key.month:02d}",
|
|
||||||
'abs_profit': value["amount"],
|
|
||||||
'fiat_value': self._fiat_converter.convert_amount(
|
|
||||||
value['amount'],
|
|
||||||
stake_currency,
|
|
||||||
fiat_display_currency
|
|
||||||
) if self._fiat_converter else 0,
|
|
||||||
'trade_count': value["trades"],
|
|
||||||
}
|
|
||||||
for key, value in profit_months.items()
|
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
|
@ -425,6 +365,7 @@ class RPC:
|
||||||
return {
|
return {
|
||||||
"trades": output,
|
"trades": output,
|
||||||
"trades_count": len(output),
|
"trades_count": len(output),
|
||||||
|
"offset": offset,
|
||||||
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
|
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,7 +380,7 @@ class RPC:
|
||||||
return 'losses'
|
return 'losses'
|
||||||
else:
|
else:
|
||||||
return 'draws'
|
return 'draws'
|
||||||
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)])
|
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False)
|
||||||
# Sell reason
|
# Sell reason
|
||||||
exit_reasons = {}
|
exit_reasons = {}
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
@ -467,7 +408,8 @@ class RPC:
|
||||||
""" Returns cumulative profit statistics """
|
""" Returns cumulative profit statistics """
|
||||||
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
|
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
|
||||||
Trade.is_open.is_(True))
|
Trade.is_open.is_(True))
|
||||||
trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all()
|
trades: List[Trade] = Trade.get_trades(
|
||||||
|
trade_filter, include_orders=False).order_by(Trade.id).all()
|
||||||
|
|
||||||
profit_all_coin = []
|
profit_all_coin = []
|
||||||
profit_all_ratio = []
|
profit_all_ratio = []
|
||||||
|
@ -476,6 +418,8 @@ class RPC:
|
||||||
durations = []
|
durations = []
|
||||||
winning_trades = 0
|
winning_trades = 0
|
||||||
losing_trades = 0
|
losing_trades = 0
|
||||||
|
winning_profit = 0.0
|
||||||
|
losing_profit = 0.0
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
current_rate: float = 0.0
|
current_rate: float = 0.0
|
||||||
|
@ -491,8 +435,10 @@ class RPC:
|
||||||
profit_closed_ratio.append(profit_ratio)
|
profit_closed_ratio.append(profit_ratio)
|
||||||
if trade.close_profit >= 0:
|
if trade.close_profit >= 0:
|
||||||
winning_trades += 1
|
winning_trades += 1
|
||||||
|
winning_profit += trade.close_profit_abs
|
||||||
else:
|
else:
|
||||||
losing_trades += 1
|
losing_trades += 1
|
||||||
|
losing_profit += trade.close_profit_abs
|
||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
try:
|
try:
|
||||||
|
@ -508,6 +454,7 @@ class RPC:
|
||||||
profit_all_ratio.append(profit_ratio)
|
profit_all_ratio.append(profit_ratio)
|
||||||
|
|
||||||
best_pair = Trade.get_best_pair(start_date)
|
best_pair = Trade.get_best_pair(start_date)
|
||||||
|
trading_volume = Trade.get_trading_volume(start_date)
|
||||||
|
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||||
|
@ -531,6 +478,21 @@ class RPC:
|
||||||
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
|
||||||
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
|
||||||
|
|
||||||
|
profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf')
|
||||||
|
|
||||||
|
trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT),
|
||||||
|
'profit_abs': trade.close_profit_abs}
|
||||||
|
for trade in trades if not trade.is_open])
|
||||||
|
max_drawdown_abs = 0.0
|
||||||
|
max_drawdown = 0.0
|
||||||
|
if len(trades_df) > 0:
|
||||||
|
try:
|
||||||
|
(max_drawdown_abs, _, _, _, _, max_drawdown) = calculate_max_drawdown(
|
||||||
|
trades_df, value_col='profit_abs', starting_balance=starting_balance)
|
||||||
|
except ValueError:
|
||||||
|
# ValueError if no losing trade.
|
||||||
|
pass
|
||||||
|
|
||||||
profit_all_fiat = self._fiat_converter.convert_amount(
|
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_all_coin_sum,
|
profit_all_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
|
@ -569,11 +531,15 @@ class RPC:
|
||||||
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
|
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
|
||||||
'winning_trades': winning_trades,
|
'winning_trades': winning_trades,
|
||||||
'losing_trades': losing_trades,
|
'losing_trades': losing_trades,
|
||||||
|
'profit_factor': profit_factor,
|
||||||
|
'max_drawdown': max_drawdown,
|
||||||
|
'max_drawdown_abs': max_drawdown_abs,
|
||||||
|
'trading_volume': trading_volume,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||||
""" Returns current account balance per crypto """
|
""" Returns current account balance per crypto """
|
||||||
currencies = []
|
currencies: List[Dict] = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
try:
|
try:
|
||||||
tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
||||||
|
@ -600,7 +566,7 @@ class RPC:
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||||
rate = tickers.get(pair, {}).get('last', None)
|
rate = tickers.get(pair, {}).get('last')
|
||||||
if rate:
|
if rate:
|
||||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||||
rate = 1.0 / rate
|
rate = 1.0 / rate
|
||||||
|
@ -608,13 +574,12 @@ class RPC:
|
||||||
except (ExchangeError):
|
except (ExchangeError):
|
||||||
logger.warning(f" Could not get rate for pair {coin}.")
|
logger.warning(f" Could not get rate for pair {coin}.")
|
||||||
continue
|
continue
|
||||||
total = total + (est_stake or 0)
|
total = total + est_stake
|
||||||
currencies.append({
|
currencies.append({
|
||||||
'currency': coin,
|
'currency': coin,
|
||||||
# TODO: The below can be simplified if we don't assign None to values.
|
'free': balance.free,
|
||||||
'free': balance.free if balance.free is not None else 0,
|
'balance': balance.total,
|
||||||
'balance': balance.total if balance.total is not None else 0,
|
'used': balance.used,
|
||||||
'used': balance.used if balance.used is not None else 0,
|
|
||||||
'est_stake': est_stake or 0,
|
'est_stake': est_stake or 0,
|
||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
'side': 'long',
|
'side': 'long',
|
||||||
|
@ -644,7 +609,6 @@ class RPC:
|
||||||
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
|
|
||||||
trade_count = len(Trade.get_trades_proxy())
|
trade_count = len(Trade.get_trades_proxy())
|
||||||
starting_capital_ratio = 0.0
|
|
||||||
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,12 @@ class RPCManager:
|
||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.telegram import Telegram
|
||||||
self.registered_modules.append(Telegram(self._rpc, config))
|
self.registered_modules.append(Telegram(self._rpc, config))
|
||||||
|
|
||||||
|
# Enable discord
|
||||||
|
if config.get('discord', {}).get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.discord ...')
|
||||||
|
from freqtrade.rpc.discord import Discord
|
||||||
|
self.registered_modules.append(Discord(self._rpc, config))
|
||||||
|
|
||||||
# Enable Webhook
|
# Enable Webhook
|
||||||
if config.get('webhook', {}).get('enabled', False):
|
if config.get('webhook', {}).get('enabled', False):
|
||||||
logger.info('Enabling rpc.webhook ...')
|
logger.info('Enabling rpc.webhook ...')
|
||||||
|
|
|
@ -6,6 +6,7 @@ This module manage Telegram communication
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from html import escape
|
from html import escape
|
||||||
|
@ -37,6 +38,15 @@ logger.debug('Included module rpc.telegram ...')
|
||||||
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TimeunitMappings:
|
||||||
|
header: str
|
||||||
|
message: str
|
||||||
|
message2: str
|
||||||
|
callback: str
|
||||||
|
default: int
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
|
@ -225,6 +235,14 @@ class Telegram(RPCHandler):
|
||||||
# This can take up to `timeout` from the call to `start_polling`.
|
# This can take up to `timeout` from the call to `start_polling`.
|
||||||
self._updater.stop()
|
self._updater.stop()
|
||||||
|
|
||||||
|
def _exchange_from_msg(self, msg: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Extracts the exchange name from the given message.
|
||||||
|
:param msg: The message to extract the exchange name from.
|
||||||
|
:return: The exchange name.
|
||||||
|
"""
|
||||||
|
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
|
||||||
|
|
||||||
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
|
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
|
||||||
if self._rpc._fiat_converter:
|
if self._rpc._fiat_converter:
|
||||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
|
@ -237,11 +255,11 @@ class Telegram(RPCHandler):
|
||||||
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
|
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
|
||||||
else {'enter': 'Short', 'entered': 'Shorted'})
|
else {'enter': 'Short', 'entered': 'Shorted'})
|
||||||
message = (
|
message = (
|
||||||
f"{emoji} *{msg['exchange']}:*"
|
f"{emoji} *{self._exchange_from_msg(msg)}:*"
|
||||||
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
||||||
f" (#{msg['trade_id']})\n"
|
f" (#{msg['trade_id']})\n"
|
||||||
)
|
)
|
||||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
||||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
||||||
message += f"*Leverage:* `{msg['leverage']}`\n"
|
message += f"*Leverage:* `{msg['leverage']}`\n"
|
||||||
|
@ -254,7 +272,7 @@ class Telegram(RPCHandler):
|
||||||
|
|
||||||
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||||
|
|
||||||
if msg.get('fiat_currency', None):
|
if msg.get('fiat_currency'):
|
||||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||||
|
|
||||||
message += ")`"
|
message += ")`"
|
||||||
|
@ -270,7 +288,7 @@ class Telegram(RPCHandler):
|
||||||
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
||||||
msg['emoji'] = self._get_sell_emoji(msg)
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
||||||
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
|
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
|
||||||
else "")
|
else "")
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
|
@ -286,7 +304,7 @@ class Telegram(RPCHandler):
|
||||||
msg['profit_extra'] = ''
|
msg['profit_extra'] = ''
|
||||||
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
is_fill = msg['type'] == RPCMessageType.EXIT_FILL
|
||||||
message = (
|
message = (
|
||||||
f"{msg['emoji']} *{msg['exchange']}:* "
|
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
||||||
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||||
|
@ -316,33 +334,33 @@ class Telegram(RPCHandler):
|
||||||
|
|
||||||
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
|
elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
|
||||||
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
|
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
|
||||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* "
|
||||||
"Cancelling {message_side} Order for {pair} (#{trade_id}). "
|
f"Cancelling {msg['message_side']} Order for {msg['pair']} "
|
||||||
"Reason: {reason}.".format(**msg))
|
f"(#{msg['trade_id']}). Reason: {msg['reason']}.")
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||||
message = (
|
message = (
|
||||||
"*Protection* triggered due to {reason}. "
|
f"*Protection* triggered due to {msg['reason']}. "
|
||||||
"`{pair}` will be locked until `{lock_end_time}`."
|
f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`."
|
||||||
).format(**msg)
|
)
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||||
message = (
|
message = (
|
||||||
"*Protection* triggered due to {reason}. "
|
f"*Protection* triggered due to {msg['reason']}. "
|
||||||
"*All pairs* will be locked until `{lock_end_time}`."
|
f"*All pairs* will be locked until `{msg['lock_end_time']}`."
|
||||||
).format(**msg)
|
)
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STATUS:
|
elif msg_type == RPCMessageType.STATUS:
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
message = f"*Status:* `{msg['status']}`"
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.WARNING:
|
elif msg_type == RPCMessageType.WARNING:
|
||||||
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
|
message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`"
|
||||||
|
|
||||||
elif msg_type == RPCMessageType.STARTUP:
|
elif msg_type == RPCMessageType.STARTUP:
|
||||||
message = '{status}'.format(**msg)
|
message = f"{msg['status']}"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
|
raise NotImplementedError(f"Unknown message type: {msg_type}")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def send_msg(self, msg: Dict[str, Any]) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
|
@ -396,7 +414,7 @@ class Telegram(RPCHandler):
|
||||||
first_avg = filled_orders[0]["safe_price"]
|
first_avg = filled_orders[0]["safe_price"]
|
||||||
|
|
||||||
for x, order in enumerate(filled_orders):
|
for x, order in enumerate(filled_orders):
|
||||||
if not order['ft_is_entry']:
|
if not order['ft_is_entry'] or order['is_open'] is True:
|
||||||
continue
|
continue
|
||||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||||
cur_entry_amount = order["amount"]
|
cur_entry_amount = order["amount"]
|
||||||
|
@ -563,6 +581,60 @@ class Telegram(RPCHandler):
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /daily <n>
|
||||||
|
Returns a daily profit (in BTC) over the last n days.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7),
|
||||||
|
'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)',
|
||||||
|
'update_weekly', 8),
|
||||||
|
'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6),
|
||||||
|
}
|
||||||
|
val = vals[unit]
|
||||||
|
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||||
|
try:
|
||||||
|
timescale = int(context.args[0]) if context.args else val.default
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
timescale = val.default
|
||||||
|
try:
|
||||||
|
stats = self._rpc._rpc_timeunit_profit(
|
||||||
|
timescale,
|
||||||
|
stake_cur,
|
||||||
|
fiat_disp_cur,
|
||||||
|
unit
|
||||||
|
)
|
||||||
|
stats_tab = tabulate(
|
||||||
|
[[f"{period['date']} ({period['trade_count']})",
|
||||||
|
f"{round_coin_value(period['abs_profit'], stats['stake_currency'])}",
|
||||||
|
f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}",
|
||||||
|
f"{period['rel_profit']:.2%}",
|
||||||
|
] for period in stats['data']],
|
||||||
|
headers=[
|
||||||
|
f"{val.header} (count)",
|
||||||
|
f'{stake_cur}',
|
||||||
|
f'{fiat_disp_cur}',
|
||||||
|
'Profit %',
|
||||||
|
'Trades',
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = (
|
||||||
|
f'<b>{val.message} Profit over the last {timescale} {val.message2}</b>:\n'
|
||||||
|
f'<pre>{stats_tab}</pre>'
|
||||||
|
)
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||||
|
callback_path=val.callback, query=update.callback_query)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _daily(self, update: Update, context: CallbackContext) -> None:
|
def _daily(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -572,35 +644,7 @@ class Telegram(RPCHandler):
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'days')
|
||||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
|
||||||
try:
|
|
||||||
timescale = int(context.args[0]) if context.args else 7
|
|
||||||
except (TypeError, ValueError, IndexError):
|
|
||||||
timescale = 7
|
|
||||||
try:
|
|
||||||
stats = self._rpc._rpc_daily_profit(
|
|
||||||
timescale,
|
|
||||||
stake_cur,
|
|
||||||
fiat_disp_cur
|
|
||||||
)
|
|
||||||
stats_tab = tabulate(
|
|
||||||
[[day['date'],
|
|
||||||
f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
|
|
||||||
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
|
||||||
f"{day['trade_count']} trades"] for day in stats['data']],
|
|
||||||
headers=[
|
|
||||||
'Day',
|
|
||||||
f'Profit {stake_cur}',
|
|
||||||
f'Profit {fiat_disp_cur}',
|
|
||||||
'Trades',
|
|
||||||
],
|
|
||||||
tablefmt='simple')
|
|
||||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
|
||||||
callback_path="update_daily", query=update.callback_query)
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -611,36 +655,7 @@ class Telegram(RPCHandler):
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'weeks')
|
||||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
|
||||||
try:
|
|
||||||
timescale = int(context.args[0]) if context.args else 8
|
|
||||||
except (TypeError, ValueError, IndexError):
|
|
||||||
timescale = 8
|
|
||||||
try:
|
|
||||||
stats = self._rpc._rpc_weekly_profit(
|
|
||||||
timescale,
|
|
||||||
stake_cur,
|
|
||||||
fiat_disp_cur
|
|
||||||
)
|
|
||||||
stats_tab = tabulate(
|
|
||||||
[[week['date'],
|
|
||||||
f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}",
|
|
||||||
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
|
||||||
f"{week['trade_count']} trades"] for week in stats['data']],
|
|
||||||
headers=[
|
|
||||||
'Monday',
|
|
||||||
f'Profit {stake_cur}',
|
|
||||||
f'Profit {fiat_disp_cur}',
|
|
||||||
'Trades',
|
|
||||||
],
|
|
||||||
tablefmt='simple')
|
|
||||||
message = f'<b>Weekly Profit over the last {timescale} weeks ' \
|
|
||||||
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
|
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
|
||||||
callback_path="update_weekly", query=update.callback_query)
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -651,36 +666,7 @@ class Telegram(RPCHandler):
|
||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_cur = self._config['stake_currency']
|
self._timeunit_stats(update, context, 'months')
|
||||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
|
||||||
try:
|
|
||||||
timescale = int(context.args[0]) if context.args else 6
|
|
||||||
except (TypeError, ValueError, IndexError):
|
|
||||||
timescale = 6
|
|
||||||
try:
|
|
||||||
stats = self._rpc._rpc_monthly_profit(
|
|
||||||
timescale,
|
|
||||||
stake_cur,
|
|
||||||
fiat_disp_cur
|
|
||||||
)
|
|
||||||
stats_tab = tabulate(
|
|
||||||
[[month['date'],
|
|
||||||
f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}",
|
|
||||||
f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
|
||||||
f"{month['trade_count']} trades"] for month in stats['data']],
|
|
||||||
headers=[
|
|
||||||
'Month',
|
|
||||||
f'Profit {stake_cur}',
|
|
||||||
f'Profit {fiat_disp_cur}',
|
|
||||||
'Trades',
|
|
||||||
],
|
|
||||||
tablefmt='simple')
|
|
||||||
message = f'<b>Monthly Profit over the last {timescale} months' \
|
|
||||||
f'</b>:\n<pre>{stats_tab}</pre> '
|
|
||||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
|
||||||
callback_path="update_monthly", query=update.callback_query)
|
|
||||||
except RPCException as e:
|
|
||||||
self._send_msg(str(e))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -744,12 +730,18 @@ class Telegram(RPCHandler):
|
||||||
f"*Total Trade Count:* `{trade_count}`\n"
|
f"*Total Trade Count:* `{trade_count}`\n"
|
||||||
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
|
f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* "
|
||||||
f"`{first_trade_date}`\n"
|
f"`{first_trade_date}`\n"
|
||||||
f"*Latest Trade opened:* `{latest_trade_date}\n`"
|
f"*Latest Trade opened:* `{latest_trade_date}`\n"
|
||||||
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
|
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
|
||||||
)
|
)
|
||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
markdown_msg += (
|
||||||
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`")
|
f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||||
|
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n"
|
||||||
|
f"*Trading volume:* `{round_coin_value(stats['trading_volume'], stake_cur)}`\n"
|
||||||
|
f"*Profit factor:* `{stats['profit_factor']:.2f}`\n"
|
||||||
|
f"*Max Drawdown:* `{stats['max_drawdown']:.2%} "
|
||||||
|
f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`"
|
||||||
|
)
|
||||||
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
|
|
||||||
|
@ -889,7 +881,7 @@ class Telegram(RPCHandler):
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc._rpc_start()
|
msg = self._rpc._rpc_start()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg(f"Status: `{msg['status']}`")
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _stop(self, update: Update, context: CallbackContext) -> None:
|
def _stop(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -901,7 +893,7 @@ class Telegram(RPCHandler):
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc._rpc_stop()
|
msg = self._rpc._rpc_stop()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg(f"Status: `{msg['status']}`")
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _reload_config(self, update: Update, context: CallbackContext) -> None:
|
def _reload_config(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -913,7 +905,7 @@ class Telegram(RPCHandler):
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc._rpc_reload_config()
|
msg = self._rpc._rpc_reload_config()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg(f"Status: `{msg['status']}`")
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
|
def _stopbuy(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -925,7 +917,7 @@ class Telegram(RPCHandler):
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc._rpc_stopbuy()
|
msg = self._rpc._rpc_stopbuy()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg(f"Status: `{msg['status']}`")
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
@ -1087,9 +1079,9 @@ class Telegram(RPCHandler):
|
||||||
trade_id = int(context.args[0])
|
trade_id = int(context.args[0])
|
||||||
msg = self._rpc._rpc_delete(trade_id)
|
msg = self._rpc._rpc_delete(trade_id)
|
||||||
self._send_msg((
|
self._send_msg((
|
||||||
'`{result_msg}`\n'
|
f"`{msg['result_msg']}`\n"
|
||||||
'Please make sure to take care of this asset on the exchange manually.'
|
'Please make sure to take care of this asset on the exchange manually.'
|
||||||
).format(**msg))
|
))
|
||||||
|
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
@ -1417,7 +1409,7 @@ class Telegram(RPCHandler):
|
||||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||||
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
||||||
"regardless of profit`\n"
|
"regardless of profit`\n"
|
||||||
"*/fe <trade_id>|all:* `Alias to /forceexit`\n"
|
"*/fx <trade_id>|all:* `Alias to /forceexit`\n"
|
||||||
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
|
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
|
||||||
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||||
"*/whitelist:* `Show current whitelist` \n"
|
"*/whitelist:* `Show current whitelist` \n"
|
||||||
|
|
|
@ -45,21 +45,21 @@ class Webhook(RPCHandler):
|
||||||
try:
|
try:
|
||||||
whconfig = self._config['webhook']
|
whconfig = self._config['webhook']
|
||||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||||
valuedict = whconfig.get('webhookentry', None)
|
valuedict = whconfig.get('webhookentry')
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
||||||
valuedict = whconfig.get('webhookentrycancel', None)
|
valuedict = whconfig.get('webhookentrycancel')
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
||||||
valuedict = whconfig.get('webhookentryfill', None)
|
valuedict = whconfig.get('webhookentryfill')
|
||||||
elif msg['type'] == RPCMessageType.EXIT:
|
elif msg['type'] == RPCMessageType.EXIT:
|
||||||
valuedict = whconfig.get('webhookexit', None)
|
valuedict = whconfig.get('webhookexit')
|
||||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||||
valuedict = whconfig.get('webhookexitfill', None)
|
valuedict = whconfig.get('webhookexitfill')
|
||||||
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
||||||
valuedict = whconfig.get('webhookexitcancel', None)
|
valuedict = whconfig.get('webhookexitcancel')
|
||||||
elif msg['type'] in (RPCMessageType.STATUS,
|
elif msg['type'] in (RPCMessageType.STATUS,
|
||||||
RPCMessageType.STARTUP,
|
RPCMessageType.STARTUP,
|
||||||
RPCMessageType.WARNING):
|
RPCMessageType.WARNING):
|
||||||
valuedict = whconfig.get('webhookstatus', None)
|
valuedict = whconfig.get('webhookstatus')
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||||
if not valuedict:
|
if not valuedict:
|
||||||
|
|
|
@ -289,6 +289,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in target (base) currency that's going to be traded.
|
:param amount: Amount in target (base) currency that's going to be traded.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
or current rate for market orders.
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
|
@ -316,6 +317,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in base currency.
|
:param amount: Amount in base currency.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
or current rate for market orders.
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param exit_reason: Exit reason.
|
:param exit_reason: Exit reason.
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
@ -509,8 +511,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
return current_order_rate
|
return current_order_rate
|
||||||
|
|
||||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_leverage: float, max_leverage: float, side: str,
|
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
|
||||||
**kwargs) -> float:
|
side: str, **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Customize leverage for each new trade. This method is only called in futures mode.
|
Customize leverage for each new trade. This method is only called in futures mode.
|
||||||
|
|
||||||
|
@ -519,6 +521,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
:param proposed_leverage: A leverage proposed by the bot.
|
:param proposed_leverage: A leverage proposed by the bot.
|
||||||
:param max_leverage: Max leverage allowed on this pair
|
:param max_leverage: Max leverage allowed on this pair
|
||||||
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -161,6 +161,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in target (base) currency that's going to be traded.
|
:param amount: Amount in target (base) currency that's going to be traded.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
or current rate for market orders.
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
|
@ -188,6 +189,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in base currency.
|
:param amount: Amount in base currency.
|
||||||
:param rate: Rate that's going to be used when using limit orders
|
:param rate: Rate that's going to be used when using limit orders
|
||||||
|
or current rate for market orders.
|
||||||
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
|
||||||
:param exit_reason: Exit reason.
|
:param exit_reason: Exit reason.
|
||||||
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
|
||||||
|
@ -267,8 +269,8 @@ def adjust_trade_position(self, trade: 'Trade', current_time: 'datetime',
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_leverage: float, max_leverage: float, side: str,
|
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
|
||||||
**kwargs) -> float:
|
side: str, **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Customize leverage for each new trade. This method is only called in futures mode.
|
Customize leverage for each new trade. This method is only called in futures mode.
|
||||||
|
|
||||||
|
@ -277,6 +279,7 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
|
||||||
:param proposed_leverage: A leverage proposed by the bot.
|
:param proposed_leverage: A leverage proposed by the bot.
|
||||||
:param max_leverage: Max leverage allowed on this pair
|
:param max_leverage: Max leverage allowed on this pair
|
||||||
|
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -131,9 +131,9 @@ class Wallets:
|
||||||
if isinstance(balances[currency], dict):
|
if isinstance(balances[currency], dict):
|
||||||
self._wallets[currency] = Wallet(
|
self._wallets[currency] = Wallet(
|
||||||
currency,
|
currency,
|
||||||
balances[currency].get('free', None),
|
balances[currency].get('free'),
|
||||||
balances[currency].get('used', None),
|
balances[currency].get('used'),
|
||||||
balances[currency].get('total', None)
|
balances[currency].get('total')
|
||||||
)
|
)
|
||||||
# Remove currencies no longer in get_balances output
|
# Remove currencies no longer in get_balances output
|
||||||
for currency in deepcopy(self._wallets):
|
for currency in deepcopy(self._wallets):
|
||||||
|
|
|
@ -7,23 +7,23 @@
|
||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.960
|
mypy==0.961
|
||||||
pre-commit==2.19.0
|
pre-commit==2.19.0
|
||||||
pytest==7.1.2
|
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.8.1
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
isort==5.10.1
|
isort==5.10.1
|
||||||
# For datetime mocking
|
# For datetime mocking
|
||||||
time-machine==2.7.0
|
time-machine==2.7.1
|
||||||
|
|
||||||
# Convert jupyter notebooks to markdown documents
|
# Convert jupyter notebooks to markdown documents
|
||||||
nbconvert==6.5.0
|
nbconvert==6.5.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.0.1
|
types-cachetools==5.2.1
|
||||||
types-filelock==3.2.6
|
types-filelock==3.2.7
|
||||||
types-requests==2.27.29
|
types-requests==2.28.0
|
||||||
types-tabulate==0.8.9
|
types-tabulate==0.8.11
|
||||||
types-python-dateutil==2.8.17
|
types-python-dateutil==2.8.18
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
scipy==1.8.1
|
scipy==1.8.1
|
||||||
scikit-learn==1.1.1
|
scikit-learn==1.1.1
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.7.0
|
filelock==3.7.1
|
||||||
progressbar2==4.0.0
|
progressbar2==4.0.0
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.8.0
|
plotly==5.9.0
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
numpy==1.22.4
|
numpy==1.23.0
|
||||||
pandas==1.4.2
|
pandas==1.4.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.84.39
|
ccxt==1.89.14
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==37.0.2
|
cryptography==37.0.2
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
SQLAlchemy==1.4.36
|
SQLAlchemy==1.4.39
|
||||||
python-telegram-bot==13.12
|
python-telegram-bot==13.12
|
||||||
arrow==1.2.2
|
arrow==1.2.2
|
||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.27.1
|
requests==2.28.0
|
||||||
urllib3==1.26.9
|
urllib3==1.26.9
|
||||||
jsonschema==4.5.1
|
jsonschema==4.6.0
|
||||||
TA-Lib==0.4.24
|
TA-Lib==0.4.24
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
tabulate==0.8.9
|
tabulate==0.8.10
|
||||||
pycoingecko==2.2.0
|
pycoingecko==2.2.0
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
tables==3.7.0
|
tables==3.7.0
|
||||||
|
@ -28,20 +28,20 @@ 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
|
# Properly format api responses
|
||||||
orjson==3.6.8
|
orjson==3.7.3
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.78.0
|
fastapi==0.78.0
|
||||||
uvicorn==0.17.6
|
uvicorn==0.18.1
|
||||||
pyjwt==2.4.0
|
pyjwt==2.4.0
|
||||||
aiofiles==0.8.0
|
aiofiles==0.8.0
|
||||||
psutil==5.9.1
|
psutil==5.9.1
|
||||||
|
|
||||||
# Support for colorized terminal output
|
# Support for colorized terminal output
|
||||||
colorama==0.4.4
|
colorama==0.4.5
|
||||||
# Building config files interactively
|
# Building config files interactively
|
||||||
questionary==1.10.0
|
questionary==1.10.0
|
||||||
prompt-toolkit==3.0.29
|
prompt-toolkit==3.0.29
|
||||||
|
|
|
@ -261,7 +261,7 @@ class FtRestClient():
|
||||||
}
|
}
|
||||||
return self._post("forcebuy", data=data)
|
return self._post("forcebuy", data=data)
|
||||||
|
|
||||||
def force_enter(self, pair, side, price=None):
|
def forceenter(self, pair, side, price=None):
|
||||||
"""Force entering a trade
|
"""Force entering a trade
|
||||||
|
|
||||||
:param pair: Pair to buy (ETH/BTC)
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
|
@ -273,7 +273,7 @@ class FtRestClient():
|
||||||
"side": side,
|
"side": side,
|
||||||
"price": price,
|
"price": price,
|
||||||
}
|
}
|
||||||
return self._post("force_enter", data=data)
|
return self._post("forceenter", data=data)
|
||||||
|
|
||||||
def forceexit(self, tradeid):
|
def forceexit(self, tradeid):
|
||||||
"""Force-exit a trade.
|
"""Force-exit a trade.
|
||||||
|
|
4
setup.sh
4
setup.sh
|
@ -87,6 +87,10 @@ function updateenv() {
|
||||||
echo "Failed installing Freqtrade"
|
echo "Failed installing Freqtrade"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Installing freqUI"
|
||||||
|
freqtrade install-ui
|
||||||
|
|
||||||
echo "pip install completed"
|
echo "pip install completed"
|
||||||
echo
|
echo
|
||||||
if [[ $dev =~ ^[Yy]$ ]]; then
|
if [[ $dev =~ ^[Yy]$ ]]; then
|
||||||
|
|
|
@ -78,8 +78,20 @@ def get_args(args):
|
||||||
|
|
||||||
|
|
||||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||||
def get_mock_coro(return_value):
|
# TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped.
|
||||||
|
def get_mock_coro(return_value=None, side_effect=None):
|
||||||
async def mock_coro(*args, **kwargs):
|
async def mock_coro(*args, **kwargs):
|
||||||
|
if side_effect:
|
||||||
|
if isinstance(side_effect, list):
|
||||||
|
effect = side_effect.pop(0)
|
||||||
|
else:
|
||||||
|
effect = side_effect
|
||||||
|
if isinstance(effect, Exception):
|
||||||
|
raise effect
|
||||||
|
if callable(effect):
|
||||||
|
return effect(*args, **kwargs)
|
||||||
|
return effect
|
||||||
|
else:
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
return Mock(wraps=mock_coro)
|
return Mock(wraps=mock_coro)
|
||||||
|
@ -325,7 +337,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
||||||
Trade.query.session.flush()
|
Trade.query.session.flush()
|
||||||
|
|
||||||
|
|
||||||
def create_mock_trades_usdt(fee, use_db: bool = True):
|
def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True):
|
||||||
"""
|
"""
|
||||||
Create some fake trades ...
|
Create some fake trades ...
|
||||||
"""
|
"""
|
||||||
|
@ -335,26 +347,29 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
|
||||||
else:
|
else:
|
||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
|
||||||
|
is_short1 = is_short if is_short is not None else True
|
||||||
|
is_short2 = is_short if is_short is not None else False
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = mock_trade_usdt_1(fee)
|
trade = mock_trade_usdt_1(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_2(fee)
|
trade = mock_trade_usdt_2(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_3(fee)
|
trade = mock_trade_usdt_3(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_4(fee)
|
trade = mock_trade_usdt_4(fee, is_short2)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_5(fee)
|
trade = mock_trade_usdt_5(fee, is_short2)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_6(fee)
|
trade = mock_trade_usdt_6(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_usdt_7(fee)
|
trade = mock_trade_usdt_7(fee, is_short1)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
if use_db:
|
if use_db:
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
|
@ -29,6 +29,7 @@ def mock_order_1(is_short: bool):
|
||||||
'average': 0.123,
|
'average': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ def mock_order_2(is_short: bool):
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +81,7 @@ def mock_order_2_sell(is_short: bool):
|
||||||
'price': 0.128,
|
'price': 0.128,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +129,7 @@ def mock_order_3(is_short: bool):
|
||||||
'price': 0.05,
|
'price': 0.05,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,6 +145,7 @@ def mock_order_3_sell(is_short: bool):
|
||||||
'average': 0.06,
|
'average': 0.06,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +191,7 @@ def mock_order_4(is_short: bool):
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 123.0,
|
'remaining': 123.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,6 +231,7 @@ def mock_order_5(is_short: bool):
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +246,7 @@ def mock_order_5_stoploss(is_short: bool):
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
|
'cost': 0.0,
|
||||||
'remaining': 123.0,
|
'remaining': 123.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,6 +289,7 @@ def mock_order_6(is_short: bool):
|
||||||
'price': 0.15,
|
'price': 0.15,
|
||||||
'amount': 2.0,
|
'amount': 2.0,
|
||||||
'filled': 2.0,
|
'filled': 2.0,
|
||||||
|
'cost': 0.3,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +304,7 @@ def mock_order_6_sell(is_short: bool):
|
||||||
'price': 0.15 if is_short else 0.20,
|
'price': 0.15 if is_short else 0.20,
|
||||||
'amount': 2.0,
|
'amount': 2.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
|
'cost': 0.0,
|
||||||
'remaining': 2.0,
|
'remaining': 2.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,6 +347,7 @@ def short_order():
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.129,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,6 +362,7 @@ def exit_short_order():
|
||||||
'price': 0.128,
|
'price': 0.128,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
|
'cost': 15.744,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -424,6 +436,7 @@ def leverage_order():
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
|
'cost': 15.129,
|
||||||
'leverage': 5.0
|
'leverage': 5.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,6 +452,7 @@ def leverage_order_sell():
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
'filled': 123.0,
|
'filled': 123.0,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
|
'cost': 15.744,
|
||||||
'leverage': 5.0
|
'leverage': 5.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,47 +6,84 @@ from freqtrade.persistence.models import Order, Trade
|
||||||
MOCK_TRADE_COUNT = 6
|
MOCK_TRADE_COUNT = 6
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_1():
|
def entry_side(is_short: bool):
|
||||||
|
return "sell" if is_short else "buy"
|
||||||
|
|
||||||
|
|
||||||
|
def exit_side(is_short: bool):
|
||||||
|
return "buy" if is_short else "sell"
|
||||||
|
|
||||||
|
|
||||||
|
def direc(is_short: bool):
|
||||||
|
return "short" if is_short else "long"
|
||||||
|
|
||||||
|
|
||||||
|
def mock_order_usdt_1(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '1234',
|
'id': f'prod_entry_1_{direc(is_short)}',
|
||||||
'symbol': 'ADA/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 10.0,
|
||||||
'amount': 10.0,
|
'amount': 2.0,
|
||||||
'filled': 10.0,
|
'filled': 2.0,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_1(fee):
|
def mock_order_usdt_1_exit(is_short: bool):
|
||||||
|
return {
|
||||||
|
'id': f'prod_exit_1_{direc(is_short)}',
|
||||||
|
'symbol': 'LTC/USDT',
|
||||||
|
'status': 'closed',
|
||||||
|
'side': exit_side(is_short),
|
||||||
|
'type': 'limit',
|
||||||
|
'price': 8.0,
|
||||||
|
'amount': 2.0,
|
||||||
|
'filled': 2.0,
|
||||||
|
'remaining': 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mock_trade_usdt_1(fee, is_short: bool):
|
||||||
|
"""
|
||||||
|
Simulate prod entry with open sell order
|
||||||
|
"""
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ADA/USDT',
|
pair='LTC/USDT',
|
||||||
stake_amount=20.0,
|
stake_amount=20.0,
|
||||||
amount=10.0,
|
amount=2.0,
|
||||||
amount_requested=10.0,
|
amount_requested=2.0,
|
||||||
|
open_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=20),
|
||||||
|
close_date=datetime.now(tz=timezone.utc) - timedelta(days=2, minutes=5),
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
is_open=True,
|
is_open=False,
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
open_rate=10.0,
|
||||||
open_rate=2.0,
|
close_rate=8.0,
|
||||||
|
close_profit=-0.2,
|
||||||
|
close_profit_abs=-4.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='dry_run_buy_12345',
|
strategy='SampleStrategy',
|
||||||
strategy='StrategyTestV2',
|
open_order_id=f'prod_exit_1_{direc(is_short)}',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_1(), 'ADA/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_1(is_short), 'LTC/USDT', entry_side(is_short))
|
||||||
|
trade.orders.append(o)
|
||||||
|
o = Order.parse_from_ccxt_object(mock_order_usdt_1_exit(is_short),
|
||||||
|
'LTC/USDT', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_2():
|
def mock_order_usdt_2(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '1235',
|
'id': f'1235_{direc(is_short)}',
|
||||||
'symbol': 'ETC/USDT',
|
'symbol': 'ETC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 100.0,
|
'amount': 100.0,
|
||||||
|
@ -55,12 +92,12 @@ def mock_order_usdt_2():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_2_sell():
|
def mock_order_usdt_2_exit(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '12366',
|
'id': f'12366_{direc(is_short)}',
|
||||||
'symbol': 'ETC/USDT',
|
'symbol': 'ETC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.05,
|
'price': 2.05,
|
||||||
'amount': 100.0,
|
'amount': 100.0,
|
||||||
|
@ -69,7 +106,7 @@ def mock_order_usdt_2_sell():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_2(fee):
|
def mock_trade_usdt_2(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Closed trade...
|
Closed trade...
|
||||||
"""
|
"""
|
||||||
|
@ -82,30 +119,33 @@ def mock_trade_usdt_2(fee):
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=2.0,
|
open_rate=2.0,
|
||||||
close_rate=2.05,
|
close_rate=2.05,
|
||||||
close_profit=5.0,
|
close_profit=0.05,
|
||||||
close_profit_abs=3.9875,
|
close_profit_abs=3.9875,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
open_order_id='dry_run_sell_12345',
|
open_order_id=f'12366_{direc(is_short)}',
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
exit_reason='sell_signal',
|
enter_tag='TEST1',
|
||||||
|
exit_reason='exit_signal',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_2(), 'ETC/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_2(is_short), 'ETC/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_2_sell(), 'ETC/USDT', 'sell')
|
o = Order.parse_from_ccxt_object(
|
||||||
|
mock_order_usdt_2_exit(is_short), 'ETC/USDT', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_3():
|
def mock_order_usdt_3(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '41231a12a',
|
'id': f'41231a12a_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 1.0,
|
'price': 1.0,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
|
@ -114,12 +154,12 @@ def mock_order_usdt_3():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_3_sell():
|
def mock_order_usdt_3_exit(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '41231a666a',
|
'id': f'41231a666a_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 1.1,
|
'price': 1.1,
|
||||||
'average': 1.1,
|
'average': 1.1,
|
||||||
|
@ -129,7 +169,7 @@ def mock_order_usdt_3_sell():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_3(fee):
|
def mock_trade_usdt_3(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Closed trade
|
Closed trade
|
||||||
"""
|
"""
|
||||||
|
@ -142,29 +182,32 @@ def mock_trade_usdt_3(fee):
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=1.0,
|
open_rate=1.0,
|
||||||
close_rate=1.1,
|
close_rate=1.1,
|
||||||
close_profit=10.0,
|
close_profit=0.1,
|
||||||
close_profit_abs=9.8425,
|
close_profit_abs=9.8425,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
enter_tag='TEST3',
|
||||||
exit_reason='roi',
|
exit_reason='roi',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc),
|
close_date=datetime.now(tz=timezone.utc),
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_3(), 'XRP/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_3(is_short), 'XRP/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_3_sell(), 'XRP/USDT', 'sell')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_3_exit(is_short),
|
||||||
|
'XRP/USDT', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_4():
|
def mock_order_usdt_4(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_12345',
|
'id': f'prod_buy_12345_{direc(is_short)}',
|
||||||
'symbol': 'ETC/USDT',
|
'symbol': 'ETC/USDT',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 10.0,
|
'amount': 10.0,
|
||||||
|
@ -173,7 +216,7 @@ def mock_order_usdt_4():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_4(fee):
|
def mock_trade_usdt_4(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Simulate prod entry
|
Simulate prod entry
|
||||||
"""
|
"""
|
||||||
|
@ -188,21 +231,22 @@ def mock_trade_usdt_4(fee):
|
||||||
is_open=True,
|
is_open=True,
|
||||||
open_rate=2.0,
|
open_rate=2.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='prod_buy_12345',
|
open_order_id=f'prod_buy_12345_{direc(is_short)}',
|
||||||
strategy='StrategyTestV2',
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_4(), 'ETC/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_4(is_short), 'ETC/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_5():
|
def mock_order_usdt_5(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_3455',
|
'id': f'prod_buy_3455_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 10.0,
|
'amount': 10.0,
|
||||||
|
@ -211,12 +255,12 @@ def mock_order_usdt_5():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_5_stoploss():
|
def mock_order_usdt_5_stoploss(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_stoploss_3455',
|
'id': f'prod_stoploss_3455_{direc(is_short)}',
|
||||||
'symbol': 'XRP/USDT',
|
'symbol': 'XRP/USDT',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 2.0,
|
'price': 2.0,
|
||||||
'amount': 10.0,
|
'amount': 10.0,
|
||||||
|
@ -225,7 +269,7 @@ def mock_order_usdt_5_stoploss():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_5(fee):
|
def mock_trade_usdt_5(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Simulate prod entry with stoploss
|
Simulate prod entry with stoploss
|
||||||
"""
|
"""
|
||||||
|
@ -241,22 +285,23 @@ def mock_trade_usdt_5(fee):
|
||||||
open_rate=2.0,
|
open_rate=2.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
stoploss_order_id='prod_stoploss_3455',
|
stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_5(), 'XRP/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_5(is_short), 'XRP/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(), 'XRP/USDT', 'stoploss')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_5_stoploss(is_short), 'XRP/USDT', 'stoploss')
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_6():
|
def mock_order_usdt_6(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_6',
|
'id': f'prod_entry_6_{direc(is_short)}',
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 10.0,
|
'price': 10.0,
|
||||||
'amount': 2.0,
|
'amount': 2.0,
|
||||||
|
@ -265,12 +310,12 @@ def mock_order_usdt_6():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_6_sell():
|
def mock_order_usdt_6_exit(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_sell_6',
|
'id': f'prod_exit_6_{direc(is_short)}',
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'LTC/USDT',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 12.0,
|
'price': 12.0,
|
||||||
'amount': 2.0,
|
'amount': 2.0,
|
||||||
|
@ -279,7 +324,7 @@ def mock_order_usdt_6_sell():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_6(fee):
|
def mock_trade_usdt_6(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Simulate prod entry with open sell order
|
Simulate prod entry with open sell order
|
||||||
"""
|
"""
|
||||||
|
@ -295,69 +340,49 @@ def mock_trade_usdt_6(fee):
|
||||||
open_rate=10.0,
|
open_rate=10.0,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
open_order_id="prod_sell_6",
|
open_order_id=f'prod_exit_6_{direc(is_short)}',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_6(), 'LTC/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_6(is_short), 'LTC/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_6_exit(is_short),
|
||||||
|
'LTC/USDT', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_7():
|
def mock_order_usdt_7(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_7',
|
'id': f'1234_{direc(is_short)}',
|
||||||
'symbol': 'LTC/USDT',
|
'symbol': 'ADA/USDT',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': entry_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 10.0,
|
'price': 2.0,
|
||||||
'amount': 2.0,
|
'amount': 10.0,
|
||||||
'filled': 2.0,
|
'filled': 10.0,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_usdt_7_sell():
|
def mock_trade_usdt_7(fee, is_short: bool):
|
||||||
return {
|
|
||||||
'id': 'prod_sell_7',
|
|
||||||
'symbol': 'LTC/USDT',
|
|
||||||
'status': 'closed',
|
|
||||||
'side': 'sell',
|
|
||||||
'type': 'limit',
|
|
||||||
'price': 8.0,
|
|
||||||
'amount': 2.0,
|
|
||||||
'filled': 2.0,
|
|
||||||
'remaining': 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_usdt_7(fee):
|
|
||||||
"""
|
|
||||||
Simulate prod entry with open sell order
|
|
||||||
"""
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/USDT',
|
pair='ADA/USDT',
|
||||||
stake_amount=20.0,
|
stake_amount=20.0,
|
||||||
amount=2.0,
|
amount=10.0,
|
||||||
amount_requested=2.0,
|
amount_requested=10.0,
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
|
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
is_open=False,
|
is_open=True,
|
||||||
open_rate=10.0,
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||||
close_rate=8.0,
|
open_rate=2.0,
|
||||||
close_profit=-0.2,
|
|
||||||
close_profit_abs=-4.0,
|
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
open_order_id=f'1234_{direc(is_short)}',
|
||||||
open_order_id="prod_sell_6",
|
strategy='StrategyTestV2',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_usdt_7(is_short), 'ADA/USDT', entry_side(is_short))
|
||||||
trade.orders.append(o)
|
|
||||||
o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell')
|
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
|
@ -85,7 +85,7 @@ def test_load_backtest_data_new_format(testdatadir):
|
||||||
filename = testdatadir / "backtest_results/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)
|
||||||
assert len(bt_data) == 179
|
assert len(bt_data) == 179
|
||||||
|
|
||||||
# Test loading from string (must yield same result)
|
# Test loading from string (must yield same result)
|
||||||
|
@ -110,7 +110,7 @@ def test_load_backtest_data_multi(testdatadir):
|
||||||
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)
|
||||||
assert set(bt_data.columns) == set(
|
assert set(bt_data.columns) == set(
|
||||||
BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
|
BT_DATA_COLUMNS)
|
||||||
assert len(bt_data) == 179
|
assert len(bt_data) == 179
|
||||||
|
|
||||||
# Test loading from string (must yield same result)
|
# Test loading from string (must yield same result)
|
||||||
|
|
191
tests/data/test_entryexitanalysis.py
Executable file
191
tests/data/test_entryexitanalysis.py
Executable file
|
@ -0,0 +1,191 @@
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
|
||||||
|
from freqtrade.commands.optimize_commands import start_backtesting
|
||||||
|
from freqtrade.enums import ExitType
|
||||||
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
from tests.conftest import get_args, patch_exchange, patched_configuration_load_config_file
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def entryexitanalysis_cleanup() -> None:
|
||||||
|
yield None
|
||||||
|
|
||||||
|
Backtesting.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmpdir, capsys):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
default_conf.update({
|
||||||
|
"use_exit_signal": True,
|
||||||
|
"exit_profit_only": False,
|
||||||
|
"exit_profit_offset": 0.0,
|
||||||
|
"ignore_roi_if_entry_signal": False,
|
||||||
|
})
|
||||||
|
patch_exchange(mocker)
|
||||||
|
result1 = pd.DataFrame({'pair': ['ETH/BTC', 'LTC/BTC', 'ETH/BTC', 'LTC/BTC'],
|
||||||
|
'profit_ratio': [0.025, 0.05, -0.1, -0.05],
|
||||||
|
'profit_abs': [0.5, 2.0, -4.0, -2.0],
|
||||||
|
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
|
||||||
|
'2018-01-30 03:30:00',
|
||||||
|
'2018-01-30 08:10:00',
|
||||||
|
'2018-01-31 13:30:00', ], utc=True
|
||||||
|
),
|
||||||
|
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
|
||||||
|
'2018-01-30 05:35:00',
|
||||||
|
'2018-01-30 09:10:00',
|
||||||
|
'2018-01-31 15:00:00', ], utc=True),
|
||||||
|
'trade_duration': [235, 40, 60, 90],
|
||||||
|
'is_open': [False, False, False, False],
|
||||||
|
'stake_amount': [0.01, 0.01, 0.01, 0.01],
|
||||||
|
'open_rate': [0.104445, 0.10302485, 0.10302485, 0.10302485],
|
||||||
|
'close_rate': [0.104969, 0.103541, 0.102041, 0.102541],
|
||||||
|
"is_short": [False, False, False, False],
|
||||||
|
'enter_tag': ["enter_tag_long_a",
|
||||||
|
"enter_tag_long_b",
|
||||||
|
"enter_tag_long_a",
|
||||||
|
"enter_tag_long_b"],
|
||||||
|
'exit_reason': [ExitType.ROI,
|
||||||
|
ExitType.EXIT_SIGNAL,
|
||||||
|
ExitType.STOP_LOSS,
|
||||||
|
ExitType.TRAILING_STOP_LOSS]
|
||||||
|
})
|
||||||
|
|
||||||
|
backtestmock = MagicMock(side_effect=[
|
||||||
|
{
|
||||||
|
'results': result1,
|
||||||
|
'config': default_conf,
|
||||||
|
'locks': [],
|
||||||
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
|
'canceled_trade_entries': 0,
|
||||||
|
'canceled_entry_orders': 0,
|
||||||
|
'replaced_entry_orders': 0,
|
||||||
|
'final_balance': 1000,
|
||||||
|
}
|
||||||
|
])
|
||||||
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['ETH/BTC', 'LTC/BTC', 'DASH/BTC']))
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
|
|
||||||
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'backtesting',
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--datadir', str(testdatadir),
|
||||||
|
'--user-data-dir', str(tmpdir),
|
||||||
|
'--timeframe', '5m',
|
||||||
|
'--timerange', '1515560100-1517287800',
|
||||||
|
'--export', 'signals',
|
||||||
|
'--cache', 'none',
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start_backtesting(args)
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'BACKTESTING REPORT' in captured.out
|
||||||
|
assert 'EXIT REASON STATS' in captured.out
|
||||||
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
||||||
|
|
||||||
|
base_args = [
|
||||||
|
'backtesting-analysis',
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--datadir', str(testdatadir),
|
||||||
|
'--user-data-dir', str(tmpdir),
|
||||||
|
]
|
||||||
|
|
||||||
|
# test group 0 and indicator list
|
||||||
|
args = get_args(base_args +
|
||||||
|
['--analysis-groups', "0",
|
||||||
|
'--indicator-list', "close", "rsi", "profit_abs"]
|
||||||
|
)
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'LTC/BTC' in captured.out
|
||||||
|
assert 'ETH/BTC' in captured.out
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' in captured.out
|
||||||
|
assert 'exit_signal' in captured.out
|
||||||
|
assert 'roi' in captured.out
|
||||||
|
assert 'stop_loss' in captured.out
|
||||||
|
assert 'trailing_stop_loss' in captured.out
|
||||||
|
assert '0.5' in captured.out
|
||||||
|
assert '-4' in captured.out
|
||||||
|
assert '-2' in captured.out
|
||||||
|
assert '-3.5' in captured.out
|
||||||
|
assert '50' in captured.out
|
||||||
|
assert '0' in captured.out
|
||||||
|
assert '0.01616' in captured.out
|
||||||
|
assert '34.049' in captured.out
|
||||||
|
assert '0.104104' in captured.out
|
||||||
|
assert '47.0996' in captured.out
|
||||||
|
|
||||||
|
# test group 1
|
||||||
|
args = get_args(base_args + ['--analysis-groups', "1"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' in captured.out
|
||||||
|
assert 'total_profit_pct' in captured.out
|
||||||
|
assert '-3.5' in captured.out
|
||||||
|
assert '-1.75' in captured.out
|
||||||
|
assert '-7.5' in captured.out
|
||||||
|
assert '-3.75' in captured.out
|
||||||
|
assert '0' in captured.out
|
||||||
|
|
||||||
|
# test group 2
|
||||||
|
args = get_args(base_args + ['--analysis-groups', "2"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' in captured.out
|
||||||
|
assert 'exit_signal' in captured.out
|
||||||
|
assert 'roi' in captured.out
|
||||||
|
assert 'stop_loss' in captured.out
|
||||||
|
assert 'trailing_stop_loss' in captured.out
|
||||||
|
assert 'total_profit_pct' in captured.out
|
||||||
|
assert '-10' in captured.out
|
||||||
|
assert '-5' in captured.out
|
||||||
|
assert '2.5' in captured.out
|
||||||
|
|
||||||
|
# test group 3
|
||||||
|
args = get_args(base_args + ['--analysis-groups', "3"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'LTC/BTC' in captured.out
|
||||||
|
assert 'ETH/BTC' in captured.out
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' in captured.out
|
||||||
|
assert 'total_profit_pct' in captured.out
|
||||||
|
assert '-7.5' in captured.out
|
||||||
|
assert '-3.75' in captured.out
|
||||||
|
assert '-1.75' in captured.out
|
||||||
|
assert '0' in captured.out
|
||||||
|
assert '2' in captured.out
|
||||||
|
|
||||||
|
# test group 4
|
||||||
|
args = get_args(base_args + ['--analysis-groups', "4"])
|
||||||
|
start_analysis_entries_exits(args)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'LTC/BTC' in captured.out
|
||||||
|
assert 'ETH/BTC' in captured.out
|
||||||
|
assert 'enter_tag_long_a' in captured.out
|
||||||
|
assert 'enter_tag_long_b' in captured.out
|
||||||
|
assert 'exit_signal' in captured.out
|
||||||
|
assert 'roi' in captured.out
|
||||||
|
assert 'stop_loss' in captured.out
|
||||||
|
assert 'trailing_stop_loss' in captured.out
|
||||||
|
assert 'total_profit_pct' in captured.out
|
||||||
|
assert '-10' in captured.out
|
||||||
|
assert '-5' in captured.out
|
||||||
|
assert '-4' in captured.out
|
||||||
|
assert '0.5' in captured.out
|
||||||
|
assert '1' in captured.out
|
||||||
|
assert '2.5' in captured.out
|
|
@ -199,8 +199,13 @@ class TestCCXTExchange():
|
||||||
l2 = exchange.fetch_l2_order_book(pair)
|
l2 = exchange.fetch_l2_order_book(pair)
|
||||||
assert 'asks' in l2
|
assert 'asks' in l2
|
||||||
assert 'bids' in l2
|
assert 'bids' in l2
|
||||||
|
assert len(l2['asks']) >= 1
|
||||||
|
assert len(l2['bids']) >= 1
|
||||||
l2_limit_range = exchange._ft_has['l2_limit_range']
|
l2_limit_range = exchange._ft_has['l2_limit_range']
|
||||||
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
|
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
|
||||||
|
if exchangename == 'gateio':
|
||||||
|
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
|
||||||
|
return
|
||||||
for val in [1, 2, 5, 25, 100]:
|
for val in [1, 2, 5, 25, 100]:
|
||||||
l2 = exchange.fetch_l2_order_book(pair, val)
|
l2 = exchange.fetch_l2_order_book(pair, val)
|
||||||
if not l2_limit_range or val in l2_limit_range:
|
if not l2_limit_range or val in l2_limit_range:
|
||||||
|
|
|
@ -33,6 +33,12 @@ def test_validate_order_types_gateio(default_conf, mocker):
|
||||||
match=r'Exchange .* does not support market orders.'):
|
match=r'Exchange .* does not support market orders.'):
|
||||||
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||||
|
|
||||||
|
# market-orders supported on futures markets.
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
default_conf['margin_mode'] = 'isolated'
|
||||||
|
ex = ExchangeResolver.load_exchange('gateio', default_conf, True)
|
||||||
|
assert ex
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
||||||
|
|
|
@ -123,5 +123,5 @@ def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||||
assert exchange.stoploss_adjust(1501, order, 'sell')
|
assert exchange.stoploss_adjust(1501, order, 'sell')
|
||||||
assert not exchange.stoploss_adjust(1499, order, 'sell')
|
assert not exchange.stoploss_adjust(1499, order, 'sell')
|
||||||
# Test with invalid order case
|
# Test with invalid order case
|
||||||
order['info']['stop'] = None
|
order['stopPrice'] = None
|
||||||
assert not exchange.stoploss_adjust(1501, order, 'sell')
|
assert exchange.stoploss_adjust(1501, order, 'sell')
|
||||||
|
|
|
@ -6,7 +6,7 @@ import pytest
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.enums.candletype import CandleType
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.exchange.exchange import timeframe_to_minutes
|
from freqtrade.exchange.exchange import timeframe_to_minutes
|
||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_mock_coro, get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,7 +273,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
||||||
'fetchLeverageTiers': False,
|
'fetchLeverageTiers': False,
|
||||||
'fetchMarketLeverageTiers': True,
|
'fetchMarketLeverageTiers': True,
|
||||||
})
|
})
|
||||||
api_mock.fetch_market_leverage_tiers = MagicMock(side_effect=[
|
api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
'tier': 1,
|
'tier': 1,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import pytest
|
||||||
from freqtrade.data.history import get_timerange
|
from freqtrade.data.history import get_timerange
|
||||||
from freqtrade.enums import ExitType
|
from freqtrade.enums import ExitType
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
from freqtrade.persistence.trade_model import LocalTrade
|
||||||
from tests.conftest import patch_exchange
|
from tests.conftest import patch_exchange
|
||||||
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
|
||||||
_get_frame_time_from_offset, tests_timeframe)
|
_get_frame_time_from_offset, tests_timeframe)
|
||||||
|
@ -964,5 +965,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
||||||
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
|
assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
|
||||||
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
|
||||||
assert res.is_short == trade.is_short
|
assert res.is_short == trade.is_short
|
||||||
|
assert len(LocalTrade.trades) == len(data.trades)
|
||||||
|
assert len(LocalTrade.trades_open) == 0
|
||||||
backtesting.cleanup()
|
backtesting.cleanup()
|
||||||
del backtesting
|
del backtesting
|
||||||
|
|
|
@ -795,10 +795,27 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
'is_open': [False, False],
|
'is_open': [False, False],
|
||||||
'enter_tag': [None, None],
|
'enter_tag': [None, None],
|
||||||
"is_short": [False, False],
|
"is_short": [False, False],
|
||||||
|
'open_timestamp': [1517251200000, 1517283000000],
|
||||||
|
'close_timestamp': [1517265300000, 1517285400000],
|
||||||
|
'orders': [
|
||||||
|
[
|
||||||
|
{'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy',
|
||||||
|
'order_filled_timestamp': 1517251200000, 'ft_is_entry': True},
|
||||||
|
{'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell',
|
||||||
|
'order_filled_timestamp': 1517265300000, 'ft_is_entry': False}
|
||||||
|
], [
|
||||||
|
{'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy',
|
||||||
|
'order_filled_timestamp': 1517283000000, 'ft_is_entry': True},
|
||||||
|
{'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell',
|
||||||
|
'order_filled_timestamp': 1517285400000, 'ft_is_entry': False}
|
||||||
|
]
|
||||||
|
]
|
||||||
})
|
})
|
||||||
pd.testing.assert_frame_equal(results, expected)
|
pd.testing.assert_frame_equal(results, expected)
|
||||||
|
assert 'orders' in results.columns
|
||||||
data_pair = processed[pair]
|
data_pair = processed[pair]
|
||||||
for _, t in results.iterrows():
|
for _, t in results.iterrows():
|
||||||
|
assert len(t['orders']) == 2
|
||||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||||
# Check open trade rate alignes to open rate
|
# Check open trade rate alignes to open rate
|
||||||
assert ln is not None
|
assert ln is not None
|
||||||
|
|
|
@ -70,9 +70,14 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
||||||
'is_open': [False, False],
|
'is_open': [False, False],
|
||||||
'enter_tag': [None, None],
|
'enter_tag': [None, None],
|
||||||
'is_short': [False, False],
|
'is_short': [False, False],
|
||||||
|
'open_timestamp': [1517251200000, 1517283000000],
|
||||||
|
'close_timestamp': [1517265300000, 1517285400000],
|
||||||
})
|
})
|
||||||
pd.testing.assert_frame_equal(results, expected)
|
pd.testing.assert_frame_equal(results.drop(columns=['orders']), expected)
|
||||||
data_pair = processed[pair]
|
data_pair = processed[pair]
|
||||||
|
assert len(results.iloc[0]['orders']) == 6
|
||||||
|
assert len(results.iloc[1]['orders']) == 2
|
||||||
|
|
||||||
for _, t in results.iterrows():
|
for _, t in results.iterrows():
|
||||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||||
# Check open trade rate alignes to open rate
|
# Check open trade rate alignes to open rate
|
||||||
|
|
|
@ -861,6 +861,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||||
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
||||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
||||||
|
assert hyperopt.backtesting.strategy.bot_loop_started is True
|
||||||
|
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
|
||||||
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
assert hyperopt.backtesting.strategy.buy_rsi.value == 35
|
||||||
|
|
|
@ -171,7 +171,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
||||||
_backup_file(filename_last, copy_file=True)
|
_backup_file(filename_last, copy_file=True)
|
||||||
assert not filename.is_file()
|
assert not filename.is_file()
|
||||||
|
|
||||||
store_backtest_stats(filename, stats)
|
store_backtest_stats(filename, stats, '2022_01_01_15_05_13')
|
||||||
|
|
||||||
# get real Filename (it's btresult-<date>.json)
|
# get real Filename (it's btresult-<date>.json)
|
||||||
last_fn = get_latest_backtest_filename(filename_last.parent)
|
last_fn = get_latest_backtest_filename(filename_last.parent)
|
||||||
|
@ -194,7 +194,7 @@ def test_store_backtest_stats(testdatadir, mocker):
|
||||||
|
|
||||||
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json')
|
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json')
|
||||||
|
|
||||||
store_backtest_stats(testdatadir, {'metadata': {}})
|
store_backtest_stats(testdatadir, {'metadata': {}}, '2022_01_01_15_05_13')
|
||||||
|
|
||||||
assert dump_mock.call_count == 3
|
assert dump_mock.call_count == 3
|
||||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
|
@ -202,7 +202,7 @@ def test_store_backtest_stats(testdatadir, mocker):
|
||||||
|
|
||||||
dump_mock.reset_mock()
|
dump_mock.reset_mock()
|
||||||
filename = testdatadir / 'testresult.json'
|
filename = testdatadir / 'testresult.json'
|
||||||
store_backtest_stats(filename, {'metadata': {}})
|
store_backtest_stats(filename, {'metadata': {}}, '2022_01_01_15_05_13')
|
||||||
assert dump_mock.call_count == 3
|
assert dump_mock.call_count == 3
|
||||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
# result will be testdatadir / testresult-<timestamp>.json
|
# result will be testdatadir / testresult-<timestamp>.json
|
||||||
|
@ -216,7 +216,7 @@ def test_store_backtest_candles(testdatadir, mocker):
|
||||||
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
||||||
|
|
||||||
# mock directory exporting
|
# mock directory exporting
|
||||||
store_backtest_signal_candles(testdatadir, candle_dict)
|
store_backtest_signal_candles(testdatadir, candle_dict, '2022_01_01_15_05_13')
|
||||||
|
|
||||||
assert dump_mock.call_count == 1
|
assert dump_mock.call_count == 1
|
||||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
|
@ -225,7 +225,7 @@ def test_store_backtest_candles(testdatadir, mocker):
|
||||||
dump_mock.reset_mock()
|
dump_mock.reset_mock()
|
||||||
# mock file exporting
|
# mock file exporting
|
||||||
filename = Path(testdatadir / 'testresult')
|
filename = Path(testdatadir / 'testresult')
|
||||||
store_backtest_signal_candles(filename, candle_dict)
|
store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
|
||||||
assert dump_mock.call_count == 1
|
assert dump_mock.call_count == 1
|
||||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
# result will be testdatadir / testresult-<timestamp>_signals.pkl
|
||||||
|
@ -238,7 +238,7 @@ def test_write_read_backtest_candles(tmpdir):
|
||||||
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}}
|
||||||
|
|
||||||
# test directory exporting
|
# test directory exporting
|
||||||
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict)
|
stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13')
|
||||||
scp = open(stored_file, "rb")
|
scp = open(stored_file, "rb")
|
||||||
pickled_signal_candles = joblib.load(scp)
|
pickled_signal_candles = joblib.load(scp)
|
||||||
scp.close()
|
scp.close()
|
||||||
|
@ -252,7 +252,7 @@ def test_write_read_backtest_candles(tmpdir):
|
||||||
|
|
||||||
# test file exporting
|
# test file exporting
|
||||||
filename = Path(tmpdir / 'testresult')
|
filename = Path(tmpdir / 'testresult')
|
||||||
stored_file = store_backtest_signal_candles(filename, candle_dict)
|
stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13')
|
||||||
scp = open(stored_file, "rb")
|
scp = open(stored_file, "rb")
|
||||||
pickled_signal_candles = joblib.load(scp)
|
pickled_signal_candles = joblib.load(scp)
|
||||||
scp.close()
|
scp.close()
|
||||||
|
|
|
@ -762,8 +762,8 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog
|
||||||
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
||||||
create_mock_trades_usdt(fee)
|
create_mock_trades_usdt(fee)
|
||||||
pm.refresh_pairlist()
|
pm.refresh_pairlist()
|
||||||
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT',
|
assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', 'LTC/USDT',
|
||||||
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT']
|
'NEO/USDT', 'TKN/USDT', 'ADA/USDT', ]
|
||||||
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
# assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
||||||
|
|
||||||
# Move to "outside" of lookback window, so original sorting is restored.
|
# Move to "outside" of lookback window, so original sorting is restored.
|
||||||
|
|
|
@ -11,11 +11,11 @@ from freqtrade.edge import PairInfo
|
||||||
from freqtrade.enums import SignalDirection, State, TradingMode
|
from freqtrade.enums import SignalDirection, State, TradingMode
|
||||||
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.persistence.models import Order
|
|
||||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
from freqtrade.persistence.pairlock_middleware import PairLocks
|
||||||
from freqtrade.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal
|
from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot,
|
||||||
|
patch_get_signal)
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
@ -284,7 +284,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||||
assert isnan(fiat_profit_sum)
|
assert isnan(fiat_profit_sum)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -294,45 +294,35 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
markets=PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
create_mock_trades_usdt(fee)
|
||||||
stake_currency = default_conf['stake_currency']
|
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
stake_currency = default_conf_usdt['stake_currency']
|
||||||
|
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
# Create some test data
|
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate buy & sell
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
update.message.text = '/daily 2'
|
days = rpc._rpc_timeunit_profit(7, stake_currency, fiat_display_currency)
|
||||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
|
||||||
assert len(days['data']) == 7
|
assert len(days['data']) == 7
|
||||||
assert days['stake_currency'] == default_conf['stake_currency']
|
assert days['stake_currency'] == default_conf_usdt['stake_currency']
|
||||||
assert days['fiat_display_currency'] == default_conf['fiat_display_currency']
|
assert days['fiat_display_currency'] == default_conf_usdt['fiat_display_currency']
|
||||||
for day in days['data']:
|
for day in days['data']:
|
||||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
# {'date': datetime.date(2022, 6, 11), 'abs_profit': 13.8299999,
|
||||||
assert (day['abs_profit'] == 0.0 or
|
# 'starting_balance': 1055.37, 'rel_profit': 0.0131044,
|
||||||
day['abs_profit'] == 0.00006217)
|
# 'fiat_value': 0.0, 'trade_count': 2}
|
||||||
|
assert day['abs_profit'] in (0.0, pytest.approx(13.8299999), pytest.approx(-4.0))
|
||||||
assert (day['fiat_value'] == 0.0 or
|
assert day['rel_profit'] in (0.0, pytest.approx(0.01310441), pytest.approx(-0.00377583))
|
||||||
day['fiat_value'] == 0.76748865)
|
assert day['trade_count'] in (0, 1, 2)
|
||||||
|
assert day['starting_balance'] in (pytest.approx(1059.37), pytest.approx(1055.37))
|
||||||
|
assert day['fiat_value'] in (0.0, )
|
||||||
# ensure first day is current date
|
# ensure first day is current date
|
||||||
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
assert str(days['data'][0]['date']) == str(datetime.utcnow().date())
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||||
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
rpc._rpc_timeunit_profit(0, stake_currency, fiat_display_currency)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('is_short', [True, False])
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
@ -416,13 +406,8 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||||
assert stoploss_mock.call_count == 0
|
assert stoploss_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
|
|
||||||
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
|
|
||||||
)
|
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -430,10 +415,9 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
stake_currency = default_conf_usdt['stake_currency']
|
||||||
stake_currency = default_conf['stake_currency']
|
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
|
@ -446,75 +430,40 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
assert res['latest_trade_timestamp'] == 0
|
assert res['latest_trade_timestamp'] == 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker_sell_up
|
|
||||||
)
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker_sell_up
|
|
||||||
)
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
assert pytest.approx(stats['profit_closed_coin']) == 9.83
|
||||||
assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2)
|
assert pytest.approx(stats['profit_closed_percent_mean']) == -1.67
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
assert pytest.approx(stats['profit_closed_fiat']) == 10.813
|
||||||
assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
|
assert pytest.approx(stats['profit_all_coin']) == -77.45964918
|
||||||
assert prec_satoshi(stats['profit_all_percent_mean'], 2.89)
|
assert pytest.approx(stats['profit_all_percent_mean']) == -57.86
|
||||||
assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
|
assert pytest.approx(stats['profit_all_fiat']) == -85.205614098
|
||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 7
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == '2 days ago'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
assert stats['avg_duration'] in ('0:17:40')
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'XRP/USDT'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert stats['best_rate'] == 10.0
|
||||||
|
|
||||||
# Test non-available pair
|
# Test non-available pair
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
|
MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available")))
|
||||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert stats['trade_count'] == 2
|
assert stats['trade_count'] == 7
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == '2 days ago'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||||
assert stats['avg_duration'] in ('0:00:00', '0:00:01', '0:00:02')
|
assert stats['avg_duration'] in ('0:17:40')
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'XRP/USDT'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert stats['best_rate'] == 10.0
|
||||||
assert isnan(stats['profit_all_coin'])
|
assert isnan(stats['profit_all_coin'])
|
||||||
|
|
||||||
|
|
||||||
# Test that rpc_trade_statistics can handle trades that lacks
|
# Test that rpc_trade_statistics can handle trades that lacks
|
||||||
# trade.open_rate (it is set to None)
|
# trade.open_rate (it is set to None)
|
||||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
def test_rpc_trade_statistics_closed(mocker, default_conf_usdt, ticker, fee):
|
||||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.rpc.fiat_convert.CoinGeckoAPI',
|
|
||||||
get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}),
|
|
||||||
)
|
|
||||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0)
|
return_value=1.1)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -522,46 +471,32 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf_usdt['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf_usdt['fiat_display_currency']
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
# Update the ticker with a market going up
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker_sell_up,
|
|
||||||
get_fee=fee
|
|
||||||
)
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
for trade in Trade.query.order_by(Trade.id).all():
|
for trade in Trade.query.order_by(Trade.id).all():
|
||||||
trade.open_rate = None
|
trade.open_rate = None
|
||||||
|
|
||||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
assert stats['profit_closed_coin'] == 0
|
||||||
assert prec_satoshi(stats['profit_closed_percent_mean'], 0)
|
assert stats['profit_closed_percent_mean'] == 0
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
assert stats['profit_closed_fiat'] == 0
|
||||||
assert prec_satoshi(stats['profit_all_coin'], 0)
|
assert stats['profit_all_coin'] == 0
|
||||||
assert prec_satoshi(stats['profit_all_percent_mean'], 0)
|
assert stats['profit_all_percent_mean'] == 0
|
||||||
assert prec_satoshi(stats['profit_all_fiat'], 0)
|
assert stats['profit_all_fiat'] == 0
|
||||||
assert stats['trade_count'] == 1
|
assert stats['trade_count'] == 7
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == '2 days ago'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == '17 minutes ago'
|
||||||
assert stats['avg_duration'] == '0:00:00'
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'XRP/USDT'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert stats['best_rate'] == 10.0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_balance_handle_error(default_conf, mocker):
|
def test_rpc_balance_handle_error(default_conf, mocker):
|
||||||
|
@ -913,8 +848,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||||
assert cancel_order_mock.call_count == 3
|
assert cancel_order_mock.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -923,34 +857,21 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
res = rpc._rpc_performance()
|
res = rpc._rpc_performance()
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['pair'] == 'ETH/BTC'
|
assert res[0]['pair'] == 'XRP/USDT'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert res[0]['profit_pct'] == 10.0
|
||||||
|
|
||||||
|
|
||||||
def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -964,34 +885,22 @@ def test_enter_tag_performance_handle(default_conf, ticker, limit_buy_order, fee
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
|
create_mock_trades_usdt(fee)
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
res = rpc._rpc_enter_tag_performance(None)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['enter_tag'] == 'Other'
|
assert res[0]['enter_tag'] == 'TEST3'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert res[0]['profit_pct'] == 10.0
|
||||||
|
|
||||||
trade.enter_tag = "TEST_TAG"
|
|
||||||
res = rpc._rpc_enter_tag_performance(None)
|
res = rpc._rpc_enter_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['enter_tag'] == 'TEST_TAG'
|
assert res[0]['enter_tag'] == 'TEST3'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert res[0]['profit_pct'] == 10.0
|
||||||
|
|
||||||
|
|
||||||
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
|
@ -1023,8 +932,7 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||||
|
|
||||||
|
|
||||||
def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -1033,39 +941,22 @@ def test_exit_reason_performance_handle(default_conf, ticker, limit_buy_order, f
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
res = rpc._rpc_exit_reason_performance(None)
|
res = rpc._rpc_exit_reason_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['exit_reason'] == 'Other'
|
assert res[0]['exit_reason'] == 'roi'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert res[0]['profit_pct'] == 10.0
|
||||||
|
|
||||||
trade.exit_reason = "TEST1"
|
assert res[1]['exit_reason'] == 'exit_signal'
|
||||||
res = rpc._rpc_exit_reason_performance(None)
|
assert res[2]['exit_reason'] == 'Other'
|
||||||
|
|
||||||
assert len(res) == 1
|
|
||||||
assert res[0]['exit_reason'] == 'TEST1'
|
|
||||||
assert res[0]['count'] == 1
|
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
|
@ -1097,8 +988,7 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
assert prec_satoshi(res[0]['profit_pct'], 0.5)
|
||||||
|
|
||||||
|
|
||||||
def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -1112,35 +1002,14 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
res = rpc._rpc_mix_tag_performance(None)
|
res = rpc._rpc_mix_tag_performance(None)
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 3
|
||||||
assert res[0]['mix_tag'] == 'Other Other'
|
assert res[0]['mix_tag'] == 'TEST3 roi'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
assert res[0]['profit_pct'] == 10.0
|
||||||
|
|
||||||
trade.enter_tag = "TESTBUY"
|
|
||||||
trade.exit_reason = "TESTSELL"
|
|
||||||
res = rpc._rpc_mix_tag_performance(None)
|
|
||||||
|
|
||||||
assert len(res) == 1
|
|
||||||
assert res[0]['mix_tag'] == 'TESTBUY TESTSELL'
|
|
||||||
assert res[0]['count'] == 1
|
|
||||||
assert prec_satoshi(res[0]['profit_pct'], 6.2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee):
|
||||||
|
|
|
@ -578,9 +578,10 @@ def test_api_trades(botclient, mocker, fee, markets, is_short):
|
||||||
)
|
)
|
||||||
rc = client_get(client, f"{BASE_URI}/trades")
|
rc = client_get(client, f"{BASE_URI}/trades")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json()) == 3
|
assert len(rc.json()) == 4
|
||||||
assert rc.json()['trades_count'] == 0
|
assert rc.json()['trades_count'] == 0
|
||||||
assert rc.json()['total_trades'] == 0
|
assert rc.json()['total_trades'] == 0
|
||||||
|
assert rc.json()['offset'] == 0
|
||||||
|
|
||||||
create_mock_trades(fee, is_short=is_short)
|
create_mock_trades(fee, is_short=is_short)
|
||||||
Trade.query.session.flush()
|
Trade.query.session.flush()
|
||||||
|
@ -724,7 +725,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||||
'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075,
|
'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075,
|
||||||
'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015,
|
'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015,
|
||||||
'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06,
|
'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06,
|
||||||
'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2}
|
'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2,
|
||||||
|
'profit_factor': 0.0, 'trading_volume': 91.074,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
False,
|
False,
|
||||||
|
@ -737,7 +740,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||||
'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075,
|
'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075,
|
||||||
'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015,
|
'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015,
|
||||||
'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07,
|
'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07,
|
||||||
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0}
|
'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0,
|
||||||
|
'profit_factor': None, 'trading_volume': 91.074,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
|
@ -750,7 +755,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||||
'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025,
|
'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025,
|
||||||
'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005,
|
'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005,
|
||||||
'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06,
|
'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06,
|
||||||
'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1}
|
'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1,
|
||||||
|
'profit_factor': 0.02775724835771106, 'trading_volume': 91.074,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected):
|
def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected):
|
||||||
|
@ -803,6 +810,10 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected)
|
||||||
'closed_trade_count': 2,
|
'closed_trade_count': 2,
|
||||||
'winning_trades': expected['winning_trades'],
|
'winning_trades': expected['winning_trades'],
|
||||||
'losing_trades': expected['losing_trades'],
|
'losing_trades': expected['losing_trades'],
|
||||||
|
'profit_factor': expected['profit_factor'],
|
||||||
|
'max_drawdown': ANY,
|
||||||
|
'max_drawdown_abs': ANY,
|
||||||
|
'trading_volume': expected['trading_volume'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -852,8 +863,8 @@ def test_api_performance(botclient, fee):
|
||||||
close_rate=0.265441,
|
close_rate=0.265441,
|
||||||
|
|
||||||
)
|
)
|
||||||
trade.close_profit = trade.calc_profit_ratio()
|
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
|
||||||
trade.close_profit_abs = trade.calc_profit()
|
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
|
||||||
Trade.query.session.add(trade)
|
Trade.query.session.add(trade)
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -868,8 +879,8 @@ def test_api_performance(botclient, fee):
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
close_rate=0.391
|
close_rate=0.391
|
||||||
)
|
)
|
||||||
trade.close_profit = trade.calc_profit_ratio()
|
trade.close_profit = trade.calc_profit_ratio(trade.close_rate)
|
||||||
trade.close_profit_abs = trade.calc_profit()
|
trade.close_profit_abs = trade.calc_profit(trade.close_rate)
|
||||||
|
|
||||||
Trade.query.session.add(trade)
|
Trade.query.session.add(trade)
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
@ -1384,12 +1395,14 @@ def test_api_strategies(botclient):
|
||||||
rc = client_get(client, f"{BASE_URI}/strategies")
|
rc = client_get(client, f"{BASE_URI}/strategies")
|
||||||
|
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
|
|
||||||
assert rc.json() == {'strategies': [
|
assert rc.json() == {'strategies': [
|
||||||
'HyperoptableStrategy',
|
'HyperoptableStrategy',
|
||||||
'InformativeDecoratorTest',
|
'InformativeDecoratorTest',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'StrategyTestV3',
|
'StrategyTestV3',
|
||||||
'StrategyTestV3Futures',
|
'StrategyTestV3Analysis',
|
||||||
|
'StrategyTestV3Futures'
|
||||||
]}
|
]}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,9 @@ from freqtrade.persistence.models import Order
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
from freqtrade.rpc.rpc import RPCException
|
from freqtrade.rpc.rpc import RPCException
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt,
|
||||||
log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist)
|
get_patched_freqtradebot, log_has, log_has_re, patch_exchange,
|
||||||
|
patch_get_signal, patch_whitelist)
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
class DummyCls(Telegram):
|
||||||
|
@ -404,12 +405,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
|
||||||
default_conf['max_open_trades'] = 1
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=1.1
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -417,25 +416,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
|
|
||||||
patch_get_signal(freqtradebot)
|
|
||||||
|
|
||||||
|
# Move date to within day
|
||||||
|
time_machine.move_to('2022-06-11 08:00:00+00:00')
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobjs)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
# /daily 2
|
# /daily 2
|
||||||
|
@ -446,10 +432,11 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
assert "Daily Profit over the last 2 days</b>:" in msg_mock.call_args_list[0][0][0]
|
assert "Daily Profit over the last 2 days</b>:" in msg_mock.call_args_list[0][0][0]
|
||||||
assert 'Day ' in msg_mock.call_args_list[0][0][0]
|
assert 'Day ' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(2) 13.83 USDT 15.21 USD 1.31%' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -458,32 +445,23 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert "Daily Profit over the last 7 days</b>:" in msg_mock.call_args_list[0][0][0]
|
assert "Daily Profit over the last 7 days</b>:" in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert '(1)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
freqtradebot.config['max_open_trades'] = 2
|
|
||||||
# Add two other trades
|
|
||||||
n = freqtradebot.enter_positions()
|
|
||||||
assert n == 2
|
|
||||||
|
|
||||||
trades = Trade.query.all()
|
|
||||||
for trade in trades:
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.update_trade(oobjs)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# /daily 1
|
# /daily 1
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._daily(update=update, context=context)
|
telegram._daily(update=update, context=context)
|
||||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 13.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 15.21 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
assert '(2)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
|
@ -512,15 +490,14 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["today"]
|
context.args = ["today"]
|
||||||
telegram._daily(update=update, context=context)
|
telegram._daily(update=update, context=context)
|
||||||
assert str('Daily Profit over the last 7 days</b>:') in msg_mock.call_args_list[0][0][0]
|
assert 'Daily Profit over the last 7 days</b>:' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
default_conf_usdt['max_open_trades'] = 1
|
||||||
default_conf['max_open_trades'] = 1
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=1.1
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -528,25 +505,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
|
# Move to saturday - so all trades are within that week
|
||||||
patch_get_signal(freqtradebot)
|
time_machine.move_to('2022-06-11')
|
||||||
|
create_mock_trades_usdt(fee)
|
||||||
# Create some test data
|
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobjs)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
# /weekly 2
|
# /weekly 2
|
||||||
|
@ -560,10 +522,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
|
first_iso_day_of_current_week = today - timedelta(days=today.weekday())
|
||||||
assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0]
|
assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -573,44 +535,10 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
|
assert "Weekly Profit over the last 8 weeks (starting from Monday)</b>:" \
|
||||||
in msg_mock.call_args_list[0][0][0]
|
in msg_mock.call_args_list[0][0][0]
|
||||||
assert 'Weekly' in msg_mock.call_args_list[0][0][0]
|
assert 'Weekly' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
|
||||||
msg_mock.reset_mock()
|
|
||||||
freqtradebot.config['max_open_trades'] = 2
|
|
||||||
# Add two other trades
|
|
||||||
n = freqtradebot.enter_positions()
|
|
||||||
assert n == 2
|
|
||||||
|
|
||||||
trades = Trade.query.all()
|
|
||||||
for trade in trades:
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.update_trade(oobjs)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# /weekly 1
|
|
||||||
# By default, the 8 previous weeks are shown
|
|
||||||
# So the previous modified trade should be excluded from the stats
|
|
||||||
context = MagicMock()
|
|
||||||
context.args = ["1"]
|
|
||||||
telegram._weekly(update=update, context=context)
|
|
||||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
|
||||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
|
||||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker
|
|
||||||
)
|
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
|
||||||
patch_get_signal(freqtradebot)
|
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -629,16 +557,17 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["this week"]
|
context.args = ["this week"]
|
||||||
telegram._weekly(update=update, context=context)
|
telegram._weekly(update=update, context=context)
|
||||||
assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \
|
assert (
|
||||||
|
'Weekly Profit over the last 8 weeks (starting from Monday)</b>:'
|
||||||
in msg_mock.call_args_list[0][0][0]
|
in msg_mock.call_args_list[0][0][0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None:
|
||||||
limit_sell_order, mocker) -> None:
|
default_conf_usdt['max_open_trades'] = 1
|
||||||
default_conf['max_open_trades'] = 1
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=1.1
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -646,25 +575,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
|
# Move to day within the month so all mock trades fall into this week.
|
||||||
patch_get_signal(freqtradebot)
|
time_machine.move_to('2022-06-11')
|
||||||
|
create_mock_trades_usdt(fee)
|
||||||
# Create some test data
|
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobjs)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
# /monthly 2
|
# /monthly 2
|
||||||
|
@ -677,10 +591,10 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
current_month = f"{today.year}-{today.month:02} "
|
current_month = f"{today.year}-{today.month:02} "
|
||||||
assert current_month in msg_mock.call_args_list[0][0][0]
|
assert current_month in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
@ -691,24 +605,13 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
|
assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
|
||||||
assert 'Month ' in msg_mock.call_args_list[0][0][0]
|
assert 'Month ' in msg_mock.call_args_list[0][0][0]
|
||||||
assert current_month in msg_mock.call_args_list[0][0][0]
|
assert current_month in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
assert '(0)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Reset msg_mock
|
# Reset msg_mock
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
freqtradebot.config['max_open_trades'] = 2
|
|
||||||
# Add two other trades
|
|
||||||
n = freqtradebot.enter_positions()
|
|
||||||
assert n == 2
|
|
||||||
|
|
||||||
trades = Trade.query.all()
|
|
||||||
for trade in trades:
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
trade.update_trade(oobjs)
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
# /monthly 12
|
# /monthly 12
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
|
@ -716,24 +619,14 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
telegram._monthly(update=update, context=context)
|
telegram._monthly(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Monthly Profit over the last 12 months</b>:' in msg_mock.call_args_list[0][0][0]
|
assert 'Monthly Profit over the last 12 months</b>:' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
assert ' 9.83 USDT' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
assert ' 10.81 USD' in msg_mock.call_args_list[0][0][0]
|
||||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
assert '(3)' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
|
# The one-digit months should contain a zero, Eg: September 2021 = "2021-09"
|
||||||
# Since we loaded the last 12 months, any month should appear
|
# Since we loaded the last 12 months, any month should appear
|
||||||
assert str('-09') in msg_mock.call_args_list[0][0][0]
|
assert str('-09') in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker
|
|
||||||
)
|
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
|
||||||
patch_get_signal(freqtradebot)
|
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
@ -754,16 +647,16 @@ def test_monthly_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
assert str('Monthly Profit over the last 6 months</b>:') in msg_mock.call_args_list[0][0][0]
|
assert str('Monthly Profit over the last 6 months</b>:') in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_sell_order_usdt, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker_usdt,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
telegram._profit(update=update, context=MagicMock())
|
telegram._profit(update=update, context=MagicMock())
|
||||||
|
@ -775,10 +668,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
# Test with invalid 2nd argument (should silently pass)
|
# Test with invalid 2nd argument (should silently pass)
|
||||||
context.args = ["aaa"]
|
context.args = ["aaa"]
|
||||||
|
@ -786,15 +675,16 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'No closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01)
|
mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=1000)
|
||||||
assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
assert ('∙ `0.298 USDT (0.50%) (0.03 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
in msg_mock.call_args_list[-1][0][0])
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up)
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
oobj = Order.parse_from_ccxt_object(
|
||||||
|
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
|
|
||||||
trade.close_date = datetime.now(timezone.utc)
|
trade.close_date = datetime.now(timezone.utc)
|
||||||
|
@ -805,20 +695,22 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
telegram._profit(update=update, context=context)
|
telegram._profit(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
in msg_mock.call_args_list[-1][0][0])
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert ('∙ `0.00006217 BTC (6.20%) (0.62 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`'
|
||||||
in msg_mock.call_args_list[-1][0][0])
|
in msg_mock.call_args_list[-1][0][0])
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `6.253 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert '*Trading volume:* `60 USDT`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('is_short', [True, False])
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
|
def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None:
|
||||||
limit_buy_order, limit_sell_order, mocker, is_short) -> None:
|
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -1350,71 +1242,43 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None:
|
||||||
assert fbuy_mock.call_count == 1
|
assert fbuy_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
telegram._performance(update=update, context=MagicMock())
|
telegram._performance(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>XRP/USDT\t9.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_entry_tag_performance_handle(
|
def test_telegram_entry_tag_performance_handle(
|
||||||
default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None:
|
default_conf_usdt, update, ticker, fee, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
create_mock_trades_usdt(fee)
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.enter_tag = "TESTBUY"
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
telegram._enter_tag_performance(update=update, context=context)
|
telegram._enter_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>TEST1\t3.987 USDT (5.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
context.args = [trade.pair]
|
context.args = ['XRP/USDT']
|
||||||
telegram._enter_tag_performance(update=update, context=context)
|
telegram._enter_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
@ -1427,37 +1291,24 @@ def test_telegram_entry_tag_performance_handle(
|
||||||
assert "Error" in msg_mock.call_args_list[0][0][0]
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
create_mock_trades_usdt(fee)
|
||||||
freqtradebot.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
trade.exit_reason = 'TESTSELL'
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
telegram._exit_reason_performance(update=update, context=context)
|
telegram._exit_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>roi\t9.842 USDT (10.00%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
context.args = [trade.pair]
|
context.args = ['XRP/USDT']
|
||||||
|
|
||||||
telegram._exit_reason_performance(update=update, context=context)
|
telegram._exit_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
|
@ -1471,43 +1322,27 @@ def test_telegram_exit_reason_performance_handle(default_conf, update, ticker, f
|
||||||
assert "Error" in msg_mock.call_args_list[0][0][0]
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.enter_positions()
|
create_mock_trades_usdt(fee)
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
trade.enter_tag = "TESTBUY"
|
|
||||||
trade.exit_reason = "TESTSELL"
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.is_open = False
|
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
telegram._mix_tag_performance(update=update, context=context)
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
|
assert ('<code>TEST3 roi\t9.842 USDT (10.00%) (1)</code>'
|
||||||
in msg_mock.call_args_list[0][0][0])
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
context.args = [trade.pair]
|
context.args = ['XRP/USDT']
|
||||||
telegram._mix_tag_performance(update=update, context=context)
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
@ -1847,7 +1682,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
|
||||||
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
||||||
|
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
f'\N{LARGE BLUE CIRCLE} *Binance:* {enter} ETH/BTC (#1)\n'
|
f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n'
|
||||||
f'*Enter Tag:* `{enter_signal}`\n'
|
f'*Enter Tag:* `{enter_signal}`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
f'{leverage_text}'
|
f'{leverage_text}'
|
||||||
|
@ -1887,7 +1722,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, en
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'reason': CANCEL_REASON['TIMEOUT']
|
'reason': CANCEL_REASON['TIMEOUT']
|
||||||
})
|
})
|
||||||
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* '
|
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance (dry):* '
|
||||||
'Cancelling enter Order for ETH/BTC (#1). '
|
'Cancelling enter Order for ETH/BTC (#1). '
|
||||||
'Reason: cancelled due to timeout.')
|
'Reason: cancelled due to timeout.')
|
||||||
|
|
||||||
|
@ -1949,7 +1784,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
|
||||||
})
|
})
|
||||||
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
|
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
f'\N{CHECK MARK} *Binance:* {entered}ed ETH/BTC (#1)\n'
|
f'\N{CHECK MARK} *Binance (dry):* {entered}ed ETH/BTC (#1)\n'
|
||||||
f'*Enter Tag:* `{enter_signal}`\n'
|
f'*Enter Tag:* `{enter_signal}`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
f"{leverage_text}"
|
f"{leverage_text}"
|
||||||
|
@ -1987,7 +1822,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
'*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
|
||||||
'*Enter Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
|
@ -2021,7 +1856,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
'close_date': arrow.utcnow(),
|
'close_date': arrow.utcnow(),
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41%`\n'
|
'*Unrealized Profit:* `-57.41%`\n'
|
||||||
'*Enter Tag:* `buy_signal1`\n'
|
'*Enter Tag:* `buy_signal1`\n'
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
|
@ -2050,10 +1885,12 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
||||||
'reason': 'Cancelled on exchange'
|
'reason': 'Cancelled on exchange'
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance:* Cancelling exit Order for KEY/ETH (#1).'
|
'\N{WARNING SIGN} *Binance (dry):* Cancelling exit Order for KEY/ETH (#1).'
|
||||||
' Reason: Cancelled on exchange.')
|
' Reason: Cancelled on exchange.')
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
# Test with live mode (no dry appendix)
|
||||||
|
telegram._config['dry_run'] = False
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
'type': RPCMessageType.EXIT_CANCEL,
|
'type': RPCMessageType.EXIT_CANCEL,
|
||||||
'trade_id': 1,
|
'trade_id': 1,
|
||||||
|
@ -2102,7 +1939,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
|
||||||
|
|
||||||
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance:* Exited KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n'
|
||||||
'*Profit:* `-57.41%`\n'
|
'*Profit:* `-57.41%`\n'
|
||||||
f'*Enter Tag:* `{enter_signal}`\n'
|
f'*Enter Tag:* `{enter_signal}`\n'
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
|
@ -2158,6 +1995,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None:
|
||||||
def test_send_msg_buy_notification_no_fiat(
|
def test_send_msg_buy_notification_no_fiat(
|
||||||
default_conf, mocker, message_type, enter, enter_signal, leverage) -> None:
|
default_conf, mocker, message_type, enter, enter_signal, leverage) -> None:
|
||||||
del default_conf['fiat_display_currency']
|
del default_conf['fiat_display_currency']
|
||||||
|
default_conf['dry_run'] = False
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
|
@ -2227,7 +2065,7 @@ def test_send_msg_sell_notification_no_fiat(
|
||||||
|
|
||||||
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
'\N{WARNING SIGN} *Binance:* Exiting KEY/ETH (#1)\n'
|
'\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
|
||||||
'*Unrealized Profit:* `-57.41%`\n'
|
'*Unrealized Profit:* `-57.41%`\n'
|
||||||
f'*Enter Tag:* `{enter_signal}`\n'
|
f'*Enter Tag:* `{enter_signal}`\n'
|
||||||
'*Exit Reason:* `stop_loss`\n'
|
'*Exit Reason:* `stop_loss`\n'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -7,6 +8,7 @@ from requests import RequestException
|
||||||
|
|
||||||
from freqtrade.enums import ExitType, RPCMessageType
|
from freqtrade.enums import ExitType, RPCMessageType
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.discord import Discord
|
||||||
from freqtrade.rpc.webhook import Webhook
|
from freqtrade.rpc.webhook import Webhook
|
||||||
from tests.conftest import get_patched_freqtradebot, log_has
|
from tests.conftest import get_patched_freqtradebot, log_has
|
||||||
|
|
||||||
|
@ -406,3 +408,42 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog):
|
||||||
webhook._send_msg(msg)
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_discord(default_conf, mocker):
|
||||||
|
|
||||||
|
default_conf["discord"] = {
|
||||||
|
'enabled': True,
|
||||||
|
'webhook_url': "https://webhookurl..."
|
||||||
|
}
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
discord = Discord(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.EXIT_FILL,
|
||||||
|
'trade_id': 1,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'direction': 'Long',
|
||||||
|
'gain': "profit",
|
||||||
|
'close_rate': 0.005,
|
||||||
|
'amount': 0.8,
|
||||||
|
'order_type': 'limit',
|
||||||
|
'open_date': datetime.now() - timedelta(days=1),
|
||||||
|
'close_date': datetime.now(),
|
||||||
|
'open_rate': 0.004,
|
||||||
|
'current_rate': 0.005,
|
||||||
|
'profit_amount': 0.001,
|
||||||
|
'profit_ratio': 0.20,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'enter_tag': 'enter_tagggg',
|
||||||
|
'exit_reason': ExitType.STOP_LOSS.value,
|
||||||
|
}
|
||||||
|
discord.send_msg(msg=msg)
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'embeds' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'title' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||||
|
assert 'color' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||||
|
assert 'fields' in msg_mock.call_args_list[0][0][0]['embeds'][0]
|
||||||
|
|
|
@ -44,6 +44,11 @@ class HyperoptableStrategy(StrategyTestV2):
|
||||||
})
|
})
|
||||||
return prot
|
return prot
|
||||||
|
|
||||||
|
bot_loop_started = False
|
||||||
|
|
||||||
|
def bot_loop_start(self):
|
||||||
|
self.bot_loop_started = True
|
||||||
|
|
||||||
def bot_start(self, **kwargs) -> None:
|
def bot_start(self, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Parameters can also be defined here ...
|
Parameters can also be defined here ...
|
||||||
|
|
|
@ -178,8 +178,8 @@ class StrategyTestV3(IStrategy):
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_leverage: float, max_leverage: float, side: str,
|
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str],
|
||||||
**kwargs) -> float:
|
side: str, **kwargs) -> float:
|
||||||
# Return 3.0 in all cases.
|
# Return 3.0 in all cases.
|
||||||
# Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly.
|
# Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly.
|
||||||
|
|
||||||
|
|
175
tests/strategy/strats/strategy_test_v3_analysis.py
Normal file
175
tests/strategy/strats/strategy_test_v3_analysis.py
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
|
||||||
|
RealParameter)
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyTestV3Analysis(IStrategy):
|
||||||
|
"""
|
||||||
|
Strategy used by tests freqtrade bot.
|
||||||
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
|
Please look at the SampleStrategy in the user_data/strategy directory
|
||||||
|
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||||
|
for samples and inspiration.
|
||||||
|
"""
|
||||||
|
INTERFACE_VERSION = 3
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy
|
||||||
|
minimal_roi = {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
stoploss = -0.10
|
||||||
|
|
||||||
|
# Optimal timeframe for the strategy
|
||||||
|
timeframe = '5m'
|
||||||
|
|
||||||
|
# Optional order type mapping
|
||||||
|
order_types = {
|
||||||
|
'entry': 'limit',
|
||||||
|
'exit': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Number of candles the strategy requires before producing valid signals
|
||||||
|
startup_candle_count: int = 20
|
||||||
|
|
||||||
|
# Optional time in force for orders
|
||||||
|
order_time_in_force = {
|
||||||
|
'entry': 'gtc',
|
||||||
|
'exit': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
|
buy_params = {
|
||||||
|
'buy_rsi': 35,
|
||||||
|
# Intentionally not specified, so "default" is tested
|
||||||
|
# 'buy_plusdi': 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
sell_params = {
|
||||||
|
'sell_rsi': 74,
|
||||||
|
'sell_minusdi': 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
||||||
|
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
|
||||||
|
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
||||||
|
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
|
||||||
|
load=False)
|
||||||
|
protection_enabled = BooleanParameter(default=True)
|
||||||
|
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||||
|
|
||||||
|
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
|
||||||
|
# @property
|
||||||
|
# def protections(self):
|
||||||
|
# prot = []
|
||||||
|
# if self.protection_enabled.value:
|
||||||
|
# prot.append({
|
||||||
|
# "method": "CooldownPeriod",
|
||||||
|
# "stop_duration_candles": self.protection_cooldown_lookback.value
|
||||||
|
# })
|
||||||
|
# return prot
|
||||||
|
|
||||||
|
bot_started = False
|
||||||
|
|
||||||
|
def bot_start(self):
|
||||||
|
self.bot_started = True
|
||||||
|
|
||||||
|
def informative_pairs(self):
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
# Momentum Indicator
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
# ADX
|
||||||
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
|
||||||
|
# Minus Directional Indicator / Movement
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
|
|
||||||
|
# Plus Directional Indicator / Movement
|
||||||
|
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
|
||||||
|
# Stoch fast
|
||||||
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
|
dataframe['fastk'] = stoch_fast['fastk']
|
||||||
|
|
||||||
|
# Bollinger bands
|
||||||
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_middleband'] = bollinger['mid']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
|
||||||
|
# EMA - Exponential Moving Average
|
||||||
|
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['rsi'] < self.buy_rsi.value) &
|
||||||
|
(dataframe['fastd'] < 35) &
|
||||||
|
(dataframe['adx'] > 30) &
|
||||||
|
(dataframe['plus_di'] > self.buy_plusdi.value)
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 65) &
|
||||||
|
(dataframe['plus_di'] > self.buy_plusdi.value)
|
||||||
|
),
|
||||||
|
['enter_long', 'enter_tag']] = 1, 'enter_tag_long'
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value)
|
||||||
|
),
|
||||||
|
['enter_short', 'enter_tag']] = 1, 'enter_tag_short'
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) |
|
||||||
|
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
||||||
|
) &
|
||||||
|
(dataframe['adx'] > 10) &
|
||||||
|
(dataframe['minus_di'] > 0)
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 70) &
|
||||||
|
(dataframe['minus_di'] > self.sell_minusdi.value)
|
||||||
|
),
|
||||||
|
['exit_long', 'exit_tag']] = 1, 'exit_tag_long'
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)
|
||||||
|
),
|
||||||
|
['exit_long', 'exit_tag']] = 1, 'exit_tag_short'
|
||||||
|
|
||||||
|
return dataframe
|
|
@ -20,7 +20,8 @@ from freqtrade.strategy.hyper import detect_parameters
|
||||||
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
|
from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
|
||||||
DecimalParameter, IntParameter, RealParameter)
|
DecimalParameter, IntParameter, RealParameter)
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
|
from tests.conftest import (CURRENT_TEST_STRATEGY, TRADE_SIDES, create_mock_trades, log_has,
|
||||||
|
log_has_re)
|
||||||
|
|
||||||
from .strats.strategy_test_v3 import StrategyTestV3
|
from .strats.strategy_test_v3 import StrategyTestV3
|
||||||
|
|
||||||
|
@ -615,6 +616,7 @@ def test_leverage_callback(default_conf, side) -> None:
|
||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=5.0,
|
max_leverage=5.0,
|
||||||
side=side,
|
side=side,
|
||||||
|
entry_tag=None,
|
||||||
) == 1
|
) == 1
|
||||||
|
|
||||||
default_conf['strategy'] = CURRENT_TEST_STRATEGY
|
default_conf['strategy'] = CURRENT_TEST_STRATEGY
|
||||||
|
@ -626,6 +628,7 @@ def test_leverage_callback(default_conf, side) -> None:
|
||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=5.0,
|
max_leverage=5.0,
|
||||||
side=side,
|
side=side,
|
||||||
|
entry_tag='entry_tag_test',
|
||||||
) == 3
|
) == 3
|
||||||
|
|
||||||
|
|
||||||
|
@ -810,6 +813,28 @@ def test_strategy_safe_wrapper(value):
|
||||||
assert ret == value
|
assert ret == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_strategy_safe_wrapper_trade_copy(fee):
|
||||||
|
create_mock_trades(fee)
|
||||||
|
|
||||||
|
def working_method(trade):
|
||||||
|
assert len(trade.orders) > 0
|
||||||
|
assert trade.orders
|
||||||
|
trade.orders = []
|
||||||
|
assert len(trade.orders) == 0
|
||||||
|
return trade
|
||||||
|
|
||||||
|
trade = Trade.get_open_trades()[0]
|
||||||
|
# Don't assert anything before strategy_wrapper.
|
||||||
|
# This ensures that relationship loading works correctly.
|
||||||
|
ret = strategy_safe_wrapper(working_method, message='DeadBeef')(trade=trade)
|
||||||
|
assert isinstance(ret, Trade)
|
||||||
|
assert id(trade) != id(ret)
|
||||||
|
# Did not modify the original order
|
||||||
|
assert len(trade.orders) > 0
|
||||||
|
assert len(ret.orders) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_parameters():
|
def test_hyperopt_parameters():
|
||||||
from skopt.space import Categorical, Integer, Real
|
from skopt.space import Categorical, Integer, Real
|
||||||
with pytest.raises(OperationalException, match=r"Name is determined.*"):
|
with pytest.raises(OperationalException, match=r"Name is determined.*"):
|
||||||
|
|
|
@ -34,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) == 5
|
assert len(strategies) == 6
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,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) == 6
|
assert len(strategies) == 7
|
||||||
# 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]) == 5
|
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 None]) == 1
|
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -210,13 +210,14 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
|
||||||
#
|
#
|
||||||
# mocking the ticker: price is falling ...
|
# mocking the ticker: price is falling ...
|
||||||
enter_price = limit_order['buy']['price']
|
enter_price = limit_order['buy']['price']
|
||||||
|
ticker_val = {
|
||||||
|
'bid': enter_price,
|
||||||
|
'ask': enter_price,
|
||||||
|
'last': enter_price,
|
||||||
|
}
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value=ticker_val),
|
||||||
'bid': enter_price * buy_price_mult,
|
|
||||||
'ask': enter_price * buy_price_mult,
|
|
||||||
'last': enter_price * buy_price_mult,
|
|
||||||
}),
|
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
#############################################
|
#############################################
|
||||||
|
@ -229,9 +230,12 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order['buy'], 'ADA/USDT', 'buy')
|
|
||||||
trade.update_trade(oobj)
|
|
||||||
#############################################
|
#############################################
|
||||||
|
ticker_val.update({
|
||||||
|
'bid': enter_price * buy_price_mult,
|
||||||
|
'ask': enter_price * buy_price_mult,
|
||||||
|
'last': enter_price * buy_price_mult,
|
||||||
|
})
|
||||||
|
|
||||||
# stoploss shoud be hit
|
# stoploss shoud be hit
|
||||||
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||||
|
@ -2147,7 +2151,7 @@ def test_handle_trade(
|
||||||
|
|
||||||
assert trade.close_rate == 2.0 if is_short else 2.2
|
assert trade.close_rate == 2.0 if is_short else 2.2
|
||||||
assert trade.close_profit == close_profit
|
assert trade.close_profit == close_profit
|
||||||
assert trade.calc_profit() == 5.685
|
assert trade.calc_profit(trade.close_rate) == 5.685
|
||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
assert trade.exit_reason == 'sell_signal1'
|
assert trade.exit_reason == 'sell_signal1'
|
||||||
|
|
||||||
|
@ -3771,6 +3775,7 @@ def test_exit_profit_only(
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade.is_short == is_short
|
assert trade.is_short == is_short
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||||
|
trade.update_order(limit_order[eside])
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
if profit_only:
|
if profit_only:
|
||||||
|
@ -3946,9 +3951,9 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_
|
||||||
|
|
||||||
# Test if entry-signal is absent (should sell due to roi = true)
|
# Test if entry-signal is absent (should sell due to roi = true)
|
||||||
if is_short:
|
if is_short:
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_short=False)
|
patch_get_signal(freqtrade, enter_long=False, exit_short=False, exit_tag='something')
|
||||||
else:
|
else:
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_long=False)
|
patch_get_signal(freqtrade, enter_long=False, exit_long=False, exit_tag='something')
|
||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is True
|
||||||
assert trade.exit_reason == ExitType.ROI.value
|
assert trade.exit_reason == ExitType.ROI.value
|
||||||
|
|
||||||
|
@ -4059,6 +4064,7 @@ def test_trailing_stop_loss_positive(
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade.is_short == is_short
|
assert trade.is_short == is_short
|
||||||
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
|
||||||
|
trade.update_order(limit_order[eside])
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
# stop-loss not reached
|
# stop-loss not reached
|
||||||
|
@ -4802,10 +4808,19 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
|
||||||
assert len(Order.get_open_orders()) == 2
|
assert len(Order.get_open_orders()) == 2
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=ExchangeError)
|
||||||
freqtrade.startup_update_open_orders()
|
freqtrade.startup_update_open_orders()
|
||||||
assert log_has_re(r"Error updating Order .*", caplog)
|
assert log_has_re(r"Error updating Order .*", caplog)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
|
||||||
|
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order')
|
||||||
|
# Orders which are no longer found after X days should be assumed as canceled.
|
||||||
|
freqtrade.startup_update_open_orders()
|
||||||
|
assert log_has_re(r"Order is older than \d days.*", caplog)
|
||||||
|
assert hto_mock.call_count == 2
|
||||||
|
assert hto_mock.call_args_list[0][0][0]['status'] == 'canceled'
|
||||||
|
assert hto_mock.call_args_list[1][0][0]['status'] == 'canceled'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
|
|
@ -606,9 +606,9 @@ def test_calc_open_close_trade_price(
|
||||||
trade.close_rate = 2.2
|
trade.close_rate = 2.2
|
||||||
trade.recalc_open_trade_value()
|
trade.recalc_open_trade_value()
|
||||||
assert isclose(trade._calc_open_trade_value(), open_value)
|
assert isclose(trade._calc_open_trade_value(), open_value)
|
||||||
assert isclose(trade.calc_close_trade_value(), close_value)
|
assert isclose(trade.calc_close_trade_value(trade.close_rate), close_value)
|
||||||
assert isclose(trade.calc_profit(), round(profit, 8))
|
assert isclose(trade.calc_profit(trade.close_rate), round(profit, 8))
|
||||||
assert pytest.approx(trade.calc_profit_ratio()) == profit_ratio
|
assert pytest.approx(trade.calc_profit_ratio(trade.close_rate)) == profit_ratio
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -660,7 +660,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
|
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
|
||||||
trade.update_trade(oobj)
|
trade.update_trade(oobj)
|
||||||
assert trade.calc_close_trade_value() == 0.0
|
assert trade.calc_close_trade_value(trade.close_rate) == 0.0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -813,7 +813,7 @@ def test_calc_close_trade_price(
|
||||||
funding_fees=funding_fees
|
funding_fees=funding_fees
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'close_trade'
|
trade.open_order_id = 'close_trade'
|
||||||
assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result
|
assert round(trade.calc_close_trade_value(rate=close_rate), 8) == result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -884,6 +884,17 @@ def test_calc_close_trade_price(
|
||||||
('binance', False, 3, 2.2, 0.0025, 4.684999, 0.23366583, futures, -1),
|
('binance', False, 3, 2.2, 0.0025, 4.684999, 0.23366583, futures, -1),
|
||||||
('binance', True, 1, 2.2, 0.0025, -7.315, -0.12222222, futures, -1),
|
('binance', True, 1, 2.2, 0.0025, -7.315, -0.12222222, futures, -1),
|
||||||
('binance', True, 3, 2.2, 0.0025, -7.315, -0.36666666, futures, -1),
|
('binance', True, 3, 2.2, 0.0025, -7.315, -0.36666666, futures, -1),
|
||||||
|
|
||||||
|
# FUTURES, funding_fee=0
|
||||||
|
('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309, futures, 0),
|
||||||
|
('binance', False, 3, 2.1, 0.0025, 2.6925, 0.13428928, futures, 0),
|
||||||
|
('binance', True, 1, 2.1, 0.0025, -3.3074999, -0.05526316, futures, 0),
|
||||||
|
('binance', True, 3, 2.1, 0.0025, -3.3074999, -0.16578947, futures, 0),
|
||||||
|
|
||||||
|
('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815, futures, 0),
|
||||||
|
('binance', False, 3, 1.9, 0.0025, -3.2925, -0.16421446, futures, 0),
|
||||||
|
('binance', True, 1, 1.9, 0.0025, 2.7075, 0.0452381, futures, 0),
|
||||||
|
('binance', True, 3, 1.9, 0.0025, 2.7075, 0.13571429, futures, 0),
|
||||||
])
|
])
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_profit(
|
def test_calc_profit(
|
||||||
|
@ -2064,6 +2075,24 @@ def test_get_trades_proxy(fee, use_db, is_short):
|
||||||
Trade.use_db = True
|
Trade.use_db = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_get_trades__query(fee, is_short):
|
||||||
|
query = Trade.get_trades([])
|
||||||
|
# without orders there should be no join issued.
|
||||||
|
query1 = Trade.get_trades([], include_orders=False)
|
||||||
|
|
||||||
|
assert "JOIN orders" in str(query)
|
||||||
|
assert "JOIN orders" not in str(query1)
|
||||||
|
|
||||||
|
create_mock_trades(fee, is_short)
|
||||||
|
query = Trade.get_trades([])
|
||||||
|
query1 = Trade.get_trades([], include_orders=False)
|
||||||
|
|
||||||
|
assert "JOIN orders" in str(query)
|
||||||
|
assert "JOIN orders" not in str(query1)
|
||||||
|
|
||||||
|
|
||||||
def test_get_trades_backtest():
|
def test_get_trades_backtest():
|
||||||
Trade.use_db = False
|
Trade.use_db = False
|
||||||
with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"):
|
with pytest.raises(NotImplementedError, match=r"`Trade.get_trades\(\)` not .*"):
|
||||||
|
@ -2258,6 +2287,7 @@ def test_Trade_object_idem():
|
||||||
'get_exit_reason_performance',
|
'get_exit_reason_performance',
|
||||||
'get_enter_tag_performance',
|
'get_enter_tag_performance',
|
||||||
'get_mix_tag_performance',
|
'get_mix_tag_performance',
|
||||||
|
'get_trading_volume',
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2687,5 +2717,7 @@ def test_order_to_ccxt(limit_buy_order_open):
|
||||||
del raw_order['fee']
|
del raw_order['fee']
|
||||||
del raw_order['datetime']
|
del raw_order['datetime']
|
||||||
del raw_order['info']
|
del raw_order['info']
|
||||||
|
assert raw_order['stopPrice'] is None
|
||||||
|
del raw_order['stopPrice']
|
||||||
del limit_buy_order_open['datetime']
|
del limit_buy_order_open['datetime']
|
||||||
assert raw_order == limit_buy_order_open
|
assert raw_order == limit_buy_order_open
|
||||||
|
|
Loading…
Reference in New Issue
Block a user