Merge branch 'develop' into pr/sobeit2020/4218

This commit is contained in:
Matthias 2021-01-29 19:11:19 +01:00
commit d8353bc90e
86 changed files with 705 additions and 718 deletions

View File

@ -3,13 +3,15 @@ FROM freqtradeorg/freqtrade:develop
# Install dependencies
COPY requirements-dev.txt /freqtrade/
RUN apt-get update \
&& apt-get -y install git sudo vim \
&& apt-get -y install git mercurial sudo vim \
&& apt-get clean \
&& pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \
&& useradd -u 1000 -U -m ftuser \
&& mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \
&& echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \
&& echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \
&& mv /root/.local /home/ftuser/.local/ \
&& chown ftuser:ftuser -R /home/ftuser/.local/ \
&& chown ftuser: -R /home/ftuser/
USER ftuser

View File

@ -79,13 +79,13 @@ jobs:
- name: Backtesting
run: |
cp config.json.example config.json
cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
@ -171,13 +171,13 @@ jobs:
- name: Backtesting
run: |
cp config.json.example config.json
cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all
@ -238,13 +238,13 @@ jobs:
- name: Backtesting
run: |
cp config.json.example config.json
cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
- name: Hyperopt
run: |
cp config.json.example config.json
cp config_bittrex.json.example config.json
freqtrade create-userdir --userdir user_data
freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all

View File

@ -26,12 +26,12 @@ jobs:
# - coveralls || true
name: pytest
- script:
- cp config.json.example config.json
- cp config_bittrex.json.example config.json
- freqtrade create-userdir --userdir user_data
- freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy
name: backtest
- script:
- cp config.json.example config.json
- cp config_bittrex.json.example config.json
- freqtrade create-userdir --userdir user_data
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily
name: hyperopt

View File

@ -12,7 +12,7 @@ Few pointers for contributions:
- New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR.
- PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished).
If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
## Getting started

View File

@ -1,5 +1,4 @@
include LICENSE
include README.md
include config.json.example
recursive-include freqtrade *.py
recursive-include freqtrade/templates/ *.j2 *.ipynb

View File

@ -113,7 +113,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/start`: Starts the trader.
- `/stop`: Stops the trader.
- `/stopbuy`: Stop entering new trades.
- `/status [table]`: Lists all open trades.
- `/status <trade_id>|[table]`: Lists all or specific open trades.
- `/profit`: Lists cumulative profit from all finished trades
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
- `/performance`: Show performance of each finished trade grouped by pair
@ -138,7 +138,7 @@ For any questions not covered by the documentation or for further information ab
Please check out our [discord server](https://discord.gg/MA9v74M).
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA).
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)

View File

@ -30,7 +30,7 @@ if [ $? -ne 0 ]; then
fi
# Run backtest
docker run --rm -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy
docker run --rm -v $(pwd)/config_bittrex.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy DefaultStrategy
if [ $? -ne 0 ]; then
echo "failed running backtest"

View File

@ -90,6 +90,7 @@
"username": "freqtrader",
"password": "SuperSecurePassword"
},
"bot_name": "freqtrade",
"initial_state": "running",
"forcebuy_enable": false,
"internals": {

View File

@ -85,6 +85,7 @@
"username": "freqtrader",
"password": "SuperSecurePassword"
},
"bot_name": "freqtrade",
"initial_state": "running",
"forcebuy_enable": false,
"internals": {

View File

@ -177,6 +177,7 @@
"username": "freqtrader",
"password": "SuperSecurePassword"
},
"bot_name": "freqtrade",
"db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running",
"forcebuy_enable": false,

View File

@ -95,6 +95,7 @@
"username": "freqtrader",
"password": "SuperSecurePassword"
},
"bot_name": "freqtrade",
"initial_state": "running",
"forcebuy_enable": false,
"internals": {

View File

@ -63,7 +63,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results['profit_percent'].sum()
total_profit = results['profit_ratio'].sum()
trade_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
@ -77,10 +77,10 @@ Currently, the arguments are:
* `results`: DataFrame containing the result
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_percent, profit_abs, open_date, open_rate, open_fee, close_date, close_rate, close_fee, amount, trade_duration, open_at_end, sell_reason`
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
* `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the hyperopting TimeFrame
* `min_date`: End date of the hyperopting TimeFrame
* `min_date`: Start date of the timerange used
* `min_date`: End date of the timerange used
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.

View File

@ -262,9 +262,9 @@ It contains some useful key metrics about performance of your strategy on backte
```
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this.
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
- `Total trades`: Identical to the total trades of the backtest output table.
- `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table.
- `Total Profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table.
- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade

View File

@ -49,8 +49,9 @@ 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.
* Load historic data for configured pairlist.
* Calculate indicators (calls `populate_indicators()`).
* Calls `populate_buy_trend()` and `populate_sell_trend()`
* Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair)
* Loops per candle simulating entry and exit points.
* Generate backtest report output

View File

@ -16,8 +16,7 @@ In some advanced use cases, multiple configuration files can be specified and us
If you used the [Quick start](installation.md/#quick-start) method for installing
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template
for your bot configuration.
If default configuration file is not created we recommend you to use `freqtrade new-config --config config.json` to generate a basic configuration file.
The Freqtrade configuration file is to be written in the JSON format.
@ -83,7 +82,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **Datatype:** List
| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
| `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) <br> **Datatype:** Dict
@ -110,6 +109,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `api_server.verbosity` | Logging verbosity. `info` will print all RPC Calls, while "error" will only display errors. <br>**Datatype:** Enum, either `info` or `error`. Defaults to `info`.
| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. <br>**Keep it in secret, do not disclose publicly.**<br> **Datatype:** String
| `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances. <br> **Datatype:** String, SQLAlchemy connect string
| `initial_state` | Defines the initial application state. More information below. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `stopped` or `running`
| `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean
@ -146,6 +146,7 @@ Values set in the configuration file always overwrite values set in the strategy
* `protections`
* `use_sell_signal` (ask_strategy)
* `sell_profit_only` (ask_strategy)
* `sell_profit_offset` (ask_strategy)
* `ignore_roi_if_buy_signal` (ask_strategy)
* `ignore_buying_expired_candle_after` (ask_strategy)
@ -276,6 +277,22 @@ before asking the strategy if we should buy or a sell an asset. After each wait
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
the static list of pairs) if we should buy.
### Ignoring expired candles
When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it.
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy:
``` json
"ask_strategy":{
"ignore_buying_expired_candle_after": 300,
"price_side": "bid",
// ...
},
```
### Understand order_types
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
@ -675,48 +692,6 @@ export HTTPS_PROXY="http://addr:port"
freqtrade
```
## Ignoring expired candles
When working with larger timeframes (for example 1h or more) and using a low `max_open_trades` value, the last candle can be processed as soon as a trade slot becomes available. When processing the last candle, this can lead to a situation where it may not be desirable to use the buy signal on that candle. For example, when using a condition in your strategy where you use a cross-over, that point may have passed too long ago for you to start a trade on it.
In these situations, you can enable the functionality to ignore candles that are beyond a specified period by setting `ask_strategy.ignore_buying_expired_candle_after` to a positive number, indicating the number of seconds after which the buy signal becomes expired.
For example, if your strategy is using a 1h timeframe, and you only want to buy within the first 5 minutes when a new candle comes in, you can add the following configuration to your strategy:
``` jsonc
"ask_strategy":{
"ignore_buying_expired_candle_after" = 300 # 5 minutes
"price_side": "bid",
// ...
},
```
## Embedding Strategies
Freqtrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file.
### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python
```python
from base64 import urlsafe_b64encode
with open(file, 'r') as f:
content = f.read()
content = urlsafe_b64encode(content.encode('utf-8'))
```
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
```json
"strategy": "NameOfStrategy:BASE64String"
```
Please ensure that 'NameOfStrategy' is identical to the strategy name!
## Next step
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).

View File

@ -308,10 +308,13 @@ Since this data is large by default, the files use gzip by default. They are sto
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally.
!!! Warning "do not use"
You should not use this unless you're a kraken user. Most other exchanges provide OHLCV data with sufficient history.
Example call:
```bash
freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --dl-trades
freqtrade download-data --exchange kraken --pairs XRP/EUR ETH/EUR --days 20 --dl-trades
```
!!! Note

View File

@ -2,7 +2,7 @@
This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running.
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) where you can ask questions.
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) where you can ask questions.
## Documentation

View File

@ -1,201 +0,0 @@
## Freqtrade with docker without docker-compose
!!! Warning
The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions.
### Download the official Freqtrade docker image
Pull the image from docker hub.
Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
```bash
docker pull freqtradeorg/freqtrade:stable
# Optionally tag the repository so the run-commands remain shorter
docker tag freqtradeorg/freqtrade:stable freqtrade
```
To update the image, simply run the above commands again and restart your running container.
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
!!! Note "Docker image update frequency"
The official docker images with tags `stable`, `develop` and `latest` are automatically rebuild once a week to keep the base image up-to-date.
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
### Prepare the configuration files
Even though you will use docker, you'll still need some files from the github repository.
#### Clone the git repository
Linux/Mac/Windows with WSL
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
Windows with docker
```bash
git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git
```
#### Copy `config.json.example` to `config.json`
```bash
cd freqtrade
cp -n config.json.example config.json
```
> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page.
#### Create your database file
=== "Dry-Run"
``` bash
touch tradesv3.dryrun.sqlite
```
=== "Production"
``` bash
touch tradesv3.sqlite
```
!!! Warning "Database File Path"
Make sure to use the path to the correct database file when starting the bot in Docker.
### Build your own Docker image
Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building.
To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image.
```bash
docker build -t freqtrade -f docker/Dockerfile.technical .
```
If you are developing using Docker, use `docker/Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies:
```bash
docker build -f docker/Dockerfile.develop -t freqtrade-dev .
```
!!! Warning "Include your config file manually"
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates.
#### Verify the Docker image
After the build process you can verify that the image was created with:
```bash
docker images
```
The output should contain the freqtrade image.
### Run the Docker image
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
```bash
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
!!! Warning
In this example, the database will be created inside the docker instance and will be lost when you refresh your image.
#### Adjust timezone
By default, the container will use UTC timezone.
If you would like to change the timezone use the following commands:
=== "Linux"
``` bash
-v /etc/timezone:/etc/timezone:ro
# Complete command:
docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
=== "MacOS"
```bash
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
!!! Note "MacOS Issues"
The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.<br>
A work-around for this is to start with the MacOS command above
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396).
### Run a restartable docker image
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
#### 1. Move your config file and database
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands.
```bash
mkdir ~/.freqtrade
mv config.json ~/.freqtrade
mv tradesv3.sqlite ~/.freqtrade
```
#### 2. Run the docker image
```bash
docker run -d \
--name freqtrade \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/user_data/:/freqtrade/user_data \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
```
!!! Note
When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match.
!!! Note
All available bot command line parameters can be added to the end of the `docker run` command.
!!! Note
You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system).
### Monitor your Docker instance
You can use the following commands to monitor and manage your container:
```bash
docker logs freqtrade
docker logs -f freqtrade
docker restart freqtrade
docker stop freqtrade
docker start freqtrade
```
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
!!! Note
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
### Backtest with docker
The following assumes that the download/setup of the docker image have been completed successfully.
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
```bash
docker run -d \
--name freqtrade \
-v /etc/localtime:/etc/localtime:ro \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
freqtrade backtesting --strategy AwsomelyProfitableStrategy
```
Head over to the [Backtesting Documentation](backtesting.md) for more details.
!!! Note
Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example).

