Merge branch 'develop' into pr/sobeit2020/4218
This commit is contained in:
commit
d8353bc90e
@ -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
|
||||
|
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include config.json.example
|
||||
recursive-include freqtrade *.py
|
||||
recursive-include freqtrade/templates/ *.j2 *.ipynb
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -90,6 +90,7 @@
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
|
@ -85,6 +85,7 @@
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
@ -177,6 +177,7 @@
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"db_url": "sqlite:///tradesv3.sqlite",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
|
@ -95,6 +95,7 @@
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
201
docs/docker.md
201
docs/docker.md
@ -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).
|
@ -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).
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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`.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
||||
|
@ -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.
|
||||
|
||||
------
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
mkdocs-material==6.2.4
|
||||
mkdocs-material==6.2.5
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==8.1
|
||||
|
@ -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!
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
---
|
||||
|
@ -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'])
|
||||
|
||||
|
@ -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
|
||||
|
@ -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': {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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,7 +385,54 @@ 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 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.
|
||||
max_open_trades = self.strategy.config['max_open_trades']
|
||||
else:
|
||||
logger.info(
|
||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||
max_open_trades = 0
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
for pair, df in preprocessed.items():
|
||||
preprocessed[pair] = trim_dataframe(df, timerange)
|
||||
min_date, max_date = history.get_timerange(preprocessed)
|
||||
|
||||
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 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=self.config.get('position_stacking', False),
|
||||
enable_protections=self.config.get('enable_protections', False),
|
||||
)
|
||||
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
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
@ -431,55 +441,15 @@ class Backtesting:
|
||||
"""
|
||||
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 = {}
|
||||
min_date = None
|
||||
max_date = None
|
||||
for strat in self.strategylist:
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
self._set_strategy(strat)
|
||||
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
|
||||
|
||||
# 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.
|
||||
max_open_trades = self.strategy.config['max_open_trades']
|
||||
else:
|
||||
logger.info(
|
||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||
max_open_trades = 0
|
||||
|
||||
# need to reprocess data every time to populate signals
|
||||
preprocessed = self.strategy.ohlcvdata_to_dataframe(data)
|
||||
|
||||
# Trim startup period from analyzed dataframe
|
||||
for pair, df in preprocessed.items():
|
||||
preprocessed[pair] = trim_dataframe(df, timerange)
|
||||
min_date, max_date = history.get_timerange(preprocessed)
|
||||
|
||||
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
|
||||
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,
|
||||
enable_protections=self.config.get('enable_protections', False),
|
||||
)
|
||||
all_results[self.strategy.get_strategy_name()] = {
|
||||
'results': results,
|
||||
'config': self.strategy.config,
|
||||
'locks': PairLocks.locks,
|
||||
}
|
||||
|
||||
stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date)
|
||||
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)
|
||||
|
@ -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)
|
||||
|
@ -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)} '
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
if config['max_open_trades'] != float('inf') else -1),
|
||||
'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)}%"),
|
||||
|
@ -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,
|
||||
|
@ -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,10 +175,10 @@ 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)
|
||||
axis=1)
|
||||
trade_buys = go.Scatter(
|
||||
x=trades["open_date"],
|
||||
y=trades["open_rate"],
|
||||
@ -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.
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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]:
|
||||
"""
|
||||
|
@ -2,22 +2,41 @@ 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 = []
|
||||
for pair_wc in wildcardpl:
|
||||
try:
|
||||
comp = re.compile(pair_wc)
|
||||
result += [
|
||||
pair for pair in available_pairs if re.match(comp, pair)
|
||||
]
|
||||
except re.error as err:
|
||||
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
|
||||
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.fullmatch(comp, pair)
|
||||
]
|
||||
except re.error as err:
|
||||
raise ValueError(f"Wildcard error in {pair_wc}, {err}")
|
||||
return result
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
trades = Trade.get_open_trades()
|
||||
# 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:
|
||||
|
@ -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"
|
||||
|
@ -63,6 +63,7 @@
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
"bot_name": "freqtrade",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==4.14.1
|
||||
plotly==4.14.3
|
||||
|
||||
|
@ -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
|
||||
|
46
setup.sh
46
setup.sh
@ -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 "-------------------------"
|
||||
|
@ -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))
|
||||
|
@ -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())
|
||||
|
@ -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']:
|
||||
|
@ -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'},
|
||||
|
@ -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],
|
||||
|
@ -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]
|
||||
|
@ -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 '
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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],
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
2
tests/testdata/backtest-result_new.json
vendored
2
tests/testdata/backtest-result_new.json
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user