View File

@ -8,9 +8,7 @@ Start by downloading and installing Docker CE for your platform:
* [Windows](https://docs.docker.com/docker-for-windows/install/)
* [Linux](https://docs.docker.com/install/)
Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start).
Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below.
To simplify running freqtrade, please install [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the below [docker quick start guide](#docker-quick-start).
## Freqtrade with docker-compose
@ -71,7 +69,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a
!!! Question "How to edit the bot configuration?"
You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration.
You can also change the both Strategy and commands by editing the `docker-compose.yml` file.
You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file.
#### Adding a custom strategy
@ -83,7 +81,8 @@ The `SampleStrategy` is run by default.
!!! Warning "`SampleStrategy` is just a demo!"
The `SampleStrategy` is there for your reference and give you ideas for your own strategy.
Please always backtest the strategy and use dry-run for some time before risking real money!
Please always backtest your strategy and use dry-run for some time before risking real money!
You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md).
Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above).
@ -91,18 +90,23 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li
docker-compose up -d
```
#### Monitoring the bot
You can check for running instances with `docker-compose ps`.
This should list the service `freqtrade` as `running`. If that's not the case, best check the logs (see next point).
#### Docker-compose logs
Logs will be located at: `user_data/logs/freqtrade.log`.
You can check the latest log with the command `docker-compose logs -f`.
Logs will be written to: `user_data/logs/freqtrade.log`.
You can also check the latest log with the command `docker-compose logs -f`.
#### Database
The database will be at: `user_data/tradesv3.sqlite`
The database will be located at: `user_data/tradesv3.sqlite`
#### Updating freqtrade with docker-compose
To update freqtrade when using `docker-compose` is as simple as running the following 2 commands:
Updating freqtrade when using `docker-compose` is as simple as running the following 2 commands:
``` bash
# Download the latest image
@ -120,7 +124,7 @@ This will first pull the latest image, and will then restart the container with
Advanced users may edit the docker-compose file further to include all possible options or arguments.
All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade <command> <optional arguments>`.
All freqtrade arguments will be available by running `docker-compose run --rm freqtrade <command> <optional arguments>`.
!!! Note "`docker-compose run --rm`"
Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command).

View File

@ -143,7 +143,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD
### Why does it take a long time to run hyperopt?
* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you.
* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you.
* If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers:

View File

@ -35,7 +35,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
#### Static Pair List
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration.
By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. The pairlist also supports wildcards (in regex-style) - so `.*/BTC` will include all pairs with BTC as a stake.
It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.

View File

@ -65,7 +65,7 @@ The below example stops trading for all pairs for 4 candles after the last trade
`MaxDrawdown` uses all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after the last trade - assuming that the bot needs some time to let markets recover.
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
```json
"protections": [
@ -77,7 +77,6 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri
"max_allowed_drawdown": 0.2
},
],
```
#### Low Profit Pairs

View File

@ -65,7 +65,7 @@ For any questions not covered by the documentation or for further information ab
Please check out our [discord server](https://discord.gg/MA9v74M).
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA).
You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA).
## Ready to try?

View File

@ -2,14 +2,13 @@
This page explains how to prepare your environment for running the bot.
The documentation describes various ways to install freqtrade
* [Scrip Installation](#script-installation)
* [Manual Installation](#manual-installation)
* [Installation with Conda](#installation-with-conda)
* [Docker images](docker.md) (separate page)
* [Docker images](docker_quickstart.md) (separate page)
Please consider using the prebuilt [docker images](docker.md) to get started quickly to try freqtrade and evaluate how it works.
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works.
------

View File

@ -1,3 +1,3 @@
mkdocs-material==6.2.4
mkdocs-material==6.2.5
mdx_truly_sane_lists==1.2
pymdown-extensions==8.1

View File

@ -398,3 +398,29 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
```
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
## Embedding Strategies
Freqtrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file.
### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python
```python
from base64 import urlsafe_b64encode
with open(file, 'r') as f:
content = f.read()
content = urlsafe_b64encode(content.encode('utf-8'))
```
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
```json
"strategy": "NameOfStrategy:BASE64String"
```
Please ensure that 'NameOfStrategy' is identical to the strategy name!

View File

@ -653,7 +653,7 @@ The following example queries for the current pair and trades from today, howeve
if self.config['runmode'].value in ('live', 'dry_run'):
trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=1),
Trade.is_open == False,
Trade.is_open.is_(False),
]).order_by(Trade.close_date).all()
# Summarize profit for this pair.
curdayprofit = sum(trade.close_profit for trade in trades)
@ -719,7 +719,7 @@ if self.config['runmode'].value in ('live', 'dry_run'):
# fetch closed trades for the last 2 days
trades = Trade.get_trades([Trade.pair == metadata['pair'],
Trade.open_date > datetime.utcnow() - timedelta(days=2),
Trade.is_open == False,
Trade.is_open.is_(False),
]).all()
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
sumprofit = sum(trade.close_profit for trade in trades)

View File

@ -137,6 +137,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/show_config` | Shows part of the current configuration with relevant settings to operation
| `/logs [limit]` | Show last log messages.
| `/status` | Lists all open trades
| `/status <trade_id>` | Lists one or more specific trade. Separate multiple <trade_id> with a blank space.
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | List all recently closed trades in a table format.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.

View File

@ -1,4 +1,4 @@
We **strongly** recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure).
We **strongly** recommend that Windows users use [Docker](docker_quickstart.md) as this will work much easier and smoother (also more secure).
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
Otherwise, try the instructions below.
@ -52,6 +52,6 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first.
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first.
---

View File

@ -10,6 +10,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
refresh_backtest_trades_data)
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode
@ -42,15 +43,17 @@ def start_download_data(args: Dict[str, Any]) -> None:
"Downloading data requires a list of pairs. "
"Please check the documentation on how to configure this.")
logger.info(f"About to download pairs: {config['pairs']}, "
f"intervals: {config['timeframes']} to {config['datadir']}")
pairs_not_available: List[str] = []
# Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# Manual validations of relevant settings
exchange.validate_pairs(config['pairs'])
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
logger.info(f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}")
for timeframe in config['timeframes']:
exchange.validate_timeframes(timeframe)
@ -58,20 +61,20 @@ def start_download_data(args: Dict[str, Any]) -> None:
if config.get('download_trades'):
pairs_not_available = refresh_backtest_trades_data(
exchange, pairs=config['pairs'], datadir=config['datadir'],
exchange, pairs=expanded_pairs, datadir=config['datadir'],
timerange=timerange, erase=bool(config.get('erase')),
data_format=config['dataformat_trades'])
# Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv(
pairs=config['pairs'], timeframes=config['timeframes'],
pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
data_format_ohlcv=config['dataformat_ohlcv'],
data_format_trades=config['dataformat_trades'],
)
else:
pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=config['pairs'], timeframes=config['timeframes'],
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
data_format=config['dataformat_ohlcv'])

View File

@ -54,7 +54,7 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
return conf
except ValidationError as e:
logger.critical(
f"Invalid configuration. See config.json.example. Reason: {e}"
f"Invalid configuration. Reason: {e}"
)
raise ValidationError(
best_match(Draft4Validator(conf_schema).iter_errors(conf)).message

View File

@ -116,6 +116,7 @@ CONF_SCHEMA = {
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_only_offset_is_reached': {'type': 'boolean'},
'bot_name': {'type': 'string'},
'unfilledtimeout': {
'type': 'object',
'properties': {

View File

@ -2,9 +2,8 @@
Helpers when analyzing backtest data
"""
import logging
from datetime import timezone
from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union
import numpy as np
import pandas as pd
@ -16,9 +15,22 @@ from freqtrade.persistence import Trade, init_db
logger = logging.getLogger(__name__)
# must align with columns in backtest.py
BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "trade_duration",
"open_rate", "close_rate", "open_at_end", "sell_reason"]
# Old format - maybe remove?
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
# Mid-term format, crated by BacktestResult Named Tuple
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
# Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'open_rate', 'close_rate',
'fee_open', 'fee_close', 'trade_duration',
'profit_ratio', 'profit_abs', 'sell_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ]
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
@ -154,7 +166,7 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
)
else:
# old format - only with lists.
df = pd.DataFrame(data, columns=BT_DATA_COLUMNS)
df = pd.DataFrame(data, columns=BT_DATA_COLUMNS_OLD)
df['open_date'] = pd.to_datetime(df['open_date'],
unit='s',
@ -166,7 +178,10 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
utc=True,
infer_datetime_format=True
)
# Create compatibility with new format
df['profit_abs'] = df['close_rate'] - df['open_rate']
if 'profit_ratio' not in df.columns:
df['profit_ratio'] = df['profit_percent']
df = df.sort_values("open_date").reset_index(drop=True)
return df
@ -209,6 +224,20 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
return df_final[df_final['open_trades'] > max_open_trades]
def trade_list_to_dataframe(trades: List[Trade]) -> pd.DataFrame:
"""
Convert list of Trade objects to pandas Dataframe
:param trades: List of trade objects
:return: Dataframe with BT_DATA_COLUMNS
"""
df = pd.DataFrame.from_records([t.to_json() for t in trades], columns=BT_DATA_COLUMNS)
if len(df) > 0:
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[:, 'close_rate'] = df['close_rate'].astype('float64')
return df
def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataFrame:
"""
Load trades from a DB (using dburl)
@ -219,36 +248,10 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF
"""
init_db(db_url, clean_open_orders=False)
columns = ["pair", "open_date", "close_date", "profit", "profit_percent",
"open_rate", "close_rate", "amount", "trade_duration", "sell_reason",
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
"stake_amount", "max_rate", "min_rate", "id", "exchange",
"stop_loss", "initial_stop_loss", "strategy", "timeframe"]
filters = []
if strategy:
filters.append(Trade.strategy == strategy)
trades = pd.DataFrame([(t.pair,
t.open_date.replace(tzinfo=timezone.utc),
t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None,
t.calc_profit(), t.calc_profit_ratio(),
t.open_rate, t.close_rate, t.amount,
(round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
if t.close_date else None),
t.sell_reason,
t.fee_open, t.fee_close,
t.open_rate_requested,
t.close_rate_requested,
t.stake_amount,
t.max_rate,
t.min_rate,
t.id, t.exchange,
t.stop_loss, t.initial_stop_loss,
t.strategy, t.timeframe
)
for t in Trade.get_trades(filters).all()],
columns=columns)
trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
return trades

View File

@ -12,6 +12,7 @@ from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.strategy.interface import SellType
@ -80,10 +81,12 @@ class Edge:
if config.get('fee'):
self.fee = config['fee']
else:
self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0])
self.fee = self.exchange.get_fee(symbol=expand_pairlist(
self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0])
def calculate(self) -> bool:
pairs = self.config['exchange']['pair_whitelist']
pairs = expand_pairlist(self.config['exchange']['pair_whitelist'],
list(self.exchange.markets))
heartbeat = self.edge_config.get('process_throttle_secs')
if (self._last_updated > 0) and (

View File

@ -21,6 +21,7 @@ BAD_EXCHANGES = {
"hitbtc": "This API cannot be used with Freqtrade. "
"Use `hitbtc2` exchange id to access this exchange.",
"phemex": "Does not provide history. ",
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
**dict.fromkeys([
'adara',
'anxpro',

View File

@ -25,6 +25,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier,
retrier_async)
from freqtrade.misc import deep_merge_dicts, safe_value_fallback2
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
CcxtModuleType = Any
@ -65,6 +66,7 @@ class Exchange:
"""
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._config.update(config)
@ -197,10 +199,10 @@ class Exchange:
@property
def markets(self) -> Dict:
"""exchange ccxt markets"""
if not self._api.markets:
if not self._markets:
logger.info("Markets were not loaded. Loading them now..")
self._load_markets()
return self._api.markets
return self._markets
@property
def precisionMode(self) -> str:
@ -290,7 +292,7 @@ class Exchange:
def _load_markets(self) -> None:
""" Initialize markets both sync and async """
try:
self._api.load_markets()
self._markets = self._api.load_markets()
self._load_async_markets()
self._last_markets_refresh = arrow.utcnow().int_timestamp
except ccxt.BaseError as e:
@ -305,7 +307,7 @@ class Exchange:
return None
logger.debug("Performing scheduled market reload..")
try:
self._api.load_markets(reload=True)
self._markets = self._api.load_markets(reload=True)
# Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp
@ -335,8 +337,9 @@ class Exchange:
if not self.markets:
logger.warning('Unable to validate pairs (assuming they are correct).')
return
extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
invalid_pairs = []
for pair in pairs:
for pair in extended_pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format
if self.markets and pair not in self.markets:
@ -658,8 +661,8 @@ class Exchange:
@retrier
def fetch_ticker(self, pair: str) -> dict:
try:
if (pair not in self._api.markets or
self._api.markets[pair].get('active', False) is False):
if (pair not in self.markets or
self.markets[pair].get('active', False) is False):
raise ExchangeError(f"Pair {pair} not available")
data = self._api.fetch_ticker(pair)
return data

View File

@ -200,7 +200,7 @@ class FreqtradeBot(LoggingMixin):
Notify the user when the bot is stopped
and there are still open trades active.
"""
open_trades = Trade.get_trades([Trade.is_open == 1]).all()
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
if len(open_trades) != 0:
msg = {
@ -268,6 +268,10 @@ class FreqtradeBot(LoggingMixin):
Update closed trades without close fees assigned.
Only acts when Orders are in the database, otherwise the last orderid is unknown.
"""
if self.config['dry_run']:
# Updating open orders in dry-run does not make sense and will fail.
return
trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees()
for trade in trades:

View File

@ -6,14 +6,15 @@ This module contains the backtesting logic
import logging
from collections import defaultdict
from copy import deepcopy
from datetime import datetime, timedelta
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.data import history
from freqtrade.data.btanalysis import trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
@ -26,6 +27,7 @@ from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
logger = logging.getLogger(__name__)
@ -40,25 +42,6 @@ LOW_IDX = 5
HIGH_IDX = 6
class BacktestResult(NamedTuple):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_date: datetime
open_rate: float
open_fee: float
close_date: datetime
close_rate: float
close_fee: float
amount: float
trade_duration: float
open_at_end: bool
sell_reason: SellType
class Backtesting:
"""
Backtesting class, this class contains all the logic to run a backtest
@ -76,6 +59,8 @@ class Backtesting:
# Reset keys for backtesting
remove_credentials(self.config)
self.strategylist: List[IStrategy] = []
self.all_results: Dict[str, Dict] = {}
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
dataprovider = DataProvider(self.config, self.exchange)
@ -150,6 +135,10 @@ class Backtesting:
self.strategy.order_types['stoploss_on_exchange'] = False
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
"""
Loads backtest data and returns the data combined with the timerange
as tuple.
"""
timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
@ -257,7 +246,7 @@ class Backtesting:
else:
return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]:
def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[Trade]:
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
sell_row[BUY_IDX], sell_row[SELL_IDX],
@ -269,25 +258,12 @@ class Backtesting:
trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = sell.sell_type
trade.close(closerate, show_msg=False)
return trade
return BacktestResult(pair=trade.pair,
profit_percent=trade.calc_profit_ratio(rate=closerate),
profit_abs=trade.calc_profit(rate=closerate),
open_date=trade.open_date,
open_rate=trade.open_rate,
open_fee=self.fee,
close_date=sell_row[DATE_IDX],
close_rate=closerate,
close_fee=self.fee,
amount=trade.amount,
trade_duration=trade_dur,
open_at_end=False,
sell_reason=sell.sell_type
)
return None
def handle_left_open(self, open_trades: Dict[str, List[Trade]],
data: Dict[str, List[Tuple]]) -> List[BacktestResult]:
data: Dict[str, List[Tuple]]) -> List[Trade]:
"""
Handling of left open trades at the end of backtesting
"""
@ -297,24 +273,11 @@ class Backtesting:
for trade in open_trades[pair]:
sell_row = data[pair][-1]
trade_entry = BacktestResult(pair=trade.pair,
profit_percent=trade.calc_profit_ratio(
rate=sell_row[OPEN_IDX]),
profit_abs=trade.calc_profit(sell_row[OPEN_IDX]),
open_date=trade.open_date,
open_rate=trade.open_rate,
open_fee=self.fee,
close_date=sell_row[DATE_IDX],
close_rate=sell_row[OPEN_IDX],
close_fee=self.fee,
amount=trade.amount,
trade_duration=int((
sell_row[DATE_IDX] - trade.open_date
).total_seconds() // 60),
open_at_end=True,
sell_reason=SellType.FORCE_SELL
)
trades.append(trade_entry)
trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = SellType.FORCE_SELL
trade.close(sell_row[OPEN_IDX], show_msg=False)
trade.is_open = True
trades.append(trade)
return trades
def backtest(self, processed: Dict, stake_amount: float,
@ -341,7 +304,7 @@ class Backtesting:
f"start_date: {start_date}, end_date: {end_date}, "
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
)
trades = []
trades: List[Trade] = []
self.prepare_backtest(enable_protections)
# Use dict of lists with data for performance
@ -422,27 +385,15 @@ class Backtesting:
trades += self.handle_left_open(open_trades, data=data)
return DataFrame.from_records(trades, columns=BacktestResult._fields)
return trade_list_to_dataframe(trades)
def start(self) -> None:
"""
Run backtesting end-to-end
:return: None
"""
data: Dict[str, Any] = {}
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
position_stacking = self.config.get('position_stacking', False)
data, timerange = self.load_bt_data()
all_results = {}
for strat in self.strategylist:
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
backtest_start_time = datetime.now(timezone.utc)
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
if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting.
@ -463,23 +414,42 @@ class Backtesting:
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..')
# Execute backtest and print results
# Execute backtest and store results
results = self.backtest(
processed=preprocessed,
stake_amount=self.config['stake_amount'],
start_date=min_date.datetime,
end_date=max_date.datetime,
max_open_trades=max_open_trades,
position_stacking=position_stacking,
position_stacking=self.config.get('position_stacking', False),
enable_protections=self.config.get('enable_protections', False),
)
all_results[self.strategy.get_strategy_name()] = {
backtest_end_time = datetime.now(timezone.utc)
self.all_results[self.strategy.get_strategy_name()] = {
'results': results,
'config': self.strategy.config,
'locks': PairLocks.locks,
'backtest_start_time': int(backtest_start_time.timestamp()),
'backtest_end_time': int(backtest_end_time.timestamp()),
}
return min_date, max_date
stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date)
def start(self) -> None:
"""
Run backtesting end-to-end
:return: None
"""
data: Dict[str, Any] = {}
data, timerange = self.load_bt_data()
min_date = None
max_date = None
for strat in self.strategylist:
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
stats = generate_backtest_stats(data, self.all_results,
min_date=min_date, max_date=max_date)
if self.config.get('export', False):
store_backtest_stats(self.config['exportfilename'], stats)

View File

@ -42,7 +42,7 @@ class ShortTradeDurHyperOptLoss(IHyperOptLoss):
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results['profit_percent'].sum()
total_profit = results['profit_ratio'].sum()
trade_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)

View File

@ -574,20 +574,20 @@ class Hyperopt:
}
def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict:
wins = len(backtesting_results[backtesting_results.profit_percent > 0])
draws = len(backtesting_results[backtesting_results.profit_percent == 0])
losses = len(backtesting_results[backtesting_results.profit_percent < 0])
wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0])
draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0])
losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0])
return {
'trade_count': len(backtesting_results.index),
'wins': wins,
'draws': draws,
'losses': losses,
'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}",
'avg_profit': backtesting_results.profit_percent.mean() * 100.0,
'median_profit': backtesting_results.profit_percent.median() * 100.0,
'total_profit': backtesting_results.profit_abs.sum(),
'profit': backtesting_results.profit_percent.sum() * 100.0,
'duration': backtesting_results.trade_duration.mean(),
'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0,
'median_profit': backtesting_results['profit_ratio'].median() * 100.0,
'total_profit': backtesting_results['profit_abs'].sum(),
'profit': backtesting_results['profit_ratio'].sum() * 100.0,
'duration': backtesting_results['trade_duration'].mean(),
}
def _format_results_explanation_string(self, results_metrics: Dict) -> str:
@ -650,7 +650,7 @@ class Hyperopt:
# Trim startup period from analyzed dataframe
for pair, df in preprocessed.items():
preprocessed[pair] = trim_dataframe(df, timerange)
min_date, max_date = get_timerange(data)
min_date, max_date = get_timerange(preprocessed)
logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '

View File

@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
"""
Objective function, returns smaller number for better results.
"""
total_profit = results['profit_percent'].sum()
total_profit = results['profit_ratio'].sum()
return 1 - total_profit / EXPECTED_MAX_PROFIT

View File

@ -28,7 +28,7 @@ class SharpeHyperOptLoss(IHyperOptLoss):
Uses Sharpe Ratio calculation.
"""
total_profit = results["profit_percent"]
total_profit = results["profit_ratio"]
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade

View File

@ -34,9 +34,9 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
annual_risk_free_rate = 0.0
risk_free_rate = annual_risk_free_rate / days_in_year
# apply slippage per trade to profit_percent
results.loc[:, 'profit_percent_after_slippage'] = \
results['profit_percent'] - slippage_per_trade_ratio
# apply slippage per trade to profit_ratio
results.loc[:, 'profit_ratio_after_slippage'] = \
results['profit_ratio'] - slippage_per_trade_ratio
# create the index within the min_date and end max_date
t_index = date_range(start=min_date, end=max_date, freq=resample_freq,
@ -44,10 +44,10 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
sum_daily = (
results.resample(resample_freq, on='close_date').agg(
{"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0)
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0)
)
total_profit = sum_daily["profit_percent_after_slippage"] - risk_free_rate
total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate
expected_returns_mean = total_profit.mean()
up_stdev = total_profit.std()

View File

@ -28,7 +28,7 @@ class SortinoHyperOptLoss(IHyperOptLoss):
Uses Sortino Ratio calculation.
"""
total_profit = results["profit_percent"]
total_profit = results["profit_ratio"]
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
@ -36,7 +36,7 @@ class SortinoHyperOptLoss(IHyperOptLoss):
expected_returns_mean = total_profit.sum() / days_period
results['downside_returns'] = 0
results.loc[total_profit < 0, 'downside_returns'] = results['profit_percent']
results.loc[total_profit < 0, 'downside_returns'] = results['profit_ratio']
down_stdev = np.std(results['downside_returns'])
if down_stdev != 0:

View File

@ -36,9 +36,9 @@ class SortinoHyperOptLossDaily(IHyperOptLoss):
days_in_year = 365
minimum_acceptable_return = 0.0
# apply slippage per trade to profit_percent
results.loc[:, 'profit_percent_after_slippage'] = \
results['profit_percent'] - slippage_per_trade_ratio
# apply slippage per trade to profit_ratio
results.loc[:, 'profit_ratio_after_slippage'] = \
results['profit_ratio'] - slippage_per_trade_ratio
# create the index within the min_date and end max_date
t_index = date_range(start=min_date, end=max_date, freq=resample_freq,
@ -46,17 +46,17 @@ class SortinoHyperOptLossDaily(IHyperOptLoss):
sum_daily = (
results.resample(resample_freq, on='close_date').agg(
{"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0)
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0)
)
total_profit = sum_daily["profit_percent_after_slippage"] - minimum_acceptable_return
total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return
expected_returns_mean = total_profit.mean()
sum_daily['downside_returns'] = 0
sum_daily.loc[total_profit < 0, 'downside_returns'] = total_profit
total_downside = sum_daily['downside_returns']
# Here total_downside contains min(0, P - MAR) values,
# where P = sum_daily["profit_percent_after_slippage"]
# where P = sum_daily["profit_ratio_after_slippage"]
down_stdev = math.sqrt((total_downside**2).sum() / len(total_downside))
if down_stdev != 0:

View File

@ -58,14 +58,14 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
"""
Generate one result dict, with "first_column" as key.
"""
profit_sum = result['profit_percent'].sum()
profit_sum = result['profit_ratio'].sum()
profit_total = profit_sum / max_open_trades
return {
'key': first_column,
'trades': len(result),
'profit_mean': result['profit_percent'].mean() if len(result) > 0 else 0.0,
'profit_mean_pct': result['profit_percent'].mean() * 100.0 if len(result) > 0 else 0.0,
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
'profit_sum': profit_sum,
'profit_sum_pct': round(profit_sum * 100.0, 2),
'profit_total_abs': result['profit_abs'].sum(),
@ -124,8 +124,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
for reason, count in results['sell_reason'].value_counts().iteritems():
result = results.loc[results['sell_reason'] == reason]
profit_mean = result['profit_percent'].mean()
profit_sum = result['profit_percent'].sum()
profit_mean = result['profit_ratio'].mean()
profit_sum = result['profit_ratio'].sum()
profit_total = profit_sum / max_open_trades
tabular_data.append(
@ -150,7 +150,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
"""
Generate summary per strategy
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
:param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies
:return: List of Dicts containing the metrics per Strategy
"""
@ -199,15 +199,15 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winner_holding_avg': timedelta(),
'loser_holding_avg': timedelta(),
}
daily_profit = results.resample('1d', on='close_date')['profit_percent'].sum()
daily_profit = results.resample('1d', on='close_date')['profit_ratio'].sum()
worst = min(daily_profit)
best = max(daily_profit)
winning_days = sum(daily_profit > 0)
draw_days = sum(daily_profit == 0)
losing_days = sum(daily_profit < 0)
winning_trades = results.loc[results['profit_percent'] > 0]
losing_trades = results.loc[results['profit_percent'] < 0]
winning_trades = results.loc[results['profit_ratio'] > 0]
losing_trades = results.loc[results['profit_ratio'] < 0]
return {
'backtest_best_day': best,
@ -243,7 +243,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
if not isinstance(results, DataFrame):
continue
config = content['config']
max_open_trades = config['max_open_trades']
max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
stake_currency = config['stake_currency']
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
@ -253,7 +253,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=max_open_trades,
results=results.loc[results['open_at_end']],
results=results.loc[results['is_open']],
skip_nan=True)
daily_stats = generate_daily_stats(results)
best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
@ -273,8 +273,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results,
'total_trades': len(results),
'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0,
'profit_total': results['profit_percent'].sum(),
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_total': results['profit_ratio'].sum() / max_open_trades,
'profit_total_abs': results['profit_abs'].sum(),
'backtest_start': min_date.datetime,
'backtest_start_ts': min_date.int_timestamp * 1000,
@ -282,20 +282,28 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'backtest_end_ts': max_date.int_timestamp * 1000,
'backtest_days': backtest_days,
'backtest_run_start_ts': content['backtest_start_time'],
'backtest_run_end_ts': content['backtest_end_time'],
'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0,
'market_change': market_change,
'pairlist': list(btdata.keys()),
'stake_amount': config['stake_amount'],
'stake_currency': config['stake_currency'],
'max_open_trades': (config['max_open_trades']
'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
'timeframe': config['timeframe'],
'timerange': config.get('timerange', ''),
'enable_protections': config.get('enable_protections', False),
'strategy_name': strategy,
# Parameters relevant for backtesting
'stoploss': config['stoploss'],
'trailing_stop': config.get('trailing_stop', False),
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False),
'use_custom_stoploss': config.get('use_custom_stoploss', False),
'minimal_roi': config['minimal_roi'],
'use_sell_signal': config['ask_strategy']['use_sell_signal'],
'sell_profit_only': config['ask_strategy']['sell_profit_only'],
@ -307,7 +315,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
try:
max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown(
results, value_col='profit_percent')
results, value_col='profit_ratio')
strat_stats.update({
'max_drawdown': max_drawdown,
'drawdown_start': drawdown_start,
@ -385,7 +393,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
Generate summary table per strategy
:param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades used for backtest
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
:param all_results: Dict of <Strategyname: DataFrame> containing results for all strategies
:return: pretty printed table with tabulate as string
"""
floatfmt = _get_line_floatfmt()
@ -402,8 +410,8 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
def text_table_add_metrics(strat_results: Dict) -> str:
if len(strat_results['trades']) > 0:
best_trade = max(strat_results['trades'], key=lambda x: x['profit_percent'])
worst_trade = min(strat_results['trades'], key=lambda x: x['profit_percent'])
best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
metrics = [
('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
@ -417,9 +425,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
('Worst Pair', f"{strat_results['worst_pair']['key']} "
f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"),
('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"),
('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"),
('Worst trade', f"{worst_trade['pair']} "
f"{round(worst_trade['profit_percent'] * 100, 2)}%"),
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"),
('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"),

View File

@ -302,6 +302,11 @@ class Trade(_DECL_BASE):
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'close_profit_abs': self.close_profit_abs, # Deprecated
'trade_duration_s': (int((self.close_date - self.open_date).total_seconds())
if self.close_date else None),
'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60)
if self.close_date else None),
'profit_ratio': self.close_profit,
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs,

View File

@ -13,6 +13,7 @@ from freqtrade.data.history import get_timerange, load_data
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds
from freqtrade.misc import pair_to_filename
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy
@ -29,16 +30,16 @@ except ImportError:
exit(1)
def init_plotscript(config, startup_candles: int = 0):
def init_plotscript(config, markets: List, startup_candles: int = 0):
"""
Initialize objects needed for plotting
:return: Dict with candle (OHLCV) data, trades and pairs
"""
if "pairs" in config:
pairs = config['pairs']
pairs = expand_pairlist(config['pairs'], markets)
else:
pairs = config['exchange']['pair_whitelist']
pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets)
# Set timerange to use
timerange = TimeRange.parse_timerange(config.get('timerange'))
@ -174,7 +175,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
# Trades can be empty
if trades is not None and len(trades) > 0:
# Create description for sell summarizing the trade
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, "
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_ratio'] * 100, 1)}%, "
f"{row['sell_reason']}, "
f"{row['trade_duration']} min",
axis=1)
@ -194,9 +195,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
trade_sells = go.Scatter(
x=trades.loc[trades['profit_percent'] > 0, "close_date"],
y=trades.loc[trades['profit_percent'] > 0, "close_rate"],
text=trades.loc[trades['profit_percent'] > 0, "desc"],
x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] > 0, "desc"],
mode='markers',
name='Sell - Profit',
marker=dict(
@ -207,9 +208,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
)
trade_sells_loss = go.Scatter(
x=trades.loc[trades['profit_percent'] <= 0, "close_date"],
y=trades.loc[trades['profit_percent'] <= 0, "close_rate"],
text=trades.loc[trades['profit_percent'] <= 0, "desc"],
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
mode='markers',
name='Sell - Loss',
marker=dict(
@ -527,7 +528,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange)
plot_elements = init_plotscript(config, strategy.startup_candle_count)
plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange']
trades = plot_elements['trades']
pair_counter = 0
@ -562,7 +563,8 @@ def plot_profit(config: Dict[str, Any]) -> None:
But should be somewhat proportional, and therefor useful
in helping out to find a good algorithm.
"""
plot_elements = init_plotscript(config)
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
plot_elements = init_plotscript(config, list(exchange.markets))
trades = plot_elements['trades']
# Filter trades to relevant pairs
# Remove open pairs - we don't know the profit yet so can't calculate profit for these.

View File

@ -124,10 +124,21 @@ class IPairList(LoggingMixin, ABC):
"""
return self._pairlistmanager.verify_blacklist(pairlist, logmethod)
def verify_whitelist(self, pairlist: List[str], logmethod,
keep_invalid: bool = False) -> List[str]:
"""
Proxy method to verify_whitelist for easy access for child classes.
:param pairlist: Pairlist to validate
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes.
:return: pairlist - whitelisted pairs
"""
return self._pairlistmanager.verify_whitelist(pairlist, logmethod, keep_invalid)
def _whitelist_for_active_markets(self, pairlist: List[str]) -> List[str]:
"""
Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list of pairs the user might want to trade
:param pairlist: the sorted list of pairs the user might want to trade
:return: the list of pairs the user wants to trade without those unavailable or
black_listed
"""

View File

@ -43,7 +43,7 @@ class SpreadFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed
"""
if 'bid' in ticker and 'ask' in ticker:
if 'bid' in ticker and 'ask' in ticker and ticker['ask']:
spread = 1 - ticker['bid'] / ticker['ask']
if spread > self._max_spread_ratio:
self.log_once(f"Removed {pair} from whitelist, because spread "
@ -52,4 +52,6 @@ class SpreadFilter(IPairList):
return False
else:
return True
self.log_once(f"Removed {pair} from whitelist due to invalid ticker data: {ticker}",
logger.info)
return False

View File

@ -50,9 +50,12 @@ class StaticPairList(IPairList):
:return: List of pairs
"""
if self._allow_inactive:
return self._config['exchange']['pair_whitelist']
return self.verify_whitelist(
self._config['exchange']['pair_whitelist'], logger.info, keep_invalid=True
)
else:
return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist'])
return self._whitelist_for_active_markets(
self.verify_whitelist(self._config['exchange']['pair_whitelist'], logger.info))
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""

View File

@ -2,21 +2,40 @@ import re
from typing import List
def expand_pairlist(wildcardpl: List[str], available_pairs: List[str]) -> List[str]:
def expand_pairlist(wildcardpl: List[str], available_pairs: List[str],
keep_invalid: bool = False) -> List[str]:
"""
Expand pairlist potentially containing wildcards based on available markets.
This will implicitly filter all pairs in the wildcard-list which are not in available_pairs.
:param wildcardpl: List of Pairlists, which may contain regex
:param available_pairs: List of all available pairs (`exchange.get_markets().keys()`)
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes
:return expanded pairlist, with Regexes from wildcardpl applied to match all available pairs.
:raises: ValueError if a wildcard is invalid (like '*/BTC' - which should be `.*/BTC`)
"""
result = []
if keep_invalid:
for pair_wc in wildcardpl:
try:
comp = re.compile(pair_wc)
result_partial = [
pair for pair in available_pairs if re.fullmatch(comp, pair)
]
# Add all matching pairs.
# If there are no matching pairs (Pair not on exchange) keep it.
result += result_partial or [pair_wc]
except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
for element in result:
if not re.fullmatch(r'^[A-Za-z0-9/-]+$', element):
result.remove(element)
else:
for pair_wc in wildcardpl:
try:
comp = re.compile(pair_wc)
result += [
pair for pair in available_pairs if re.match(comp, pair)
pair for pair in available_pairs if re.fullmatch(comp, pair)
]
except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}")

View File

@ -59,6 +59,17 @@ class PairListManager():
"""The expanded blacklist (including wildcard expansion)"""
return expand_pairlist(self._blacklist, self._exchange.get_markets().keys())
@property
def expanded_whitelist_keep_invalid(self) -> List[str]:
"""The expanded whitelist (including wildcard expansion), maintaining invalid pairs"""
return expand_pairlist(self._whitelist, self._exchange.get_markets().keys(),
keep_invalid=True)
@property
def expanded_whitelist(self) -> List[str]:
"""The expanded whitelist (including wildcard expansion), filtering invalid pairs"""
return expand_pairlist(self._whitelist, self._exchange.get_markets().keys())
@property
def name_list(self) -> List[str]:
"""Get list of loaded Pairlist Handler names"""
@ -129,6 +140,28 @@ class PairListManager():
pairlist.remove(pair)
return pairlist
def verify_whitelist(self, pairlist: List[str], logmethod,
keep_invalid: bool = False) -> List[str]:
"""
Verify and remove items from pairlist - returning a filtered pairlist.
Logs a warning or info depending on `aswarning`.
Pairlist Handlers explicitly using this method shall use
`logmethod=logger.info` to avoid spamming with warning messages
:param pairlist: Pairlist to validate
:param logmethod: Function that'll be called, `logger.info` or `logger.warning`
:param keep_invalid: If sets to True, drops invalid pairs silently while expanding regexes.
:return: pairlist - whitelisted pairs
"""
try:
if keep_invalid:
whitelist = self.expanded_whitelist_keep_invalid
else:
whitelist = self.expanded_whitelist
except ValueError as err:
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
return []
return whitelist
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
"""
Create list of pair tuples with (pair, timeframe)

View File

@ -131,6 +131,7 @@ class ShowConfig(BaseModel):
forcebuy_enabled: bool
ask_strategy: Dict[str, Any]
bid_strategy: Dict[str, Any]
bot_name: str
state: str
runmode: str

View File

@ -121,13 +121,15 @@ class RPC:
'dry_run': config['dry_run'],
'stake_currency': config['stake_currency'],
'stake_amount': config['stake_amount'],
'max_open_trades': config['max_open_trades'],
'max_open_trades': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'),
'trailing_stop': config.get('trailing_stop'),
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'bot_name': config.get('bot_name', 'freqtrade'),
'timeframe': config.get('timeframe'),
'timeframe_ms': timeframe_to_msecs(config['timeframe']
) if 'timeframe' in config else '',
@ -143,13 +145,17 @@ class RPC:
}
return val
def _rpc_trade_status(self) -> List[Dict[str, Any]]:
def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]:
"""
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function
"""
# Fetch open trade
# Fetch open trades
if trade_ids:
trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
else:
trades = Trade.get_open_trades()
if not trades:
raise RPCException('no active trade')
else:

View File

@ -277,7 +277,14 @@ class Telegram(RPCHandler):
return
try:
results = self._rpc._rpc_trade_status()
# Check if there's at least one numerical ID provided.
# If so, try to get only these trades.
trade_ids = []
if context.args and len(context.args) > 0:
trade_ids = [int(i) for i in context.args if i.isnumeric()]
results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
messages = []
for r in results:
@ -815,7 +822,9 @@ class Telegram(RPCHandler):
"Optionally takes a rate at which to buy.` \n")
message = ("*/start:* `Starts the trader`\n"
"*/stop:* `Stops the trader`\n"
"*/status [table]:* `Lists all open trades`\n"
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
" *<trade_id> :* `Lists one or more specific trades.`\n"
" `Separate multiple <trade_id> with a blank space.`\n"
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n"

View File

@ -63,6 +63,7 @@
"username": "",
"password": ""
},
"bot_name": "freqtrade",
"initial_state": "running",
"forcebuy_enable": false,
"internals": {

View File

@ -39,8 +39,8 @@ class SampleHyperOptLoss(IHyperOptLoss):
"""
Objective function, returns smaller number for better results
"""
total_profit = results.profit_percent.sum()
trade_duration = results.trade_duration.mean()
total_profit = results['profit_ratio'].sum()
trade_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)

View File

@ -3,7 +3,6 @@ nav:
- Home: index.md
- Quickstart with Docker: docker_quickstart.md
- Installation:
- Docker without docker-compose: docker.md
- Linux/MacOS/Raspberry: installation.md
- Windows: windows_installation.md
- Freqtrade Basics: bot-basics.md

View File

@ -3,14 +3,14 @@
-r requirements-plot.txt
-r requirements-hyperopt.txt
coveralls==2.2.0
coveralls==3.0.0
flake8==3.8.4
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.2.1
mypy==0.790
pytest==6.2.1
pytest-asyncio==0.14.0
pytest-cov==2.10.1
pytest-cov==2.11.1
pytest-mock==3.5.1
pytest-random-order==1.0.4
isort==5.7.0

View File

@ -3,7 +3,7 @@
# Required for hyperopt
scipy==1.6.0
scikit-learn==0.24.0
scikit-learn==0.24.1
scikit-optimize==0.8.1
filelock==3.0.12
joblib==1.0.0

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot.
-r requirements.txt
plotly==4.14.1
plotly==4.14.3

View File

@ -1,12 +1,12 @@
numpy==1.19.5
pandas==1.2.0
pandas==1.2.1
ccxt==1.40.30
ccxt==1.40.99
aiohttp==3.7.3
SQLAlchemy==1.3.22
python-telegram-bot==13.1
arrow==0.17.0
cachetools==4.2.0
cachetools==4.2.1
requests==2.25.1
urllib3==1.26.2
wrapt==1.12.1
@ -16,7 +16,7 @@ tabulate==0.8.7
pycoingecko==1.4.0
jinja2==2.11.2
tables==3.6.1
blosc==1.10.1
blosc==1.10.2
# find first, C search in arrays
py_find_1st==1.1.4
@ -30,10 +30,10 @@ sdnotify==0.3.2
# API Server
fastapi==0.63.0
uvicorn==0.13.3
pyjwt==2.0.0
pyjwt==2.0.1
# Support for colorized terminal output
colorama==0.4.4
# Building config files interactively
questionary==1.9.0
prompt-toolkit==3.0.10
prompt-toolkit==3.0.14

View File

@ -202,52 +202,6 @@ function test_and_fix_python_on_mac() {
fi
}
function config_generator() {
echo "Starting to generate config.json"
echo
echo "Generating General configuration"
echo "-------------------------"
default_max_trades=3
read -p "Max open trades: (Default: $default_max_trades) " max_trades
max_trades=${max_trades:-$default_max_trades}
default_stake_amount=0.05
read -p "Stake amount: (Default: $default_stake_amount) " stake_amount
stake_amount=${stake_amount:-$default_stake_amount}
default_stake_currency="BTC"
read -p "Stake currency: (Default: $default_stake_currency) " stake_currency
stake_currency=${stake_currency:-$default_stake_currency}
default_fiat_currency="USD"
read -p "Fiat currency: (Default: $default_fiat_currency) " fiat_currency
fiat_currency=${fiat_currency:-$default_fiat_currency}
echo
echo "Generating exchange config "
echo "------------------------"
read -p "Exchange API key: " api_key
read -p "Exchange API Secret: " api_secret
echo
echo "Generating Telegram config"
echo "-------------------------"
read -p "Telegram Token: " token
read -p "Telegram Chat_id: " chat_id
sed -e "s/\"max_open_trades\": 3,/\"max_open_trades\": $max_trades,/g" \
-e "s/\"stake_amount\": 0.05,/\"stake_amount\": $stake_amount,/g" \
-e "s/\"stake_currency\": \"BTC\",/\"stake_currency\": \"$stake_currency\",/g" \
-e "s/\"fiat_display_currency\": \"USD\",/\"fiat_display_currency\": \"$fiat_currency\",/g" \
-e "s/\"your_exchange_key\"/\"$api_key\"/g" \
-e "s/\"your_exchange_secret\"/\"$api_secret\"/g" \
-e "s/\"your_telegram_token\"/\"$token\"/g" \
-e "s/\"your_telegram_chat_id\"/\"$chat_id\"/g" \
-e "s/\"dry_run\": false,/\"dry_run\": true,/g" config.json.example > config.json
}
function config() {
echo "-------------------------"

View File

@ -21,7 +21,7 @@ from tests.conftest_trades import MOCK_TRADE_COUNT
def test_setup_utils_configuration():
args = [
'list-exchanges', '--config', 'config.json.example',
'list-exchanges', '--config', 'config_bittrex.json.example',
]
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
@ -40,7 +40,7 @@ def test_start_trading_fail(mocker, caplog):
exitmock = mocker.patch("freqtrade.worker.Worker.exit", MagicMock())
args = [
'trade',
'-c', 'config.json.example'
'-c', 'config_bittrex.json.example'
]
start_trading(get_args(args))
assert exitmock.call_count == 1
@ -122,10 +122,10 @@ def test_list_timeframes(mocker, capsys):
match=r"This command requires a configured exchange.*"):
start_list_timeframes(pargs)
# Test with --config config.json.example
# Test with --config config_bittrex.json.example
args = [
"list-timeframes",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
]
start_list_timeframes(get_args(args))
captured = capsys.readouterr()
@ -169,7 +169,7 @@ def test_list_timeframes(mocker, capsys):
# Test with --one-column
args = [
"list-timeframes",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--one-column",
]
start_list_timeframes(get_args(args))
@ -209,10 +209,10 @@ def test_list_markets(mocker, markets, capsys):
match=r"This command requires a configured exchange.*"):
start_list_markets(pargs, False)
# Test with --config config.json.example
# Test with --config config_bittrex.json.example
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--print-list",
]
start_list_markets(get_args(args), False)
@ -239,7 +239,7 @@ def test_list_markets(mocker, markets, capsys):
# Test with --all: all markets
args = [
"list-markets", "--all",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--print-list",
]
start_list_markets(get_args(args), False)
@ -252,7 +252,7 @@ def test_list_markets(mocker, markets, capsys):
# Test list-pairs subcommand: active pairs
args = [
"list-pairs",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--print-list",
]
start_list_markets(get_args(args), True)
@ -264,7 +264,7 @@ def test_list_markets(mocker, markets, capsys):
# Test list-pairs subcommand with --all: all pairs
args = [
"list-pairs", "--all",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--print-list",
]
start_list_markets(get_args(args), True)
@ -277,7 +277,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=ETH, LTC
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "ETH", "LTC",
"--print-list",
]
@ -290,7 +290,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "LTC",
"--print-list",
]
@ -303,7 +303,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, quote=USDT, USD
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--quote", "USDT", "USD",
"--print-list",
]
@ -316,7 +316,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, quote=USDT
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--quote", "USDT",
"--print-list",
]
@ -329,7 +329,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC, quote=USDT
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "USDT",
"--print-list",
]
@ -342,7 +342,7 @@ def test_list_markets(mocker, markets, capsys):
# active pairs, base=LTC, quote=USDT
args = [
"list-pairs",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "USD",
"--print-list",
]
@ -355,7 +355,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC, quote=USDT, NONEXISTENT
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "USDT", "NONEXISTENT",
"--print-list",
]
@ -368,7 +368,7 @@ def test_list_markets(mocker, markets, capsys):
# active markets, base=LTC, quote=NONEXISTENT
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "NONEXISTENT",
"--print-list",
]
@ -381,7 +381,7 @@ def test_list_markets(mocker, markets, capsys):
# Test tabular output
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
]
start_list_markets(get_args(args), False)
captured = capsys.readouterr()
@ -391,7 +391,7 @@ def test_list_markets(mocker, markets, capsys):
# Test tabular output, no markets found
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--base", "LTC", "--quote", "NONEXISTENT",
]
start_list_markets(get_args(args), False)
@ -403,7 +403,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --print-json
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--print-json"
]
start_list_markets(get_args(args), False)
@ -415,7 +415,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --print-csv
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--print-csv"
]
start_list_markets(get_args(args), False)
@ -427,7 +427,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --one-column
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--one-column"
]
start_list_markets(get_args(args), False)
@ -439,7 +439,7 @@ def test_list_markets(mocker, markets, capsys):
# Test --one-column
args = [
"list-markets",
'--config', 'config.json.example',
'--config', 'config_bittrex.json.example',
"--one-column"
]
with pytest.raises(OperationalException, match=r"Cannot get markets.*"):
@ -781,7 +781,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
patched_configuration_load_config_file(mocker, default_conf)
args = [
'test-pairlist',
'-c', 'config.json.example'
'-c', 'config_bittrex.json.example'
]
start_test_pairlist(get_args(args))
@ -795,7 +795,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
args = [
'test-pairlist',
'-c', 'config.json.example',
'-c', 'config_bittrex.json.example',
'--one-column',
]
start_test_pairlist(get_args(args))
@ -804,7 +804,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
args = [
'test-pairlist',
'-c', 'config.json.example',
'-c', 'config_bittrex.json.example',
'--print-json',
]
start_test_pairlist(get_args(args))

View File

@ -73,7 +73,6 @@ def patched_configuration_load_config_file(mocker, config) -> None:
def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None:
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())

View File

@ -7,14 +7,13 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime
from freqtrade.configuration import TimeRange
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism,
calculate_market_change, calculate_max_drawdown,
combine_dataframes_with_mean, create_cum_profit,
extract_trades_of_period, get_latest_backtest_filename,
get_latest_hyperopt_file, load_backtest_data, load_trades,
load_trades_from_db)
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD,
analyze_trade_parallelism, calculate_market_change,
calculate_max_drawdown, combine_dataframes_with_mean,
create_cum_profit, extract_trades_of_period,
get_latest_backtest_filename, get_latest_hyperopt_file,
load_backtest_data, load_trades, load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history
from freqtrade.optimize.backtesting import BacktestResult
from tests.conftest import create_mock_trades
from tests.conftest_trades import MOCK_TRADE_COUNT
@ -55,7 +54,7 @@ def test_load_backtest_data_old_format(testdatadir):
filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame)
assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profit_abs"]
assert list(bt_data.columns) == BT_DATA_COLUMNS_OLD + ['profit_abs', 'profit_ratio']
assert len(bt_data) == 179
# Test loading from string (must yield same result)
@ -71,7 +70,7 @@ def test_load_backtest_data_new_format(testdatadir):
filename = testdatadir / "backtest-result_new.json"
bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"])
assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID)
assert len(bt_data) == 179
# Test loading from string (must yield same result)
@ -95,7 +94,7 @@ def test_load_backtest_data_multi(testdatadir):
for strategy in ('DefaultStrategy', 'TestStrategy'):
bt_data = load_backtest_data(filename, strategy=strategy)
assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"])
assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID)
assert len(bt_data) == 179
# Test loading from string (must yield same result)
@ -122,7 +121,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
assert isinstance(trades, DataFrame)
assert "pair" in trades.columns
assert "open_date" in trades.columns
assert "profit_percent" in trades.columns
assert "profit_ratio" in trades.columns
for col in BT_DATA_COLUMNS:
if col not in ['index', 'open_at_end']:

View File

@ -373,28 +373,25 @@ def test__load_markets(default_conf, mocker, caplog):
expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=expected_return)
type(api_mock).markets = expected_return
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
default_conf['exchange']['pair_whitelist'] = ['ETH/BTC']
ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False)
ex = Exchange(default_conf)
assert ex.markets == expected_return
def test_reload_markets(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}}
def load_markets(*args, **kwargs):
exchange._api.markets = updated_markets
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
api_mock = MagicMock()
api_mock.load_markets = load_markets
type(api_mock).markets = initial_markets
api_mock.load_markets = MagicMock(return_value=initial_markets)
default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
mock_markets=False)
exchange._load_async_markets = MagicMock()
exchange._last_markets_refresh = arrow.utcnow().int_timestamp
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
assert exchange.markets == initial_markets
@ -403,6 +400,7 @@ def test_reload_markets(default_conf, mocker, caplog):
assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0
api_mock.load_markets = MagicMock(return_value=updated_markets)
# more than 10 minutes have passed, reload is executed
exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60
exchange.reload_markets()
@ -429,7 +427,7 @@ def test_reload_markets_exception(default_conf, mocker, caplog):
def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
default_conf['stake_currency'] = stake_currency
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
})
@ -443,7 +441,7 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
def test_validate_stake_currency_error(default_conf, mocker, caplog):
default_conf['stake_currency'] = 'XRP'
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
})
@ -489,7 +487,7 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'},
'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'},
@ -508,7 +506,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
'XRP/BTC': {'inactive': True}
'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'}
})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
@ -540,7 +538,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC', 'info': {'IsRestricted': True}},
'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ...
@ -558,7 +556,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'BTC'},
@ -574,7 +572,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog):
api_mock = MagicMock()
default_conf['stake_currency'] = ''
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'BTC'},
@ -585,12 +583,13 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf)
assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD')
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'USDT'},

View File

@ -37,7 +37,7 @@ def hyperopt_results():
return pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [-0.1, 0.2, 0.3],
'profit_ratio': [-0.1, 0.2, 0.3],
'profit_abs': [-0.2, 0.4, 0.6],
'trade_duration': [10, 30, 10],
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],

View File

@ -510,7 +510,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
)
assert len(results) == len(data.trades)
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3)
for c, trade in enumerate(data.trades):
res = results.iloc[c]

View File

@ -350,17 +350,17 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
default_conf['timerange'] = '-1510694220'
backtesting = Backtesting(default_conf)
backtesting.strategy.bot_loop_start = MagicMock()
backtesting.start()
# check the logs, that will contain the backtest result
exists = [
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Backtesting with data from 2017-11-14 21:17:00 '
'up to 2017-11-14 22:59:00 (0 days)..'
]
for line in exists:
assert log_has(line, caplog)
assert backtesting.strategy.dp._pairlists is not None
assert backtesting.strategy.bot_loop_start.call_count == 1
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
@ -445,7 +445,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf)
def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
@ -469,21 +469,28 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
expected = pd.DataFrame(
{'pair': [pair, pair],
'profit_percent': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'stake_amount': [0.001, 0.001],
'amount': [0.00957442, 0.0097064],
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
),
'open_rate': [0.104445, 0.10302485],
'open_fee': [0.0025, 0.0025],
'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime,
Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True),
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'close_fee': [0.0025, 0.0025],
'amount': [0.00957442, 0.0097064],
'fee_open': [0.0025, 0.0025],
'fee_close': [0.0025, 0.0025],
'trade_duration': [235, 40],
'open_at_end': [False, False],
'sell_reason': [SellType.ROI, SellType.ROI]
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'sell_reason': [SellType.ROI, SellType.ROI],
'initial_stop_loss_abs': [0.0940005, 0.09272236],
'initial_stop_loss_ratio': [-0.1, -0.1],
'stop_loss_abs': [0.0940005, 0.09272236],
'stop_loss_ratio': [-0.1, -0.1],
'min_rate': [0.1038, 0.10302485],
'max_rate': [0.10501, 0.1038888],
'is_open': [False, False],
})
pd.testing.assert_frame_equal(results, expected)
data_pair = processed[pair]
@ -629,7 +636,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
# 100 buys signals
assert len(results) == 100
# One trade was force-closed at the end
assert len(results.loc[results.open_at_end]) == 0
assert len(results.loc[results['is_open']]) == 0
@pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC'])
@ -722,8 +729,6 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'Backtesting with data from 2017-11-14 21:17:00 '
@ -739,7 +744,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
patch_exchange(mocker)
backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS + ['profit_abs']))
backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS))
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
@ -786,8 +791,6 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'Backtesting with data from 2017-11-14 21:17:00 '
@ -807,7 +810,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
patch_exchange(mocker)
backtestmock = MagicMock(side_effect=[
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'],
'profit_percent': [0.0, 0.0],
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True
@ -815,13 +818,13 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True),
'trade_duration': [235, 40],
'open_at_end': [False, False],
'is_open': [False, False],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI]
}),
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_percent': [0.03, 0.01, 0.1],
'profit_ratio': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00',
@ -831,7 +834,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True),
'trade_duration': [47, 40, 20],
'open_at_end': [False, False, False],
'is_open': [False, False, False],
'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
@ -865,8 +868,6 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: 1510694220-1510700340 ...',
f'Using data directory: {testdatadir} ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Loading data from 2017-11-14 20:57:00 '
'up to 2017-11-14 22:58:00 (0 days)..',
'Backtesting with data from 2017-11-14 21:17:00 '

View File

@ -427,7 +427,7 @@ def test_format_results(hyperopt):
('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246)
]
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels)
results_metrics = hyperopt._calculate_results_metrics(df)
results_explanation = hyperopt._format_results_explanation_string(results_metrics)
@ -567,7 +567,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
trades = [
('TRX/BTC', 0.023117, 0.000233, 100)
]
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
labels = ['currency', 'profit_ratio', 'profit_abs', 'trade_duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch(

View File

@ -60,9 +60,9 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
@ -77,9 +77,9 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
@ -95,9 +95,9 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N
def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
@ -113,9 +113,9 @@ def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results
def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
@ -131,9 +131,9 @@ def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) ->
def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
@ -149,9 +149,9 @@ def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_result
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)

View File

@ -27,7 +27,7 @@ def test_text_table_bt_results():
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_ratio': [0.1, 0.2],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'wins': [2, 0],
@ -59,7 +59,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
results = {'DefStrat': {
'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
@ -72,12 +72,15 @@ def test_generate_backtest_stats(default_conf, testdatadir):
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"is_open": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
}),
'config': default_conf,
'locks': []}
'locks': [],
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
}
timerange = TimeRange.parse_timerange('1510688220-1510700340')
min_date = Arrow.fromtimestamp(1510688220)
@ -100,7 +103,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
results = {'DefStrat': {
'results': pd.DataFrame(
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, -0.013803, 0.002780],
"profit_ratio": [0.003312, 0.010801, -0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, -0.000014, 0.000003],
"open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
@ -176,7 +179,7 @@ def test_generate_pair_metrics():
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_ratio': [0.1, 0.2],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'wins': [2, 0],
@ -224,7 +227,7 @@ def test_text_table_sell_reason():
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, -0.1],
'profit_ratio': [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10],
'wins': [2, 0, 0],
@ -256,7 +259,7 @@ def test_generate_sell_reason_stats():
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, -0.1],
'profit_ratio': [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10],
'wins': [2, 0, 0],
@ -292,7 +295,7 @@ def test_text_table_strategy(default_conf):
results['TestStrategy1'] = {'results': pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, 0.3],
'profit_ratio': [0.1, 0.2, 0.3],
'profit_abs': [0.2, 0.4, 0.5],
'trade_duration': [10, 30, 10],
'wins': [2, 0, 0],
@ -304,7 +307,7 @@ def test_text_table_strategy(default_conf):
results['TestStrategy2'] = {'results': pd.DataFrame(
{
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
'profit_percent': [0.4, 0.2, 0.3],
'profit_ratio': [0.4, 0.2, 0.3],
'profit_abs': [0.4, 0.4, 0.5],
'trade_duration': [15, 30, 15],
'wins': [4, 1, 0],

View File

@ -156,6 +156,31 @@ def test_refresh_static_pairlist(mocker, markets, static_pl_conf):
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
@pytest.mark.parametrize('pairs,expected', [
(['NOEXIST/BTC', r'\+WHAT/BTC'],
['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'NOEXIST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']),
(['NOEXIST/BTC', r'*/BTC'], # This is an invalid regex
[]),
])
def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, expected, caplog):
static_pl_conf['pairlists'][0]['allow_inactive'] = True
static_pl_conf['exchange']['pair_whitelist'] += pairs
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
markets=PropertyMock(return_value=markets),
)
freqtrade.pairlists.refresh_pairlist()
# Ensure all except those in whitelist are removed
assert set(expected) == set(freqtrade.pairlists.whitelist)
assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist
if not expected:
assert log_has_re(r'Pair whitelist contains an invalid Wildcard: Wildcard error.*', caplog)
def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog):
static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC']
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)
@ -165,7 +190,6 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog):
markets=PropertyMock(return_value=markets),
)
freqtrade.pairlists.refresh_pairlist()
# List ordered by BaseVolume
whitelist = []
# Ensure all except those in whitelist are removed
assert set(whitelist) == set(freqtrade.pairlists.whitelist)
@ -695,6 +719,32 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count
def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplog):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'SpreadFilter', 'max_spread_ratio': 0.1}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
ftbot = get_patched_freqtradebot(mocker, default_conf)
ftbot.pairlists.refresh_pairlist()
assert len(ftbot.pairlists.whitelist) == 5
tickers.return_value['ETH/BTC']['ask'] = 0.0
del tickers.return_value['TKN/BTC']
del tickers.return_value['LTC/BTC']
mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers)
ftbot.pairlists.refresh_pairlist()
assert log_has_re(r'Removed .* invalid ticker data.*', caplog)
assert len(ftbot.pairlists.whitelist) == 2
@pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010,
"max_price": 1.0},
@ -846,6 +896,9 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o
(['*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
None),
(['BTC/USD'],
['BTC/USD', 'BTC/USDT'],
['BTC/USD']),
])
def test_expand_pairlist(wildcardlist, pairs, expected):
if expected is None:
@ -853,3 +906,39 @@ def test_expand_pairlist(wildcardlist, pairs, expected):
expand_pairlist(wildcardlist, pairs)
else:
assert sorted(expand_pairlist(wildcardlist, pairs)) == sorted(expected)
@pytest.mark.parametrize('wildcardlist,pairs,expected', [
(['BTC/USDT'],
['BTC/USDT'],
['BTC/USDT']),
(['BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETH/USDT']),
(['BTC/USDT', 'ETH/USDT'],
['BTC/USDT'], ['BTC/USDT', 'ETH/USDT']), # Test one too many
(['.*/USDT'],
['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple
(['.*C/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one
(['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one
(['BTC/.*', 'ETH/.*'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'],
['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one
(['*UP/USDT', 'BTC/USDT', 'ETH/USDT'],
['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'],
None),
(['HELLO/WORLD'], [], ['HELLO/WORLD']), # Invalid pair kept
(['BTC/USD'],
['BTC/USD', 'BTC/USDT'],
['BTC/USD']),
])
def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected):
if expected is None:
with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'):
expand_pairlist(wildcardlist, pairs, keep_invalid=True)
else:
assert sorted(expand_pairlist(wildcardlist, pairs, keep_invalid=True)) == sorted(expected)

View File

@ -11,11 +11,10 @@ from freqtrade.persistence.models import PairLock
@pytest.mark.usefixtures("init_persistence")
def test_PairLocks(use_db):
PairLocks.timeframe = '5m'
PairLocks.use_db = use_db
# No lock should be present
if use_db:
assert len(PairLock.query.all()) == 0
else:
PairLocks.use_db = False
assert PairLocks.use_db == use_db
@ -88,10 +87,9 @@ def test_PairLocks(use_db):
def test_PairLocks_getlongestlock(use_db):
PairLocks.timeframe = '5m'
# No lock should be present
PairLocks.use_db = use_db
if use_db:
assert len(PairLock.query.all()) == 0
else:
PairLocks.use_db = False
assert PairLocks.use_db == use_db

View File

@ -80,6 +80,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'amount': 91.07468123,
'amount_requested': 91.07468123,
'stake_amount': 0.001,
'trade_duration': None,
'trade_duration_s': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
@ -144,6 +146,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'current_rate': ANY,
'amount': 91.07468123,
'amount_requested': 91.07468123,
'trade_duration': ANY,
'trade_duration_s': ANY,
'stake_amount': 0.001,
'close_profit': None,
'close_profit_pct': None,

View File

@ -414,6 +414,7 @@ def test_api_show_config(botclient, mocker):
assert rc.json()['timeframe_ms'] == 300000
assert rc.json()['timeframe_min'] == 5
assert rc.json()['state'] == 'running'
assert rc.json()['bot_name'] == 'freqtrade'
assert not rc.json()['trailing_stop']
assert 'bid_strategy' in rc.json()
assert 'ask_strategy' in rc.json()
@ -523,13 +524,17 @@ def test_api_logs(botclient):
assert isinstance(rc.json()['logs'][0][3], str)
assert isinstance(rc.json()['logs'][0][4], str)
rc = client_get(client, f"{BASE_URI}/logs?limit=5")
assert_response(rc)
assert len(rc.json()) == 2
assert 'logs' in rc.json()
rc1 = client_get(client, f"{BASE_URI}/logs?limit=5")
assert_response(rc1)
assert len(rc1.json()) == 2
assert 'logs' in rc1.json()
# Using a fixed comparison here would make this test fail!
assert rc.json()['log_count'] == 5
assert len(rc.json()['logs']) == rc.json()['log_count']
if rc1.json()['log_count'] < 5:
# Help debugging random test failure
print(f"rc={rc.json()}")
print(f"rc1={rc1.json()}")
assert rc1.json()['log_count'] == 5
assert len(rc1.json()['logs']) == rc1.json()['log_count']
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):

View File

@ -205,13 +205,14 @@ def test_telegram_status(default_conf, update, mocker) -> None:
assert msg_mock.call_count == 1
context = MagicMock()
# /status table 2 3
context.args = ["table", "2", "3"]
# /status table
context.args = ["table"]
telegram._status(update=update, context=context)
assert status_table.call_count == 1
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
default_conf['max_open_trades'] = 3
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
@ -252,8 +253,23 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
assert 'Close Rate' not in ''.join(lines)
assert 'Close Profit' not in ''.join(lines)
assert msg_mock.call_count == 1
assert msg_mock.call_count == 3
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
assert 'LTC/BTC' in msg_mock.call_args_list[1][0][0]
msg_mock.reset_mock()
context = MagicMock()
context.args = ["2", "3"]
telegram._status(update=update, context=context)
lines = msg_mock.call_args_list[0][0][0].split('\n')
assert '' not in lines
assert 'Close Rate' not in ''.join(lines)
assert 'Close Profit' not in ''.join(lines)
assert msg_mock.call_count == 2
assert 'LTC/BTC' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:

View File

@ -172,7 +172,7 @@ def test_download_data_options() -> None:
def test_plot_dataframe_options() -> None:
args = [
'plot-dataframe',
'-c', 'config.json.example',
'-c', 'config_bittrex.json.example',
'--indicators1', 'sma10', 'sma100',
'--indicators2', 'macd', 'fastd', 'fastk',
'--plot-limit', '30',

View File

@ -2100,6 +2100,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open
def test_bot_loop_start_called_once(mocker, default_conf, caplog):
ftbot = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade')
patch_get_signal(ftbot)
ftbot.strategy.bot_loop_start = MagicMock(side_effect=ValueError)
ftbot.strategy.analyze = MagicMock()
@ -3810,6 +3811,8 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
open_order_id="123456"
)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# Ticker rate cannot be found for this to work.
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
@ -4368,6 +4371,19 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee):
freqtrade.update_closed_trades_without_assigned_fees()
# Does nothing for dry-run
trades = Trade.get_trades().all()
assert len(trades) == MOCK_TRADE_COUNT
for trade in trades:
assert trade.fee_open_cost is None
assert trade.fee_open_currency is None
assert trade.fee_close_cost is None
assert trade.fee_close_currency is None
freqtrade.config['dry_run'] = False
freqtrade.update_closed_trades_without_assigned_fees()
trades = Trade.get_trades().all()
assert len(trades) == MOCK_TRADE_COUNT

View File

@ -67,12 +67,12 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example']
args = ['trade', '-c', 'config_bittrex.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog)
assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert log_has('Fatal exception!', caplog)
@ -85,12 +85,12 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.wallets.Wallets.update', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example']
args = ['trade', '-c', 'config_bittrex.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog)
assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert log_has('SIGINT received, aborting ...', caplog)
@ -106,12 +106,12 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = ['trade', '-c', 'config.json.example']
args = ['trade', '-c', 'config_bittrex.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog)
assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert log_has('Oh snap!', caplog)
@ -157,12 +157,12 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg()
args = Arguments(['trade', '-c', 'config_bittrex.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf)
with pytest.raises(SystemExit):
main(['trade', '-c', 'config.json.example'])
main(['trade', '-c', 'config_bittrex.json.example'])
assert log_has('Using config: config.json.example ...', caplog)
assert log_has('Using config: config_bittrex.json.example ...', caplog)
assert worker_mock.call_count == 4
assert reconfigure_mock.call_count == 1
assert isinstance(worker.freqtrade, FreqtradeBot)
@ -180,7 +180,7 @@ def test_reconfigure(mocker, default_conf) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.init_db', MagicMock())
args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg()
args = Arguments(['trade', '-c', 'config_bittrex.json.example']).get_parsed_arg()
worker = Worker(args=args, config=default_conf)
freqtrade = worker.freqtrade

View File

@ -815,6 +815,8 @@ def test_to_json(default_conf, fee):
'amount': 123.0,
'amount_requested': 123.0,
'stake_amount': 0.001,
'trade_duration': None,
'trade_duration_s': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
@ -869,6 +871,8 @@ def test_to_json(default_conf, fee):
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'trade_duration': 60,
'trade_duration_s': 3600,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,

View File

@ -47,14 +47,15 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
default_conf['timeframe'] = "5m"
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
ret = init_plotscript(default_conf)
supported_markets = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf, supported_markets)
assert "ohlcv" in ret
assert "trades" in ret
assert "pairs" in ret
assert 'timerange' in ret
default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf, 20)
ret = init_plotscript(default_conf, supported_markets, 20)
assert "ohlcv" in ret
assert "TRX/BTC" in ret["ohlcv"]
assert "ADA/BTC" in ret["ohlcv"]
@ -362,7 +363,7 @@ def test_start_plot_dataframe(mocker):
aup = mocker.patch("freqtrade.plot.plotting.load_and_plot_trades", MagicMock())
args = [
"plot-dataframe",
"--config", "config.json.example",
"--config", "config_bittrex.json.example",
"--pairs", "ETH/BTC"
]
start_plot_dataframe(get_args(args))
@ -406,7 +407,7 @@ def test_start_plot_profit(mocker):
aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock())
args = [
"plot-profit",
"--config", "config.json.example",
"--config", "config_bittrex.json.example",
"--pairs", "ETH/BTC"
]
start_plot_profit(get_args(args))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long