Merge pull request #4434 from freqtrade/new_release

New release 2020.2
This commit is contained in:
Matthias 2021-02-24 19:27:43 +01:00 committed by GitHub
commit 38b96f071f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 2231 additions and 1389 deletions

View File

@ -1,5 +1,5 @@
--- ---
name: BQuestion name: Question
about: Ask a question you could not find an answer in the docs about: Ask a question you could not find an answer in the docs
title: '' title: ''
labels: "Question" labels: "Question"

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ user_data/*
user_data/notebooks/* user_data/notebooks/*
freqtrade-plot.html freqtrade-plot.html
freqtrade-profit-plot.html freqtrade-profit-plot.html
freqtrade/rpc/api_server/ui/*
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

View File

@ -12,7 +12,7 @@ Few pointers for contributions:
- New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - 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). - 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-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) 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-mm786y93-Fxo37glxMY9g8OQC5AoOIw) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
## Getting started ## Getting started

View File

@ -1,4 +1,4 @@
FROM python:3.8.6-slim-buster as base FROM python:3.9.2-slim-buster as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
@ -40,7 +40,9 @@ COPY --from=python-deps /root/.local /root/.local
# Install and execute # Install and execute
COPY . /freqtrade/ COPY . /freqtrade/
RUN pip install -e . --no-cache-dir \ RUN pip install -e . --no-cache-dir \
&& mkdir /freqtrade/user_data/ && mkdir /freqtrade/user_data/ \
&& freqtrade install-ui
ENTRYPOINT ["freqtrade"] ENTRYPOINT ["freqtrade"]
# Default to trade mode # Default to trade mode
CMD [ "trade" ] CMD [ "trade" ]

View File

@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local
# Install and execute # Install and execute
COPY . /freqtrade/ COPY . /freqtrade/
RUN pip install -e . --no-cache-dir RUN pip install -e . --no-cache-dir \
&& freqtrade install-ui
ENTRYPOINT ["freqtrade"] ENTRYPOINT ["freqtrade"]
# Default to trade mode # Default to trade mode
CMD [ "trade" ] CMD [ "trade" ]

View File

@ -2,3 +2,5 @@ include LICENSE
include README.md include README.md
recursive-include freqtrade *.py recursive-include freqtrade *.py
recursive-include freqtrade/templates/ *.j2 *.ipynb recursive-include freqtrade/templates/ *.j2 *.ipynb
include freqtrade/rpc/api_server/ui/fallback_file.html
include freqtrade/rpc/api_server/ui/favicon.ico

View File

@ -22,12 +22,21 @@ expect.
We strongly recommend you to have coding and Python knowledge. Do not We strongly recommend you to have coding and Python knowledge. Do not
hesitate to read the source code and understand the mechanism of this bot. hesitate to read the source code and understand the mechanism of this bot.
## Exchange marketplaces supported ## Supported Exchange marketplaces
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists)) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists))
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [X] [FTX](https://ftx.com)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested
Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
## Documentation ## Documentation
@ -39,7 +48,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io
- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux.
- [x] **Persistence**: Persistence is achieved through sqlite. - [x] **Persistence**: Persistence is achieved through sqlite.
- [x] **Dry-run**: Run the bot without playing money. - [x] **Dry-run**: Run the bot without paying money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/). - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/).
@ -138,7 +147,7 @@ For any questions not covered by the documentation or for further information ab
Please check out our [discord server](https://discord.gg/MA9v74M). 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-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
@ -169,7 +178,7 @@ to understand the requirements before sending your pull-requests.
Coding is not a necessity to contribute - maybe start with improving our documentation? Coding is not a necessity to contribute - maybe start with improving our documentation?
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-k9o2v5ut-jX8Mc4CwNM8CDc2Dyg96YA). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
**Important:** Always create your PR against the `develop` branch, not `stable`. **Important:** Always create your PR against the `develop` branch, not `stable`.
@ -191,5 +200,5 @@ To run this bot we recommend you a cloud instance with a minimum of:
- [pip](https://pip.pypa.io/en/stable/installing/) - [pip](https://pip.pypa.io/en/stable/installing/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) - [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
- [Docker](https://www.docker.com/products/docker) (Recommended) - [Docker](https://www.docker.com/products/docker) (Recommended)

View File

@ -51,6 +51,8 @@ fi
docker images docker images
docker push ${IMAGE_NAME} docker push ${IMAGE_NAME}
docker push ${IMAGE_NAME}:$TAG_PLOT
docker push ${IMAGE_NAME}:$TAG
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed pushing repo" echo "failed pushing repo"
return 1 return 1

View File

@ -12,15 +12,15 @@
"sell": 30 "sell": 30
}, },
"bid_strategy": { "bid_strategy": {
"use_order_book": false,
"ask_last_balance": 0.0, "ask_last_balance": 0.0,
"use_order_book": false,
"order_book_top": 1, "order_book_top": 1,
"check_depth_of_market": { "check_depth_of_market": {
"enabled": false, "enabled": false,
"bids_to_ask_delta": 1 "bids_to_ask_delta": 1
} }
}, },
"ask_strategy":{ "ask_strategy": {
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 1, "order_book_max": 1,

View File

@ -14,6 +14,11 @@ services:
container_name: freqtrade container_name: freqtrade
volumes: volumes:
- "./user_data:/freqtrade/user_data" - "./user_data:/freqtrade/user_data"
# Expose api on port 8080 (localhost only)
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
# before enabling this.
# ports:
# - "127.0.0.1:8080:8080"
# Default command used when running `docker compose up` # Default command used when running `docker compose up`
command: > command: >
trade trade

View File

@ -40,6 +40,11 @@ For the sample below, you then need to add the command line parameter `--hyperop
A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found in [userdata/hyperopts](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_loss.py). A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found in [userdata/hyperopts](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_loss.py).
``` python ``` python
from datetime import datetime
from typing import Dict
from pandas import DataFrame
from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.optimize.hyperopt import IHyperOptLoss
TARGET_TRADES = 600 TARGET_TRADES = 600
@ -54,6 +59,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
@staticmethod @staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float: *args, **kwargs) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results
@ -63,7 +69,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
* 0.25: Avoiding trade loss * 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above * 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_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
@ -77,10 +83,12 @@ Currently, the arguments are:
* `results`: DataFrame containing the result * `results`: DataFrame containing the result
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): 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)`) * `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the hyperopting TimeFrame * `min_date`: Start date of the timerange used
* `min_date`: End date of the hyperopting TimeFrame * `min_date`: End date of the timerange used
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
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. This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.

View File

@ -5,6 +5,89 @@ This page explains how to validate your strategy performance by using Backtestin
Backtesting requires historic data to be available. Backtesting requires historic data to be available.
To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation.
## Backtesting command reference
```
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-i TIMEFRAME]
[--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--enable-protections]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH]
optional arguments:
-h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `None`).
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
--stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number).
--enable-protections, --enableprotections
Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will
include configured protections
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to
backtest. Please note that ticker-interval needs to be
set either in config or via command line. When using
this together with `--export trades`, the strategy-
name is injected into the filename (so `backtest-
data.json` becomes `backtest-data-
DefaultStrategy.json`
--export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH
Save backtest results to the file with this filename.
Requires `--export` to be set as well. Example:
`--export-filename=user_data/backtest_results/backtest
_today.json`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
## Test your strategy with Backtesting ## Test your strategy with Backtesting
Now you have good Buy and Sell strategies and some historic data, you want to test it against Now you have good Buy and Sell strategies and some historic data, you want to test it against
@ -20,7 +103,7 @@ The result of backtesting will confirm if your bot has better odds of making a p
!!! Warning "Using dynamic pairlists for backtesting" !!! Warning "Using dynamic pairlists for backtesting"
Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist.
Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed. Also, when using pairlists other than StaticPairlist, reproducability of backtesting-results cannot be guaranteed.
Please read the [pairlists documentation](configuration.md#pairlists) for more information. Please read the [pairlists documentation](plugins.md#pairlists) for more information.
To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist. To achieve reproducible results, best generate a pairlist via the [`test-pairlist`](utils.md#test-pairlist) command and use that as static pairlist.
@ -262,9 +345,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). - `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 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). - `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 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 - `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade

View File

@ -4,6 +4,7 @@ This page provides you some basic concepts on how Freqtrade works and operates.
## Freqtrade terminology ## Freqtrade terminology
* Strategy: Your trading strategy, telling the bot what to do.
* Trade: Open position. * Trade: Open position.
* Open Order: Order which is currently placed on the exchange, and is not yet complete. * Open Order: Order which is currently placed on the exchange, and is not yet complete.
* Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). * Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT).

View File

@ -205,258 +205,6 @@ in production mode. Example command:
freqtrade trade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite freqtrade trade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands
Backtesting also uses the config specified via `-c/--config`.
```
usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [-s NAME]
[--strategy-path PATH] [-i TIMEFRAME]
[--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--enable-protections]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH]
optional arguments:
-h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `None`).
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
--stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number).
--enable-protections, --enableprotections
Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will
include configured protections
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to
backtest. Please note that ticker-interval needs to be
set either in config or via command line. When using
this together with `--export trades`, the strategy-
name is injected into the filename (so `backtest-
data.json` becomes `backtest-data-
DefaultStrategy.json`
--export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH
Save backtest results to the file with this filename.
Requires `--export` to be set as well. Example:
`--export-filename=user_data/backtest_results/backtest
_today.json`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
### Getting historic data for backtesting
The first time your run Backtesting, you will need to download some historic data first.
This can be accomplished by using `freqtrade download-data`.
Check the corresponding [Data Downloading](data-download.md) section for more details
## Hyperopt commands
To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your strategy.
```
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--dmmp] [--enable-protections] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME]
optional arguments:
-h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `None`).
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
--stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
--hyperopt NAME Specify hyperopt class name which will be used by the
bot.
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
Hyperopt Loss functions.
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number).
--enable-protections, --enableprotections
Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will
include configured protections
-e INT, --epochs INT Specify number of epochs (default: 100).
--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]
Specify which parameters to hyperopt. Space-separated
list.
--print-all Print all results, not only the best ones.
--no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file.
--print-json Print output in JSON format.
-j JOBS, --job-workers JOBS
The number of concurrently running jobs for
hyperoptimization (hyperopt worker processes). If -1
(default), all CPUs are used, for -2, all CPUs but one
are used, etc. If 1 is given, no parallel computing
code is used at all.
--random-state INT Set random state to some positive integer for
reproducible hyperopt results.
--min-trades INT Set minimal desired number of trades for evaluations
in the hyperopt optimization path (default: 1).
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
class (IHyperOptLoss). Different functions can
generate completely different results, since the
target for optimization is different. Built-in
Hyperopt-loss-functions are:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
## Edge commands
To know your trade expectancy and winrate against historical data, you can use Edge.
```
usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
optional arguments:
-h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
--stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
--stoplosses STOPLOSS_RANGE
Defines a range of stoploss values against which edge
will assess the strategy. The format is "min,max,step"
(without any space). Example:
`--stoplosses=-0.01,-0.1,-0.001`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
To understand edge and how to read the results, please read the [edge documentation](edge.md).
## Next step ## Next step
The optimal strategy of the bot will change with time depending of the market trends. The next step is to The optimal strategy of the bot will change with time depending of the market trends. The next step is to

View File

@ -82,17 +82,18 @@ 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.key` | API key to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `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_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. [More information](plugins.md#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.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#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_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 | `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
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async 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 | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async 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
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean | `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts | `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
| `protections` | Define one or more protections to be used. [More information below](#protections). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** List of Dicts | `protections` | Define one or more protections to be used. [More information](plugins.md#protections). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** List of Dicts
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean | `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
@ -111,12 +112,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `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 | `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 | `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 | `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` | `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <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 | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below. <br> **Datatype:** Boolean
| `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean | `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
| `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`. <br> **Datatype:** ClassName
| `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String | `strategy_path` | Adds an additional strategy lookup path (must be a directory). <br> **Datatype:** String
| `internals.process_throttle_secs` | Set the process throttle. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer | `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer
| `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0 | `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages. <br>*Defaults to `60` seconds.* <br> **Datatype:** Positive Integer or 0
| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. <br> **Datatype:** Boolean
| `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file. <br> **Datatype:** String
@ -136,6 +137,7 @@ Values set in the configuration file always overwrite values set in the strategy
* `trailing_stop_positive` * `trailing_stop_positive`
* `trailing_stop_positive_offset` * `trailing_stop_positive_offset`
* `trailing_only_offset_is_reached` * `trailing_only_offset_is_reached`
* `use_custom_stoploss`
* `process_only_new_candles` * `process_only_new_candles`
* `order_types` * `order_types`
* `order_time_in_force` * `order_time_in_force`
@ -245,38 +247,16 @@ If it is not set in either Strategy or Configuration, a default of 1000% `{"0":
!!! Note "Special case to forcesell after a specific time" !!! Note "Special case to forcesell after a specific time"
A special case presents using `"<N>": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell. A special case presents using `"<N>": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell.
### Understand stoploss
Go to the [stoploss documentation](stoploss.md) for more details.
### Understand trailing stoploss
Go to the [trailing stoploss Documentation](stoploss.md#trailing-stop-loss) for details on trailing stoploss.
### Understand initial_state
The `initial_state` configuration parameter is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
### Understand forcebuy_enable ### Understand forcebuy_enable
The `forcebuy_enable` configuration parameter enables the usage of forcebuy commands via Telegram. The `forcebuy_enable` configuration parameter enables the usage of forcebuy commands via Telegram and REST API.
This is disabled for security reasons by default, and will show a warning message on startup if enabled. For security reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled.
For example, you can send `/forcebuy ETH/BTC` Telegram command when this feature if enabled to the bot, For example, you can send `/forcebuy ETH/BTC` to the bot, which will result in freqtrade buying the pair and holds it until a regular sell-signal (ROI, stoploss, /forcesell) appears.
who then buys the pair and holds it until a regular sell-signal (ROI, stoploss, /forcesell) appears.
This can be dangerous with some strategies, so use with care. This can be dangerous with some strategies, so use with care.
See [the telegram documentation](telegram-usage.md) for details on usage. See [the telegram documentation](telegram-usage.md) for details on usage.
### Understand process_throttle_secs
The `process_throttle_secs` configuration parameter is an optional field that defines in seconds how long the bot should wait
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
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 ### 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. 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.
@ -469,137 +449,9 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
``` ```
## Prices used for orders --8<-- "includes/pricing.md"
Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling. ## Using Dry-run mode
Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data.
!!! Note
Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details.
!!! Warning "Using market orders"
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
### Buy price
#### Check depth of market
When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side.
Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value.
!!! Note
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
#### Buy price side
The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying.
The following displays an orderbook.
``` explanation
...
103
102
101 # ask
-------------Current spread
99 # bid
98
97
...
```
If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price.
In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price.
Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
Taker fees instead of maker fees will most likely apply even when using limit buy orders.
Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
#### Buy price with Orderbook enabled
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Buy price without Orderbook enabled
The following section uses `side` as the configured `bid_strategy.price_side`.
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
### Sell price
#### Sell price side
The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling.
The following displays an orderbook:
``` explanation
...
103
102
101 # ask
-------------Current spread
99 # bid
98
97
...
```
If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price.
In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price.
#### Sell price with Orderbook enabled
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot.
!!! Note
Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`.
The idea here is to place the sell order early, to be ahead in the queue.
A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number.
!!! Warning "Order_book_max > 1 - increased risks for stoplosses!"
Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed.
Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange).
!!! Warning "Order_book_max > 1 in dry-run"
Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly.
It is therefore advised to not use this setting for dry-runs.
#### Sell price without Orderbook enabled
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
### Market order pricing
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
Assuming both buy and sell are using market orders, a configuration similar to the following might be used
``` jsonc
"order_types": {
"buy": "market",
"sell": "market"
// ...
},
"bid_strategy": {
"price_side": "ask",
// ...
},
"ask_strategy":{
"price_side": "bid",
// ...
},
```
Obviously, if only one side is using limit orders, different pricing combinations can be used.
--8<-- "includes/pairlists.md"
--8<-- "includes/protections.md"
## Switch to Dry-run mode
We recommend starting the bot in the Dry-run mode to see how your bot will We recommend starting the bot in the Dry-run mode to see how your bot will
behave and what is the performance of your strategy. In the Dry-run mode the behave and what is the performance of your strategy. In the Dry-run mode the
@ -632,9 +484,10 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
### Considerations for dry-run ### Considerations for dry-run
* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in the dry-run mode. * API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode.
* Wallets (`/balance`) are simulated. * Wallets (`/balance`) are simulated based on `dry_run_wallet`.
* Orders are simulated, and will not be posted to the exchange. * Orders are simulated, and will not be posted to the exchange.
* Orders are assumed to fill immediately, and will never time out.
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled. * In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
* Open orders (not trades, which are stored in the database) are reset on bot restart. * Open orders (not trades, which are stored in the database) are reset on bot restart.

View File

@ -264,7 +264,19 @@ If you are using Binance for example:
```bash ```bash
mkdir -p user_data/data/binance mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance cp tests/testdata/pairs.json user_data/data/binance
```
If you your configuration directory `user_data` was made by docker, you may get the following error:
```
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
```
You can fix the permissions of your user-data directory as follows:
```
sudo chown -R $UID:$GID user_data
``` ```
The format of the `pairs.json` file is a simple json list. The format of the `pairs.json` file is a simple json list.

View File

@ -2,7 +2,7 @@
This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. 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-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA) 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-mm786y93-Fxo37glxMY9g8OQC5AoOIw) where you can ask questions.
## Documentation ## Documentation
@ -177,7 +177,7 @@ In `VolumePairList`, this implements different methods of sorting, does early va
### Protections ### Protections
Best read the [Protection documentation](configuration.md#protections) to understand protections. Best read the [Protection documentation](plugins.md#protections) to understand protections.
This Guide is directed towards Developers who want to develop a new protection. This Guide is directed towards Developers who want to develop a new protection.
No protection should use datetime directly, but use the provided `date_now` variable for date calculations. This preserves the ability to backtest protections. No protection should use datetime directly, but use the provided `date_now` variable for date calculations. This preserves the ability to backtest protections.

View File

@ -9,6 +9,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri
`Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
`Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others. `Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others.
## Introduction ## Introduction
Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose.
@ -208,6 +209,60 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet.
- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. - **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2\%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**.
## Edge command reference
```
usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
optional arguments:
-h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
--stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
--stoplosses STOPLOSS_RANGE
Defines a range of stoploss values against which edge
will assess the strategy. The format is "min,max,step"
(without any space). Example:
`--stoplosses=-0.01,-0.1,-0.001`
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
## Configurations ## Configurations
Edge module has following configuration options: Edge module has following configuration options:

View File

@ -38,12 +38,11 @@ you can't say much from few trades.
### Id like to make changes to the config. Can I do that without having to kill the bot? ### Id like to make changes to the config. Can I do that without having to kill the bot?
Yes. You can edit your config, use the `/stop` command in Telegram, followed by `/reload_config` and the bot will run with the new config. Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy.
### I want to improve the bot with a new strategy ### I want to improve the bot with a new strategy
That's great. We have a nice backtesting and hyperoptimization setup. See That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
### Is there a setting to only SELL the coins being held and not perform anymore BUYS? ### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
@ -143,7 +142,7 @@ freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossD
### Why does it take a long time to run hyperopt? ### 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-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. * 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-mm786y93-Fxo37glxMY9g8OQC5AoOIw) - 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: * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers:

View File

@ -32,6 +32,107 @@ source .env/bin/activate
pip install -r requirements-hyperopt.txt pip install -r requirements-hyperopt.txt
``` ```
## Hyperopt command reference
```
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--dmmp] [--enable-protections] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME]
optional arguments:
-h, --help show this help message and exit
-i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME
Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `None`).
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
--stake-amount STAKE_AMOUNT
Override the value of the `stake_amount` configuration
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
--hyperopt NAME Specify hyperopt class name which will be used by the
bot.
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
Hyperopt Loss functions.
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number).
--enable-protections, --enableprotections
Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will
include configured protections
-e INT, --epochs INT Specify number of epochs (default: 100).
--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]
Specify which parameters to hyperopt. Space-separated
list.
--print-all Print all results, not only the best ones.
--no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file.
--print-json Print output in JSON format.
-j JOBS, --job-workers JOBS
The number of concurrently running jobs for
hyperoptimization (hyperopt worker processes). If -1
(default), all CPUs are used, for -2, all CPUs but one
are used, etc. If 1 is given, no parallel computing
code is used at all.
--random-state INT Set random state to some positive integer for
reproducible hyperopt results.
--min-trades INT Set minimal desired number of trades for evaluations
in the hyperopt optimization path (default: 1).
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
class (IHyperOptLoss). Different functions can
generate completely different results, since the
target for optimization is different. Built-in
Hyperopt-loss-functions are:
ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLoss, SharpeHyperOptLossDaily,
SortinoHyperOptLoss, SortinoHyperOptLossDaily
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
Strategy arguments:
-s NAME, --strategy NAME
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
## Prepare Hyperopting ## Prepare Hyperopting
Before we start digging into Hyperopt, we recommend you to take a look at Before we start digging into Hyperopt, we recommend you to take a look at

127
docs/includes/pricing.md Normal file
View File

@ -0,0 +1,127 @@
## Prices used for orders
Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling.
Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data.
!!! Note
Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details.
!!! Warning "Using market orders"
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
### Buy price
#### Check depth of market
When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side.
Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value.
!!! Note
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
#### Buy price side
The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying.
The following displays an orderbook.
``` explanation
...
103
102
101 # ask
-------------Current spread
99 # bid
98
97
...
```
If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price.
In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price.
Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
Taker fees instead of maker fees will most likely apply even when using limit buy orders.
Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
#### Buy price with Orderbook enabled
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Buy price without Orderbook enabled
The following section uses `side` as the configured `bid_strategy.price_side`.
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
### Sell price
#### Sell price side
The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling.
The following displays an orderbook:
``` explanation
...
103
102
101 # ask
-------------Current spread
99 # bid
98
97
...
```
If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price.
In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price.
#### Sell price with Orderbook enabled
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot.
!!! Note
Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`.
The idea here is to place the sell order early, to be ahead in the queue.
A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number.
!!! Warning "Order_book_max > 1 - increased risks for stoplosses!"
Using `ask_strategy.order_book_max` higher than 1 will increase the risk the stoploss on exchange is cancelled too early, since an eventual [stoploss on exchange](#understand-order_types) will be cancelled as soon as the order is placed.
Also, the sell order will remain on the exchange for `unfilledtimeout.sell` (or until it's filled) - which can lead to missed stoplosses (with or without using stoploss on exchange).
!!! Warning "Order_book_max > 1 in dry-run"
Using `ask_strategy.order_book_max` higher than 1 will result in improper dry-run results (significantly better than real orders executed on exchange), since dry-run assumes orders to be filled almost instantly.
It is therefore advised to not use this setting for dry-runs.
#### Sell price without Orderbook enabled
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
### Market order pricing
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
Assuming both buy and sell are using market orders, a configuration similar to the following might be used
``` jsonc
"order_types": {
"buy": "market",
"sell": "market"
// ...
},
"bid_strategy": {
"price_side": "ask",
// ...
},
"ask_strategy":{
"price_side": "bid",
// ...
},
```
Obviously, if only one side is using limit orders, different pricing combinations can be used.

View File

@ -40,7 +40,9 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
#### Stoploss Guard #### Stoploss Guard
`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`). `StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`).
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.

View File

@ -35,6 +35,22 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). - Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). - Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
## Supported exchange marketplaces
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#blacklists))
- [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com)
- [X] [Kraken](https://kraken.com/)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested
Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
## Requirements ## Requirements
### Hardware requirements ### Hardware requirements
@ -65,7 +81,7 @@ For any questions not covered by the documentation or for further information ab
Please check out our [discord server](https://discord.gg/MA9v74M). 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-l9d9iqgl-9cVBIeBkCBa8j6upSmd_NA). You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-mm786y93-Fxo37glxMY9g8OQC5AoOIw).
## Ready to try? ## Ready to try?

View File

@ -2,94 +2,49 @@
This page explains how to prepare your environment for running the bot. This page explains how to prepare your environment for running the bot.
The freqtrade documentation describes various ways to install freqtrade
* [Docker images](docker_quickstart.md) (separate page)
* [Script Installation](#script-installation)
* [Manual Installation](#manual-installation)
* [Installation with Conda](#installation-with-conda)
Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works. Please consider using the prebuilt [docker images](docker_quickstart.md) to get started quickly while evaluating how freqtrade works.
## Prerequisite ------
### Requirements ## Information
Click each one for install guide: For Windows installation, please use the [windows installation guide](windows_installation.md).
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the `./setup.sh` script, if it's available for your platform.
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below)
We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended.
!!! Warning "Up-to-date clock"
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
## Quick start
Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot.
!!! Note
Windows installation is explained [here](#windows).
The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform.
!!! Note "Version considerations" !!! Note "Version considerations"
When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests).
The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable).
!!! Note !!! Note
Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository.
Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully. Also, python headers (`python<yourversion>-dev` / `python<yourversion>-devel`) must be available for the installation to complete successfully.
This can be achieved with the following commands: !!! Warning "Up-to-date clock"
The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges.
```bash
git clone https://github.com/freqtrade/freqtrade.git
cd freqtrade
# git checkout stable # Optional, see (1)
./setup.sh --install
```
(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
## Easy Installation Script (Linux/MacOS)
If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot.
```bash
$ ./setup.sh
usage:
-i,--install Install freqtrade from scratch
-u,--update Command git pull to update.
-r,--reset Hard reset your develop/stable branch.
-c,--config Easy config generator (Will override your existing file).
```
** --install **
With this option, the script will install the bot and most dependencies:
You will need to have git and python3.7+ installed beforehand for this to work.
* Mandatory software as: `ta-lib`
* Setup your virtualenv under `.env/`
This option is a combination of installation tasks, `--reset` and `--config`.
** --update **
This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot.
** --reset **
This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv.
** --config **
DEPRECATED - use `freqtrade new-config -c config.json` instead.
### Activate your virtual environment
Each time you open a new terminal, you must run `source .env/bin/activate`.
------ ------
## Custom Installation ## Requirements
These requirements apply to both [Script Installation](#script-installation) and [Manual Installation](#manual-installation).
### Install guide
* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/)
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended)
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions [below](#install-ta-lib))
### Install code
We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros.
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
@ -97,12 +52,15 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
!!! Note !!! Note
Python3.7 or higher and the corresponding pip are assumed to be available. Python3.7 or higher and the corresponding pip are assumed to be available.
=== "Ubuntu/Debian" === "Debian/Ubuntu"
#### Install necessary dependencies #### Install necessary dependencies
```bash ```bash
# update repository
sudo apt-get update sudo apt-get update
sudo apt-get install build-essential git
# install packages
sudo apt install -y python3-pip python3-venv python3-pandas python3-pip git
``` ```
=== "RaspberryPi/Raspbian" === "RaspberryPi/Raspbian"
@ -112,7 +70,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
``` bash ```bash
sudo apt-get install python3-venv libatlas-base-dev cmake sudo apt-get install python3-venv libatlas-base-dev cmake
# Use pywheels.org to speed up installation # Use pywheels.org to speed up installation
sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf
@ -125,17 +83,106 @@ OS Specific steps are listed first, the [Common](#common) section below is neces
!!! Note "Installation duration" !!! Note "Installation duration"
Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete.
Due to this, we recommend to use the prebuild docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md) Due to this, we recommend to use the pre-build docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md)
!!! Note !!! Note
The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`.
We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine.
### Common ------
#### 1. Install TA-Lib ## Freqtrade repository
Use the provided ta-lib installation script Freqtrade is an open source crypto-currency trading bot, whose code is hosted on `github.com`
```bash
# Download `develop` branch of freqtrade repository
git clone https://github.com/freqtrade/freqtrade.git
# Enter downloaded directory
cd freqtrade
# your choice (1): novice user
git checkout stable
# your choice (2): advanced user
git checkout develop
```
(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed, if you wish to stay on the (2) `develop` branch.
You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands.
------
## Script Installation
First of the ways to install Freqtrade, is to use provided the Linux/MacOS `./setup.sh` script, which install all dependencies and help you configure the bot.
Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository).
### Use /setup.sh -install (Linux/MacOS)
If you are on Debian, Ubuntu or MacOS, freqtrade provides the script to install freqtrade.
```bash
# --install, Install freqtrade from scratch
./setup.sh -i
```
### Activate your virtual environment
Each time you open a new terminal, you must run `source .env/bin/activate` to activate your virtual environment.
```bash
# then activate your .env
source ./.env/bin/activate
```
### Congratulations
[You are ready](#you-are-ready), and run the bot
### Other options of /setup.sh script
You can as well update, configure and reset the codebase of your bot with `./script.sh`
```bash
# --update, Command git pull to update.
./setup.sh -u
# --reset, Hard reset your develop/stable branch.
./setup.sh -r
```
```
** --install **
With this option, the script will install the bot and most dependencies:
You will need to have git and python3.7+ installed beforehand for this to work.
* Mandatory software as: `ta-lib`
* Setup your virtualenv under `.env/`
This option is a combination of installation tasks and `--reset`
** --update **
This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot.
** --reset **
This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv.
```
-----
## Manual Installation
Make sure you fulfill the [Requirements](#requirements) and have downloaded the [Freqtrade repository](#freqtrade-repository).
### Install TA-Lib
#### TA-Lib script installation
```bash ```bash
sudo ./build_helpers/install_ta-lib.sh sudo ./build_helpers/install_ta-lib.sh
@ -160,78 +207,194 @@ cd ..
rm -rf ./ta-lib* rm -rf ./ta-lib*
``` ```
!!! Note #### Setup Python virtual environment (virtualenv)
An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
#### 2. Setup your Python virtual environment (virtualenv) You will run freqtrade in separated `virtual environment`
!!! Note
This step is optional but strongly recommended to keep your system organized
```bash ```bash
# create virtualenv in directory /freqtrade/.env
python3 -m venv .env python3 -m venv .env
# run virtualenv
source .env/bin/activate source .env/bin/activate
``` ```
#### 3. Install Freqtrade #### Install python dependencies
Clone the git repository:
```bash ```bash
git clone https://github.com/freqtrade/freqtrade.git
cd freqtrade
git checkout stable
```
#### 4. Install python dependencies
``` bash
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install -e . python3 -m pip install -e .
``` ```
#### 5. Initialize the configuration ### Congratulations
```bash [You are ready](#you-are-ready), and run the bot
# Initialize the user_directory
freqtrade create-userdir --userdir user_data/
# Create a new configuration file #### (Optional) Post-installation Tasks
freqtrade new-config --config config.json
```
> *To edit the config please refer to [Bot Configuration](configuration.md).* !!! Note
If you run the bot on a server, you should consider using [Docker](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
#### 6. Run the Bot On Linux with software suite `systemd`, as an optional post-installation task, you may wish to setup the bot to run as a `systemd service` or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details.
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
```bash
freqtrade trade -c config.json
```
*Note*: If you run the bot on a server, you should consider using [Docker compose](docker_quickstart.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
#### 7. (Optional) Post-installation Tasks
On Linux, as an optional post-installation task, you may wish to setup the bot to run as a `systemd` service or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details.
------ ------
### Anaconda ## Installation with Conda
Freqtrade can also be installed using Anaconda (or Miniconda). Freqtrade can also be installed with Miniconda or Anaconda. We recommend using Miniconda as it's installation footprint is smaller. Conda will automatically prepare and manage the extensive library-dependencies of the Freqtrade program.
!!! Note ### What is Conda?
This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below.
``` bash Conda is a package, dependency and environment manager for multiple programming languages: [conda docs](https://docs.conda.io/projects/conda/en/latest/index.html)
conda env create -f environment.yml
### Installation with conda
#### Install Conda
[Installing on linux](https://conda.io/projects/conda/en/latest/user-guide/install/linux.html#install-linux-silent)
[Installing on windows](https://conda.io/projects/conda/en/latest/user-guide/install/windows.html)
Answer all questions. After installation, it is mandatory to turn your terminal OFF and ON again.
#### Freqtrade download
Download and install freqtrade.
```bash
# download freqtrade
git clone https://github.com/freqtrade/freqtrade.git
# enter downloaded directory 'freqtrade'
cd freqtrade
``` ```
#### Freqtrade instal: Conda Environment
Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory
```bash
conda env create -n freqtrade-conda -f environment.yml
```
!!! Note "Creating Conda Environment"
The conda command `create -n` automatically installs all nested dependencies for the selected libraries, general structure of installation command is:
```bash
# choose your own packages
conda env create -n [name of the environment] [python version] [packages]
# point to file with packages
conda env create -n [name of the environment] -f [file]
```
#### Enter/exit freqtrade-conda environment
To check available environments, type
```bash
conda env list
```
Enter installed environment
```bash
# enter conda environment
conda activate freqtrade-conda
# exit conda environment - don't do it now
conda deactivate
```
Install last python dependencies with pip
```bash
python3 -m pip install --upgrade pip
python3 -m pip install -e .
```
### Congratulations
[You are ready](#you-are-ready), and run the bot
### Important shortcuts
```bash
# list installed conda environments
conda env list
# activate base environment
conda activate
# activate freqtrade-conda environment
conda activate freqtrade-conda
#deactivate any conda environments
conda deactivate
```
### Further info on anaconda
!!! Info "New heavy packages"
It may happen that creating a new Conda environment, populated with selected packages at the moment of creation takes less time than installing a large, heavy library or application, into previously set environment.
!!! Warning "pip install within conda"
The documentation of conda says that pip should NOT be used within conda, because internal problems can occur.
However, they are rare. [Anaconda Blogpost](https://www.anaconda.com/blog/using-pip-in-a-conda-environment)
Nevertheless, that is why, the `conda-forge` channel is preferred:
* more libraries are available (less need for `pip`)
* `conda-forge` works better with `pip`
* the libraries are newer
Happy trading!
----- -----
## You are ready
You've made it this far, so you have successfully installed freqtrade.
### Initialize the configuration
```bash
# Step 1 - Initialize user folder
freqtrade create-userdir --userdir user_data
# Step 2 - Create a new configuration file
freqtrade new-config --config config.json
```
You are ready to run, read [Bot Configuration](configuration.md), remember to start with `dry_run: True` and verify that everything is working.
To learn how to setup your configuration, please refer to the [Bot Configuration](configuration.md) documentation page.
### Start the Bot
```bash
freqtrade trade --config config.json --strategy SampleStrategy
```
!!! Warning
You should read through the rest of the documentation, backtest the strategy you're going to use, and use dry-run before enabling trading with real money.
-----
## Troubleshooting ## Troubleshooting
### Common problem: "command not found"
If you used (1)`Script` or (2)`Manual` installation, you need to run the bot in virtual environment. If you get error as below, make sure venv is active.
```bash
# if:
bash: freqtrade: command not found
# then activate your .env
source ./.env/bin/activate
```
### MacOS installation error ### MacOS installation error
Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`.
@ -239,7 +402,7 @@ Newer versions of MacOS may have installation failed with errors like `error: co
This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS.
For MacOS 10.14, this can be accomplished with the below command. For MacOS 10.14, this can be accomplished with the below command.
``` bash ```bash
open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
``` ```
@ -252,13 +415,8 @@ The errors you'll see happen during installation and are related to the installa
You can install the necessary libraries with the following command: You can install the necessary libraries with the following command:
``` bash ```bash
brew install hdf5 c-blosc brew install hdf5 c-blosc
``` ```
After this, please run the installation (script) again. After this, please run the installation (script) again.
-----
Now you have an environment ready, the next step is
[Bot Configuration](configuration.md).

View File

@ -188,7 +188,7 @@ Sample configuration with inline comments explaining the process:
'senkou_a': { 'senkou_a': {
'color': 'green', #optional 'color': 'green', #optional
'fill_to': 'senkou_b', 'fill_to': 'senkou_b',
'fill_label': 'Ichimoku Cloud' #optional, 'fill_label': 'Ichimoku Cloud', #optional
'fill_color': 'rgba(255,76,46,0.2)', #optional 'fill_color': 'rgba(255,76,46,0.2)', #optional
}, },
# plot senkou_b, too. Not only the area to it. # plot senkou_b, too. Not only the area to it.

View File

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

View File

@ -1,4 +1,19 @@
# REST API Usage # REST API & FreqUI
## FreqUI
Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI.
By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`.
This same command can also be used to update freqUI, should there be a new release.
Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`).
!!! info "Alpha release"
FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose).
!!! Note "developers"
Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI.
## Configuration ## Configuration
@ -23,9 +38,6 @@ Sample configuration:
!!! Danger "Security warning" !!! Danger "Security warning"
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
!!! Danger "Password selection"
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly.
This should return the response: This should return the response:
@ -35,16 +47,22 @@ This should return the response:
All other endpoints return sensitive info and require authentication and are therefore not available through a web browser. All other endpoints return sensitive info and require authentication and are therefore not available through a web browser.
To generate a secure password, either use a password manager, or use the below code snipped. ### Security
To generate a secure password, best use a password manager, or use the below code.
``` python ``` python
import secrets import secrets
secrets.token_hex() secrets.token_hex()
``` ```
!!! Hint !!! Hint "JWT token"
Use the same method to also generate a JWT secret key (`jwt_secret_key`). Use the same method to also generate a JWT secret key (`jwt_secret_key`).
!!! Danger "Password selection"
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!).
### Configuration with docker ### Configuration with docker
If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker. If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker.
@ -57,28 +75,20 @@ If you run your bot using docker, you'll need to have the bot listen to incoming
}, },
``` ```
Add the following to your docker command: Uncomment the following from your docker-compose file:
``` bash ```yml
-p 127.0.0.1:8080:8080 ports:
``` - "127.0.0.1:8080:8080"
A complete sample-command may then look as follows:
```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 \
-p 127.0.0.1:8080:8080 \
freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy
``` ```
!!! Danger "Security warning" !!! Danger "Security warning"
By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot.
## Consuming the API
## Rest API
### Consuming the API
You can consume the API by using the script `scripts/rest_client.py`. You can consume the API by using the script `scripts/rest_client.py`.
The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system.
@ -89,7 +99,7 @@ python3 scripts/rest_client.py <command> [optional parameters]
By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour.
### Minimalistic client config #### Minimalistic client config
``` json ``` json
{ {
@ -105,7 +115,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters] python3 scripts/rest_client.py --config rest_config.json <command> [optional parameters]
``` ```
## Available endpoints ### Available endpoints
| Command | Description | | Command | Description |
|----------|-------------| |----------|-------------|
@ -264,12 +274,12 @@ whitelist
``` ```
## OpenAPI interface ### OpenAPI interface
To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration. To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration.
This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings.
## Advanced API usage using JWT tokens ### Advanced API usage using JWT tokens
!!! Note !!! Note
The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis. The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis.
@ -294,9 +304,9 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"}
``` ```
## CORS ### CORS
All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing.
Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems.
Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately.

View File

@ -33,8 +33,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_profit: float, **kwargs) -> float: current_rate: float, current_profit: float, **kwargs) -> float:
""" """
Custom stoploss logic, returning the new distance relative to current_rate (as ratio). Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
e.g. returning -0.05 would create a stoploss 5% below current_rate. e.g. returning -0.05 would create a stoploss 5% below current_rate.
@ -83,8 +83,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_profit: float, **kwargs) -> float: current_rate: float, current_profit: float, **kwargs) -> float:
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom. # Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
if current_time - timedelta(minutes=120) > trade.open_date: if current_time - timedelta(minutes=120) > trade.open_date:
@ -109,8 +109,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_profit: float, **kwargs) -> float: current_rate: float, current_profit: float, **kwargs) -> float:
if pair in ('ETH/BTC', 'XRP/BTC'): if pair in ('ETH/BTC', 'XRP/BTC'):
return -0.10 return -0.10
@ -135,8 +135,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_profit: float, **kwargs) -> float: current_rate: float, current_profit: float, **kwargs) -> float:
if current_profit < 0.04: if current_profit < 0.04:
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
@ -167,8 +167,8 @@ class AwesomeStrategy(IStrategy):
use_custom_stoploss = True use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_profit: float, **kwargs) -> float: current_rate: float, current_profit: float, **kwargs) -> float:
# Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price
if current_profit > 0.40: if current_profit > 0.40:
@ -399,6 +399,17 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
!!! Note "Parent-strategy in different files"
If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly.
``` python
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent))
from myawesomestrategy import MyAwesomeStrategy
```
## Embedding Strategies ## Embedding Strategies
Freqtrade provides you with with an easy way to embed the strategy into your configuration file. Freqtrade provides you with with an easy way to embed the strategy into your configuration file.

View File

@ -315,11 +315,11 @@ class AwesomeStrategy(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists # Check if the entry already exists
if not metadata["pair"] in self._cust_info: if not metadata["pair"] in self.cust_info:
# Create empty entry for this pair # Create empty entry for this pair
self._cust_info[metadata["pair"]] = {} self.cust_info[metadata["pair"]] = {}
if "crosstime" in self.cust_info[metadata["pair"]: if "crosstime" in self.cust_info[metadata["pair"]]:
self.cust_info[metadata["pair"]]["crosstime"] += 1 self.cust_info[metadata["pair"]]["crosstime"] += 1
else: else:
self.cust_info[metadata["pair"]]["crosstime"] = 1 self.cust_info[metadata["pair"]]["crosstime"] = 1
@ -444,14 +444,19 @@ It can also be used in specific callbacks to get the signal that caused the acti
``` python ``` python
# fetch current dataframe # fetch current dataframe
if self.dp: if self.dp:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], if self.dp.runmode.value in ('live', 'dry_run'):
timeframe=self.timeframe) dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
timeframe=self.timeframe)
``` ```
!!! Note "No data available" !!! Note "No data available"
Returns an empty dataframe if the requested pair was not cached. Returns an empty dataframe if the requested pair was not cached.
This should not happen when using whitelisted pairs. This should not happen when using whitelisted pairs.
!!! Warning "Warning about backtesting"
This method will return an empty dataframe during backtesting.
### *orderbook(pair, maximum)* ### *orderbook(pair, maximum)*
``` python ``` python
@ -462,8 +467,8 @@ if self.dp:
dataframe['best_ask'] = ob['asks'][0][0] dataframe['best_ask'] = ob['asks'][0][0]
``` ```
!!! Warning !!! Warning "Warning about backtesting"
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used. The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values.
### *ticker(pair)* ### *ticker(pair)*

View File

@ -24,7 +24,7 @@ config["strategy"] = "SampleStrategy"
# Location of the data # Location of the data
data_location = Path(config['user_data_dir'], 'data', 'binance') data_location = Path(config['user_data_dir'], 'data', 'binance')
# Pair to analyze - Only use one pair here # Pair to analyze - Only use one pair here
pair = "BTC_USDT" pair = "BTC/USDT"
``` ```
@ -34,7 +34,9 @@ from freqtrade.data.history import load_pair_history
candles = load_pair_history(datadir=data_location, candles = load_pair_history(datadir=data_location,
timeframe=config["timeframe"], timeframe=config["timeframe"],
pair=pair) pair=pair,
data_format = "hdf5",
)
# Confirm success # Confirm success
print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}") print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}")

View File

@ -391,7 +391,7 @@ $ freqtrade list-markets --exchange kraken --all
## Test pairlist ## Test pairlist
Use the `test-pairlist` subcommand to test the configuration of [dynamic pairlists](configuration.md#pairlists). Use the `test-pairlist` subcommand to test the configuration of [dynamic pairlists](plugins.md#pairlists).
Requires a configuration with specified `pairlists` attribute. Requires a configuration with specified `pairlists` attribute.
Can be used to generate static pairlists to be used during backtesting / hyperopt. Can be used to generate static pairlists to be used during backtesting / hyperopt.
@ -415,7 +415,7 @@ optional arguments:
### Examples ### Examples
Show whitelist when using a [dynamic pairlist](configuration.md#pairlists). Show whitelist when using a [dynamic pairlist](plugins.md#pairlists).
``` ```
freqtrade test-pairlist --config config.json --quote USDT BTC freqtrade test-pairlist --config config.json --quote USDT BTC

View File

@ -1,60 +1,71 @@
name: freqtrade name: freqtrade
channels: channels:
- defaults
- conda-forge - conda-forge
# - defaults
dependencies: dependencies:
# Required for app # 1/4 req main
- python>=3.7 - python>=3.7
- pip - numpy
- wheel - pandas
- numpy - pip
- pandas
- SQLAlchemy - aiohttp
- arrow - SQLAlchemy
- requests - python-telegram-bot
- urllib3 - arrow
- wrapt - cachetools
- jsonschema - requests
- tabulate - urllib3
- python-rapidjson - wrapt
- flask - jsonschema
- python-dotenv
- cachetools
- python-telegram-bot
# Optional for plotting
- plotly
# Optional for hyperopt
- scipy
- scikit-optimize
- scikit-learn
- filelock
- joblib
# Optional for development
- flake8
- pytest
- pytest-mock
- pytest-asyncio
- pytest-cov
- coveralls
- mypy
# Useful for jupyter
- jupyter
- ipykernel
- isort
- yapf
- pip:
# Required for app
- cython
- pycoingecko
- ccxt
- TA-Lib - TA-Lib
- py_find_1st - tabulate
- jinja2
- blosc
- sdnotify - sdnotify
# Optional for develpment - fastapi
- flake8-tidy-imports - uvicorn
- flake8-type-annotations - pyjwt
- pytest-random-order - colorama
- -e . - questionary
- prompt-toolkit
# ============================
# 2/4 req dev
- coveralls
- flake8
- mypy
- pytest
- pytest-asyncio
- pytest-cov
- pytest-mock
- isort
- nbconvert
# ============================
# 3/4 req hyperopt
- scipy
- scikit-learn
- filelock
- scikit-optimize
- joblib
- progressbar2
# ============================
# 4/4 req plot
- plotly
- jupyter
- pip:
- pycoingecko
- py_find_1st
- tables
- pytest-random-order
- flake8-type-annotations
- ccxt
- flake8-tidy-imports
- -e .
# - python-rapidjso

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2021.1' __version__ = '2021.2'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -10,8 +10,8 @@ from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.data_commands import (start_convert_data, start_download_data, from freqtrade.commands.data_commands import (start_convert_data, start_download_data,
start_list_data) start_list_data)
from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
start_new_strategy) start_new_hyperopt, start_new_strategy)
from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show
from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts,
start_list_markets, start_list_strategies, start_list_markets, start_list_strategies,

View File

@ -70,6 +70,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "timeframe"] "trade_source", "timeframe"]
ARGS_INSTALL_UI = ["erase_ui_only"]
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
@ -167,8 +169,8 @@ class Arguments:
from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir,
start_download_data, start_edge, start_hyperopt, start_download_data, start_edge, start_hyperopt,
start_hyperopt_list, start_hyperopt_show, start_list_data, start_hyperopt_list, start_hyperopt_show, start_install_ui,
start_list_exchanges, start_list_hyperopts, start_list_data, start_list_exchanges, start_list_hyperopts,
start_list_markets, start_list_strategies, start_list_markets, start_list_strategies,
start_list_timeframes, start_new_config, start_new_hyperopt, start_list_timeframes, start_new_config, start_new_hyperopt,
start_new_strategy, start_plot_dataframe, start_plot_profit, start_new_strategy, start_plot_dataframe, start_plot_profit,
@ -355,6 +357,14 @@ class Arguments:
test_pairlist_cmd.set_defaults(func=start_test_pairlist) test_pairlist_cmd.set_defaults(func=start_test_pairlist)
self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd)
# Add install-ui subcommand
install_ui_cmd = subparsers.add_parser(
'install-ui',
help='Install FreqUI',
)
install_ui_cmd.set_defaults(func=start_install_ui)
self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd)
# Add Plotting subcommand # Add Plotting subcommand
plot_dataframe_cmd = subparsers.add_parser( plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe', 'plot-dataframe',

View File

@ -387,6 +387,12 @@ AVAILABLE_CLI_OPTIONS = {
help='Clean all existing data for the selected exchange/pairs/timeframes.', help='Clean all existing data for the selected exchange/pairs/timeframes.',
action='store_true', action='store_true',
), ),
"erase_ui_only": Arg(
'--erase',
help="Clean UI folder, don't download new version.",
action='store_true',
default=False,
),
# Templating options # Templating options
"template": Arg( "template": Arg(
'--template', '--template',

View File

@ -1,7 +1,9 @@
import logging import logging
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict, Optional, Tuple
import requests
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir
@ -137,3 +139,87 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None:
deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) deploy_new_hyperopt(args['hyperopt'], new_path, args['template'])
else: else:
raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") raise OperationalException("`new-hyperopt` requires --hyperopt to be set.")
def clean_ui_subdir(directory: Path):
if directory.is_dir():
logger.info("Removing UI directory content.")
for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root
if p.name in ('.gitkeep', 'fallback_file.html'):
continue
if p.is_file():
p.unlink()
elif p.is_dir():
p.rmdir()
def read_ui_version(dest_folder: Path) -> Optional[str]:
file = dest_folder / '.uiversion'
if not file.is_file():
return None
with file.open('r') as f:
return f.read()
def download_and_install_ui(dest_folder: Path, dl_url: str, version: str):
from io import BytesIO
from zipfile import ZipFile
logger.info(f"Downloading {dl_url}")
resp = requests.get(dl_url).content
dest_folder.mkdir(parents=True, exist_ok=True)
with ZipFile(BytesIO(resp)) as zf:
for fn in zf.filelist:
with zf.open(fn) as x:
destfile = dest_folder / fn.filename
if fn.is_dir():
destfile.mkdir(exist_ok=True)
else:
destfile.write_bytes(x.read())
with (dest_folder / '.uiversion').open('w') as f:
f.write(version)
def get_ui_download_url() -> Tuple[str, str]:
base_url = 'https://api.github.com/repos/freqtrade/frequi/'
# Get base UI Repo path
resp = requests.get(f"{base_url}releases")
resp.raise_for_status()
r = resp.json()
latest_version = r[0]['name']
assets = r[0].get('assets', [])
dl_url = ''
if assets and len(assets) > 0:
dl_url = assets[0]['browser_download_url']
# URL not found - try assets url
if not dl_url:
assets = r[0]['assets_url']
resp = requests.get(assets)
r = resp.json()
dl_url = r[0]['browser_download_url']
return dl_url, latest_version
def start_install_ui(args: Dict[str, Any]) -> None:
dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/'
# First make sure the assets are removed.
dl_url, latest_version = get_ui_download_url()
curr_version = read_ui_version(dest_folder)
if curr_version == latest_version and not args.get('erase_ui_only'):
logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.")
return
clean_ui_subdir(dest_folder)
if args.get('erase_ui_only'):
logger.info("Erased UI directory content. Not downloading new version.")
else:
# Download a new version
download_and_install_ui(dest_folder, dl_url, latest_version)

View File

@ -45,6 +45,16 @@ USERPATH_NOTEBOOKS = 'notebooks'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
# Define decimals per coin for outputs
# Only used for outputs.
DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's
DECIMALS_PER_COIN = {
'BTC': 8,
'ETH': 5,
}
# Soure files with destination directories within user-directory # Soure files with destination directories within user-directory
USER_DATA_FILES = { USER_DATA_FILES = {
'sample_strategy.py': USERPATH_STRATEGIES, 'sample_strategy.py': USERPATH_STRATEGIES,

View File

@ -2,9 +2,8 @@
Helpers when analyzing backtest data Helpers when analyzing backtest data
""" """
import logging import logging
from datetime import timezone
from pathlib import Path 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 numpy as np
import pandas as pd import pandas as pd
@ -16,9 +15,22 @@ from freqtrade.persistence import Trade, init_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# must align with columns in backtest.py # Old format - maybe remove?
BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "trade_duration", BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
"open_rate", "close_rate", "open_at_end", "sell_reason"] "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: 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: else:
# old format - only with lists. # 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'], df['open_date'] = pd.to_datetime(df['open_date'],
unit='s', unit='s',
@ -166,7 +178,10 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
utc=True, utc=True,
infer_datetime_format=True infer_datetime_format=True
) )
# Create compatibility with new format
df['profit_abs'] = df['close_rate'] - df['open_rate'] 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) df = df.sort_values("open_date").reset_index(drop=True)
return df 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] 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: def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataFrame:
""" """
Load trades from a DB (using dburl) 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) 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 = [] filters = []
if strategy: if strategy:
filters.append(Trade.strategy == strategy) filters.append(Trade.strategy == strategy)
trades = trade_list_to_dataframe(Trade.get_trades(filters).all())
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)
return trades return trades
@ -309,7 +312,7 @@ def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close"
end = df[column].dropna().iloc[-1] end = df[column].dropna().iloc[-1]
tmp_means.append((end - start) / start) tmp_means.append((end - start) / start)
return np.mean(tmp_means) return float(np.mean(tmp_means))
def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame], def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame],
@ -334,7 +337,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
""" """
Adds a column `col_name` with the cumulative profit for the given trades array. Adds a column `col_name` with the cumulative profit for the given trades array.
:param df: DataFrame with date index :param df: DataFrame with date index
:param trades: DataFrame containing trades (requires columns close_date and profit_percent) :param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
:param col_name: Column name that will be assigned the results :param col_name: Column name that will be assigned the results
:param timeframe: Timeframe used during the operations :param timeframe: Timeframe used during the operations
:return: Returns df with one additional column, col_name, containing the cumulative profit. :return: Returns df with one additional column, col_name, containing the cumulative profit.
@ -346,8 +349,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
timeframe_minutes = timeframe_to_minutes(timeframe) timeframe_minutes = timeframe_to_minutes(timeframe)
# Resample to timeframe to make sure trades match candles # Resample to timeframe to make sure trades match candles
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date'
)[['profit_percent']].sum() )[['profit_ratio']].sum()
df.loc[:, col_name] = _trades_sum['profit_percent'].cumsum() df.loc[:, col_name] = _trades_sum['profit_ratio'].cumsum()
# Set first value to 0 # Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0 df.loc[df.iloc[0].name, col_name] = 0
# FFill to get continuous # FFill to get continuous
@ -356,13 +359,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date',
value_col: str = 'profit_percent' value_col: str = 'profit_ratio'
) -> Tuple[float, pd.Timestamp, pd.Timestamp]: ) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
""" """
Calculate max drawdown and the corresponding close dates Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_date and profit_percent) :param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param date_col: Column in DataFrame to use for dates (defaults to 'close_date')
:param value_col: Column in DataFrame to use for values (defaults to 'profit_percent') :param value_col: Column in DataFrame to use for values (defaults to 'profit_ratio')
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
:raise: ValueError if trade-dataframe was found empty. :raise: ValueError if trade-dataframe was found empty.
""" """
@ -380,3 +383,21 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
low_date = profit_results.loc[idxmin, date_col] low_date = profit_results.loc[idxmin, date_col]
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date return abs(min(max_drawdown_df['drawdown'])), high_date, low_date
def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
"""
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
:param trades: DataFrame containing trades (requires columns close_date and profit_percent)
:return: Tuple (float, float) with cumsum of profit_abs
:raise: ValueError if trade-dataframe was found empty.
"""
if len(trades) == 0:
raise ValueError("Trade dataframe empty.")
csum_df = pd.DataFrame()
csum_df['sum'] = trades['profit_abs'].cumsum()
csum_min = csum_df['sum'].min()
csum_max = csum_df['sum'].max()
return csum_min, csum_max

View File

@ -86,8 +86,12 @@ class JsonDataHandler(IDataHandler):
filename = self._pair_data_filename(self._datadir, pair, timeframe) filename = self._pair_data_filename(self._datadir, pair, timeframe)
if not filename.exists(): if not filename.exists():
return DataFrame(columns=self._columns) return DataFrame(columns=self._columns)
pairdata = read_json(filename, orient='values') try:
pairdata.columns = self._columns pairdata = read_json(filename, orient='values')
pairdata.columns = self._columns
except ValueError:
logger.error(f"Could not load data for {pair}.")
return DataFrame(columns=self._columns)
pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
'low': 'float', 'close': 'float', 'volume': 'float'}) 'low': 'float', 'close': 'float', 'volume': 'float'})
pairdata['date'] = to_datetime(pairdata['date'], pairdata['date'] = to_datetime(pairdata['date'],

View File

@ -104,6 +104,7 @@ class Edge:
exchange=self.exchange, exchange=self.exchange,
timeframe=self.strategy.timeframe, timeframe=self.strategy.timeframe,
timerange=self._timerange, timerange=self._timerange,
data_format=self.config.get('dataformat_ohlcv', 'json'),
) )
data = load_data( data = load_data(
@ -159,7 +160,8 @@ class Edge:
available_capital = (total_capital + capital_in_trade) * self._capital_ratio available_capital = (total_capital + capital_in_trade) * self._capital_ratio
allowed_capital_at_risk = available_capital * self._allowed_risk allowed_capital_at_risk = available_capital * self._allowed_risk
max_position_size = abs(allowed_capital_at_risk / stoploss) max_position_size = abs(allowed_capital_at_risk / stoploss)
position_size = min(max_position_size, free_capital) # Position size must be below available capital.
position_size = min(min(max_position_size, free_capital), available_capital)
if pair in self._cached_pairs: if pair in self._cached_pairs:
logger.info( logger.info(
'winrate: %s, expectancy: %s, position size: %s, pair: %s,' 'winrate: %s, expectancy: %s, position size: %s, pair: %s,'

View File

@ -19,5 +19,11 @@ class Bittrex(Exchange):
""" """
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit_per_timeframe": {
'1m': 1440,
'5m': 288,
'1h': 744,
'1d': 365,
},
"l2_limit_range": [1, 25, 500], "l2_limit_range": [1, 25, 500],
} }

View File

@ -3,6 +3,7 @@
Cryptocurrency Exchanges support Cryptocurrency Exchanges support
""" """
import asyncio import asyncio
import http
import inspect import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
@ -17,7 +18,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
decimal_to_precision) decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, RetryableOrderError, InvalidOrderException, OperationalException, RetryableOrderError,
@ -34,6 +35,12 @@ CcxtModuleType = Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Workaround for adding samesite support to pre 3.8 python
# Only applies to python3.7, and only on certain exchanges (kraken)
# Replicates the fix from starlette (which is actually causing this problem)
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore
class Exchange: class Exchange:
_config: Dict = {} _config: Dict = {}
@ -66,6 +73,7 @@ class Exchange:
""" """
self._api: ccxt.Exchange = None self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._config.update(config) self._config.update(config)
@ -93,7 +101,6 @@ class Exchange:
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
# Assign this directly for easy access # Assign this directly for easy access
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination = self._ft_has['trades_pagination']
@ -129,7 +136,8 @@ class Exchange:
self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {})) self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0)) self.validate_required_startup_candles(config.get('startup_candle_count', 0),
config.get('timeframe', ''))
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get( self.markets_refresh_interval: int = exchange_config.get(
@ -190,24 +198,30 @@ class Exchange:
def timeframes(self) -> List[str]: def timeframes(self) -> List[str]:
return list((self._api.timeframes or {}).keys()) return list((self._api.timeframes or {}).keys())
@property
def ohlcv_candle_limit(self) -> int:
"""exchange ohlcv candle limit"""
return int(self._ohlcv_candle_limit)
@property @property
def markets(self) -> Dict: def markets(self) -> Dict:
"""exchange ccxt markets""" """exchange ccxt markets"""
if not self._api.markets: if not self._markets:
logger.info("Markets were not loaded. Loading them now..") logger.info("Markets were not loaded. Loading them now..")
self._load_markets() self._load_markets()
return self._api.markets return self._markets
@property @property
def precisionMode(self) -> str: def precisionMode(self) -> str:
"""exchange ccxt precisionMode""" """exchange ccxt precisionMode"""
return self._api.precisionMode return self._api.precisionMode
def ohlcv_candle_limit(self, timeframe: str) -> int:
"""
Exchange ohlcv candle limit
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limts
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
:param timeframe: Timeframe to check
:return: Candle limit as integer
"""
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
timeframe, self._ft_has.get('ohlcv_candle_limit')))
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]: pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]:
""" """
@ -291,7 +305,7 @@ class Exchange:
def _load_markets(self) -> None: def _load_markets(self) -> None:
""" Initialize markets both sync and async """ """ Initialize markets both sync and async """
try: try:
self._api.load_markets() self._markets = self._api.load_markets()
self._load_async_markets() self._load_async_markets()
self._last_markets_refresh = arrow.utcnow().int_timestamp self._last_markets_refresh = arrow.utcnow().int_timestamp
except ccxt.BaseError as e: except ccxt.BaseError as e:
@ -306,7 +320,7 @@ class Exchange:
return None return None
logger.debug("Performing scheduled market reload..") logger.debug("Performing scheduled market reload..")
try: 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 # Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True) self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp self._last_markets_refresh = arrow.utcnow().int_timestamp
@ -420,15 +434,16 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Time in force policies are not supported for {self.name} yet.') f'Time in force policies are not supported for {self.name} yet.')
def validate_required_startup_candles(self, startup_candles: int) -> None: def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None:
""" """
Checks if required startup_candles is more than ohlcv_candle_limit. Checks if required startup_candles is more than ohlcv_candle_limit().
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
""" """
if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']: candle_limit = self.ohlcv_candle_limit(timeframe)
if startup_candles + 5 > candle_limit:
raise OperationalException( raise OperationalException(
f"This strategy requires {startup_candles} candles to start. " f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {self._ft_has['ohlcv_candle_limit']}.") f"{self.name} only provides {candle_limit} for {timeframe}.")
def exchange_has(self, endpoint: str) -> bool: def exchange_has(self, endpoint: str) -> bool:
""" """
@ -489,6 +504,41 @@ class Exchange:
else: else:
return 1 / pow(10, precision) return 1 / pow(10, precision)
def get_min_pair_stake_amount(self, pair: str, price: float,
stoploss: float) -> Optional[float]:
try:
market = self.markets[pair]
except KeyError:
raise ValueError(f"Can't get market information for symbol {pair}")
if 'limits' not in market:
return None
min_stake_amounts = []
limits = market['limits']
if ('cost' in limits and 'min' in limits['cost']
and limits['cost']['min'] is not None):
min_stake_amounts.append(limits['cost']['min'])
if ('amount' in limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
min_stake_amounts.append(limits['amount']['min'] * price)
if not min_stake_amounts:
return None
# reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent',
DEFAULT_AMOUNT_RESERVE_PERCENT)
amount_reserve_percent += stoploss
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
# The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here.
# See also #2575 at github.
return max(min_stake_amounts) / amount_reserve_percent
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]: rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}' order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
@ -660,8 +710,8 @@ class Exchange:
@retrier @retrier
def fetch_ticker(self, pair: str) -> dict: def fetch_ticker(self, pair: str) -> dict:
try: try:
if (pair not in self._api.markets or if (pair not in self.markets or
self._api.markets[pair].get('active', False) is False): self.markets[pair].get('active', False) is False):
raise ExchangeError(f"Pair {pair} not available") raise ExchangeError(f"Pair {pair} not available")
data = self._api.fetch_ticker(pair) data = self._api.fetch_ticker(pair)
return data return data
@ -678,7 +728,7 @@ class Exchange:
""" """
Get candle history using asyncio and returns the list of candles. Get candle history using asyncio and returns the list of candles.
Handles all async work for this. Handles all async work for this.
Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param timeframe: Timeframe to get data for :param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from :param since_ms: Timestamp in milliseconds to get history from
@ -708,7 +758,7 @@ class Exchange:
Download historic ohlcv Download historic ohlcv
""" """
one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
logger.debug( logger.debug(
"one_call: %s msecs (%s)", "one_call: %s msecs (%s)",
one_call, one_call,
@ -810,7 +860,7 @@ class Exchange:
data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
since=since_ms, since=since_ms,
limit=self._ohlcv_candle_limit) limit=self.ohlcv_candle_limit(timeframe))
# Some exchanges sort OHLCV in ASC order and others in DESC. # Some exchanges sort OHLCV in ASC order and others in DESC.
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
@ -983,7 +1033,7 @@ class Exchange:
""" """
Get trade history data using asyncio. Get trade history data using asyncio.
Handles all async work and returns the list of candles. Handles all async work and returns the list of candles.
Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call. Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
:param pair: Pair to download :param pair: Pair to download
:param since: Timestamp in milliseconds to get history from :param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.

View File

@ -179,6 +179,7 @@ class FreqtradeBot(LoggingMixin):
# Without this, freqtrade my try to recreate stoploss_on_exchange orders # Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while selling is in process, since telegram messages arrive in an different thread. # while selling is in process, since telegram messages arrive in an different thread.
with self._sell_lock: with self._sell_lock:
trades = Trade.get_open_trades()
# First process current opened trades (positions) # First process current opened trades (positions)
self.exit_positions(trades) self.exit_positions(trades)
@ -233,7 +234,7 @@ class FreqtradeBot(LoggingMixin):
_whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
return _whitelist return _whitelist
def get_free_open_trades(self): def get_free_open_trades(self) -> int:
""" """
Return the number of free open trades slots or 0 if Return the number of free open trades slots or 0 if
max number of open trades reached max number of open trades reached
@ -246,7 +247,7 @@ class FreqtradeBot(LoggingMixin):
Updates open orders based on order list kept in the database. Updates open orders based on order list kept in the database.
Mainly updates the state of orders - but may also close trades Mainly updates the state of orders - but may also close trades
""" """
if self.config['dry_run']: if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False):
# Updating open orders in dry-run does not make sense and will fail. # Updating open orders in dry-run does not make sense and will fail.
return return
@ -439,117 +440,6 @@ class FreqtradeBot(LoggingMixin):
return used_rate return used_rate
def get_trade_stake_amount(self, pair: str) -> float:
"""
Calculate stake amount for the trade
:return: float: Stake amount
:raise: DependencyException if the available stake amount is too low
"""
stake_amount: float
# Ensure wallets are uptodate.
self.wallets.update()
if self.edge:
stake_amount = self.edge.stake_amount(
pair,
self.wallets.get_free(self.config['stake_currency']),
self.wallets.get_total(self.config['stake_currency']),
Trade.total_open_trades_stakes()
)
else:
stake_amount = self.config['stake_amount']
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
stake_amount = self._calculate_unlimited_stake_amount()
return self._check_available_stake_amount(stake_amount)
def _get_available_stake_amount(self) -> float:
"""
Return the total currently available balance in stake currency,
respecting tradable_balance_ratio.
Calculated as
<open_trade stakes> + free amount ) * tradable_balance_ratio - <open_trade stakes>
"""
val_tied_up = Trade.total_open_trades_stakes()
# Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade.
# (tied up + current free) * ratio) - tied up
available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) *
self.config['tradable_balance_ratio']) - val_tied_up
return available_amount
def _calculate_unlimited_stake_amount(self) -> float:
"""
Calculate stake amount for "unlimited" stake amount
:return: 0 if max number of trades reached, else stake_amount to use.
"""
free_open_trades = self.get_free_open_trades()
if not free_open_trades:
return 0
available_amount = self._get_available_stake_amount()
return available_amount / free_open_trades
def _check_available_stake_amount(self, stake_amount: float) -> float:
"""
Check if stake amount can be fulfilled with the available balance
for the stake currency
:return: float: Stake amount
"""
available_amount = self._get_available_stake_amount()
if self.config['amend_last_stake_amount']:
# Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio
# Otherwise the remaining amount is too low to trade.
if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']):
stake_amount = min(stake_amount, available_amount)
else:
stake_amount = 0
if available_amount < stake_amount:
raise DependencyException(
f"Available balance ({available_amount} {self.config['stake_currency']}) is "
f"lower than stake amount ({stake_amount} {self.config['stake_currency']})"
)
return stake_amount
def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]:
try:
market = self.exchange.markets[pair]
except KeyError:
raise ValueError(f"Can't get market information for symbol {pair}")
if 'limits' not in market:
return None
min_stake_amounts = []
limits = market['limits']
if ('cost' in limits and 'min' in limits['cost']
and limits['cost']['min'] is not None):
min_stake_amounts.append(limits['cost']['min'])
if ('amount' in limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
min_stake_amounts.append(limits['amount']['min'] * price)
if not min_stake_amounts:
return None
# reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent',
constants.DEFAULT_AMOUNT_RESERVE_PERCENT)
amount_reserve_percent += self.strategy.stoploss
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
# The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here.
# See also #2575 at github.
return max(min_stake_amounts) / amount_reserve_percent
def create_trade(self, pair: str) -> bool: def create_trade(self, pair: str) -> bool:
""" """
Check the implemented trading strategy for buy signals. Check the implemented trading strategy for buy signals.
@ -583,7 +473,8 @@ class FreqtradeBot(LoggingMixin):
(buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
if buy and not sell: if buy and not sell:
stake_amount = self.get_trade_stake_amount(pair) stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(),
self.edge)
if not stake_amount: if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False return False
@ -646,7 +537,8 @@ class FreqtradeBot(LoggingMixin):
if not buy_limit_requested: if not buy_limit_requested:
raise PricingError('Could not determine buy price.') raise PricingError('Could not determine buy price.')
min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested) min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
self.strategy.stoploss)
if min_stake_amount is not None and min_stake_amount > stake_amount: if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning( logger.warning(
f"Can't open a new trade for {pair}: stake amount " f"Can't open a new trade for {pair}: stake amount "
@ -998,7 +890,8 @@ class FreqtradeBot(LoggingMixin):
logger.warning('Stoploss order was cancelled, but unable to recreate one.') logger.warning('Stoploss order was cancelled, but unable to recreate one.')
# Finally we check if stoploss on exchange should be moved up because of trailing. # Finally we check if stoploss on exchange should be moved up because of trailing.
if stoploss_order and self.config.get('trailing_stop', False): if stoploss_order and (self.config.get('trailing_stop', False)
or self.config.get('use_custom_stoploss', False)):
# if trailing stoploss is enabled we check if stoploss value has changed # if trailing stoploss is enabled we check if stoploss value has changed
# in which case we cancel stoploss order and put another one with new # in which case we cancel stoploss order and put another one with new
# value immediately # value immediately
@ -1178,7 +1071,9 @@ class FreqtradeBot(LoggingMixin):
if not self.exchange.check_order_canceled_empty(order): if not self.exchange.check_order_canceled_empty(order):
try: try:
# if trade is not partially completed, just delete the order # if trade is not partially completed, just delete the order
self.exchange.cancel_order(trade.open_order_id, trade.pair) co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
trade.update_order(co)
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}") logger.exception(f"Could not cancel sell order {trade.open_order_id}")
return 'error cancelling order' return 'error cancelling order'
@ -1186,6 +1081,7 @@ class FreqtradeBot(LoggingMixin):
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade) logger.info('Sell order %s for %s.', reason, trade)
trade.update_order(order)
trade.close_rate = None trade.close_rate = None
trade.close_rate_requested = None trade.close_rate_requested = None
@ -1288,6 +1184,7 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.sell_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately

View File

@ -9,13 +9,37 @@ from pathlib import Path
from typing import Any from typing import Any
from typing.io import IO from typing.io import IO
import numpy as np
import rapidjson import rapidjson
from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def decimals_per_coin(coin: str):
"""
Helper method getting decimal amount for this coin
example usage: f".{decimals_per_coin('USD')}f"
:param coin: Which coin are we printing the price / value for
"""
return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK)
def round_coin_value(value: float, coin: str, show_coin_name=True) -> str:
"""
Get price value for this coin
:param value: Value to be printed
:param coin: Which coin are we printing the price / value for
:param show_coin_name: Return string in format: "222.22 USDT" or "222.22"
:return: Formatted / rounded value (with or without coin name)
"""
if show_coin_name:
return f"{value:.{decimals_per_coin(coin)}f} {coin}"
else:
return f"{value:.{decimals_per_coin(coin)}f}"
def shorten_date(_date: str) -> str: def shorten_date(_date: str) -> str:
""" """
Trim the date so it fits on small screens Trim the date so it fits on small screens
@ -28,20 +52,6 @@ def shorten_date(_date: str) -> str:
return new_date return new_date
############################################
# Used by scripts #
# Matplotlib doesn't support ::datetime64, #
# so we need to convert it into ::datetime #
############################################
def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
"""
Convert an pandas-array of timestamps into
An numpy-array of datetimes
:return: numpy-array of datetime
"""
return dates.dt.to_pydatetime()
def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None: def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None:
""" """
Dump JSON data into a file Dump JSON data into a file

View File

@ -7,13 +7,14 @@ import logging
from collections import defaultdict from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, NamedTuple, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@ -41,25 +42,6 @@ LOW_IDX = 5
HIGH_IDX = 6 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: class Backtesting:
""" """
Backtesting class, this class contains all the logic to run a backtest Backtesting class, this class contains all the logic to run a backtest
@ -264,7 +246,7 @@ class Backtesting:
else: else:
return sell_row[OPEN_IDX] 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 = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX],
sell_row[BUY_IDX], sell_row[SELL_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
@ -276,25 +258,12 @@ class Backtesting:
trade.close_date = sell_row[DATE_IDX] trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = sell.sell_type trade.sell_reason = sell.sell_type
trade.close(closerate, show_msg=False) 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 return None
def handle_left_open(self, open_trades: Dict[str, List[Trade]], 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 Handling of left open trades at the end of backtesting
""" """
@ -304,24 +273,11 @@ class Backtesting:
for trade in open_trades[pair]: for trade in open_trades[pair]:
sell_row = data[pair][-1] sell_row = data[pair][-1]
trade_entry = BacktestResult(pair=trade.pair, trade.close_date = sell_row[DATE_IDX]
profit_percent=trade.calc_profit_ratio( trade.sell_reason = SellType.FORCE_SELL
rate=sell_row[OPEN_IDX]), trade.close(sell_row[OPEN_IDX], show_msg=False)
profit_abs=trade.calc_profit(sell_row[OPEN_IDX]), trade.is_open = True
open_date=trade.open_date, trades.append(trade)
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)
return trades return trades
def backtest(self, processed: Dict, stake_amount: float, def backtest(self, processed: Dict, stake_amount: float,
@ -348,7 +304,7 @@ class Backtesting:
f"start_date: {start_date}, end_date: {end_date}, " f"start_date: {start_date}, end_date: {end_date}, "
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
) )
trades = [] trades: List[Trade] = []
self.prepare_backtest(enable_protections) self.prepare_backtest(enable_protections)
# Use dict of lists with data for performance # Use dict of lists with data for performance
@ -429,7 +385,7 @@ class Backtesting:
trades += self.handle_left_open(open_trades, data=data) 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): def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())

View File

@ -42,7 +42,7 @@ class ShortTradeDurHyperOptLoss(IHyperOptLoss):
* 0.25: Avoiding trade loss * 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above * 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_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)

View File

@ -546,10 +546,11 @@ class Hyperopt:
) )
return self._get_results_dict(backtesting_results, min_date, max_date, return self._get_results_dict(backtesting_results, min_date, max_date,
params_dict, params_details) params_dict, params_details,
processed=processed)
def _get_results_dict(self, backtesting_results, min_date, max_date, def _get_results_dict(self, backtesting_results, min_date, max_date,
params_dict, params_details): params_dict, params_details, processed: Dict[str, DataFrame]):
results_metrics = self._calculate_results_metrics(backtesting_results) results_metrics = self._calculate_results_metrics(backtesting_results)
results_explanation = self._format_results_explanation_string(results_metrics) results_explanation = self._format_results_explanation_string(results_metrics)
@ -563,7 +564,8 @@ class Hyperopt:
loss: float = MAX_LOSS loss: float = MAX_LOSS
if trade_count >= self.config['hyperopt_min_trades']: if trade_count >= self.config['hyperopt_min_trades']:
loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count, loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count,
min_date=min_date.datetime, max_date=max_date.datetime) min_date=min_date.datetime, max_date=max_date.datetime,
config=self.config, processed=processed)
return { return {
'loss': loss, 'loss': loss,
'params_dict': params_dict, 'params_dict': params_dict,
@ -574,20 +576,20 @@ class Hyperopt:
} }
def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict:
wins = len(backtesting_results[backtesting_results.profit_percent > 0]) wins = len(backtesting_results[backtesting_results['profit_ratio'] > 0])
draws = len(backtesting_results[backtesting_results.profit_percent == 0]) draws = len(backtesting_results[backtesting_results['profit_ratio'] == 0])
losses = len(backtesting_results[backtesting_results.profit_percent < 0]) losses = len(backtesting_results[backtesting_results['profit_ratio'] < 0])
return { return {
'trade_count': len(backtesting_results.index), 'trade_count': len(backtesting_results.index),
'wins': wins, 'wins': wins,
'draws': draws, 'draws': draws,
'losses': losses, 'losses': losses,
'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}",
'avg_profit': backtesting_results.profit_percent.mean() * 100.0, 'avg_profit': backtesting_results['profit_ratio'].mean() * 100.0,
'median_profit': backtesting_results.profit_percent.median() * 100.0, 'median_profit': backtesting_results['profit_ratio'].median() * 100.0,
'total_profit': backtesting_results.profit_abs.sum(), 'total_profit': backtesting_results['profit_abs'].sum(),
'profit': backtesting_results.profit_percent.sum() * 100.0, 'profit': backtesting_results['profit_ratio'].sum() * 100.0,
'duration': backtesting_results.trade_duration.mean(), 'duration': backtesting_results['trade_duration'].mean(),
} }
def _format_results_explanation_string(self, results_metrics: Dict) -> str: def _format_results_explanation_string(self, results_metrics: Dict) -> str:

View File

@ -5,6 +5,7 @@ This module defines the interface for the loss-function for hyperopt
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from typing import Dict
from pandas import DataFrame from pandas import DataFrame
@ -19,7 +20,9 @@ class IHyperOptLoss(ABC):
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, *args, **kwargs) -> float: min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results
""" """

View File

@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
""" """
Objective function, returns smaller number for better results. 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 return 1 - total_profit / EXPECTED_MAX_PROFIT

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,9 @@ from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
from freqtrade.misc import file_dump_json calculate_max_drawdown)
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,11 +39,12 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
def _get_line_floatfmt() -> List[str]: def _get_line_floatfmt(stake_currency: str) -> List[str]:
""" """
Generate floatformat (goes in line with _generate_result_line()) Generate floatformat (goes in line with _generate_result_line())
""" """
return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd'] return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f',
'.2f', 'd', 'd', 'd', 'd']
def _get_line_header(first_column: str, stake_currency: str) -> List[str]: def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
@ -58,14 +60,14 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
""" """
Generate one result dict, with "first_column" as key. 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 profit_total = profit_sum / max_open_trades
return { return {
'key': first_column, 'key': first_column,
'trades': len(result), 'trades': len(result),
'profit_mean': result['profit_percent'].mean() 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_percent'].mean() * 100.0 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': profit_sum,
'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_sum_pct': round(profit_sum * 100.0, 2),
'profit_total_abs': result['profit_abs'].sum(), 'profit_total_abs': result['profit_abs'].sum(),
@ -124,8 +126,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
for reason, count in results['sell_reason'].value_counts().iteritems(): for reason, count in results['sell_reason'].value_counts().iteritems():
result = results.loc[results['sell_reason'] == reason] result = results.loc[results['sell_reason'] == reason]
profit_mean = result['profit_percent'].mean() profit_mean = result['profit_ratio'].mean()
profit_sum = result['profit_percent'].sum() profit_sum = result['profit_ratio'].sum()
profit_total = profit_sum / max_open_trades profit_total = profit_sum / max_open_trades
tabular_data.append( tabular_data.append(
@ -150,7 +152,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
def generate_strategy_metrics(all_results: Dict) -> List[Dict]: def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
""" """
Generate summary per strategy 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 :return: List of Dicts containing the metrics per Strategy
""" """
@ -199,15 +201,15 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
'winner_holding_avg': timedelta(), 'winner_holding_avg': timedelta(),
'loser_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) worst = min(daily_profit)
best = max(daily_profit) best = max(daily_profit)
winning_days = sum(daily_profit > 0) winning_days = sum(daily_profit > 0)
draw_days = sum(daily_profit == 0) draw_days = sum(daily_profit == 0)
losing_days = sum(daily_profit < 0) losing_days = sum(daily_profit < 0)
winning_trades = results.loc[results['profit_percent'] > 0] winning_trades = results.loc[results['profit_ratio'] > 0]
losing_trades = results.loc[results['profit_percent'] < 0] losing_trades = results.loc[results['profit_ratio'] < 0]
return { return {
'backtest_best_day': best, 'backtest_best_day': best,
@ -243,7 +245,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
if not isinstance(results, DataFrame): if not isinstance(results, DataFrame):
continue continue
config = content['config'] 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'] stake_currency = config['stake_currency']
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
@ -253,7 +255,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
results=results) results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=max_open_trades, max_open_trades=max_open_trades,
results=results.loc[results['open_at_end']], results=results.loc[results['is_open']],
skip_nan=True) skip_nan=True)
daily_stats = generate_daily_stats(results) daily_stats = generate_daily_stats(results)
best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'],
@ -273,8 +275,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'sell_reason_summary': sell_reason_stats, 'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results, 'left_open_trades': left_open_results,
'total_trades': len(results), 'total_trades': len(results),
'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_total': results['profit_percent'].sum(), 'profit_total': results['profit_ratio'].sum() / max_open_trades,
'profit_total_abs': results['profit_abs'].sum(), 'profit_total_abs': results['profit_abs'].sum(),
'backtest_start': min_date.datetime, 'backtest_start': min_date.datetime,
'backtest_start_ts': min_date.int_timestamp * 1000, 'backtest_start_ts': min_date.int_timestamp * 1000,
@ -290,8 +292,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'pairlist': list(btdata.keys()), 'pairlist': list(btdata.keys()),
'stake_amount': config['stake_amount'], 'stake_amount': config['stake_amount'],
'stake_currency': config['stake_currency'], 'stake_currency': config['stake_currency'],
'max_open_trades': (config['max_open_trades'] 'max_open_trades': max_open_trades,
if config['max_open_trades'] != float('inf') else -1), 'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1),
'timeframe': config['timeframe'], 'timeframe': config['timeframe'],
'timerange': config.get('timerange', ''), 'timerange': config.get('timerange', ''),
'enable_protections': config.get('enable_protections', False), 'enable_protections': config.get('enable_protections', False),
@ -314,7 +317,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
try: try:
max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown(
results, value_col='profit_percent') results, value_col='profit_ratio')
strat_stats.update({ strat_stats.update({
'max_drawdown': max_drawdown, 'max_drawdown': max_drawdown,
'drawdown_start': drawdown_start, 'drawdown_start': drawdown_start,
@ -322,6 +325,13 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'drawdown_end': drawdown_end, 'drawdown_end': drawdown_end,
'drawdown_end_ts': drawdown_end.timestamp() * 1000, 'drawdown_end_ts': drawdown_end.timestamp() * 1000,
}) })
csum_min, csum_max = calculate_csum(results)
strat_stats.update({
'csum_min': csum_min,
'csum_max': csum_max
})
except ValueError: except ValueError:
strat_stats.update({ strat_stats.update({
'max_drawdown': 0.0, 'max_drawdown': 0.0,
@ -329,6 +339,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'drawdown_start_ts': 0, 'drawdown_start_ts': 0,
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_end_ts': 0, 'drawdown_end_ts': 0,
'csum_min': 0,
'csum_max': 0
}) })
strategy_results = generate_strategy_metrics(all_results=all_results) strategy_results = generate_strategy_metrics(all_results=all_results)
@ -351,7 +363,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
""" """
headers = _get_line_header('Pair', stake_currency) headers = _get_line_header('Pair', stake_currency)
floatfmt = _get_line_floatfmt() floatfmt = _get_line_floatfmt(stake_currency)
output = [[ output = [[
t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'],
t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses']
@ -382,7 +394,9 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
output = [[ output = [[
t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'],
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], t['profit_mean_pct'], t['profit_sum_pct'],
round_coin_value(t['profit_total_abs'], stake_currency, False),
t['profit_total_pct'],
] for t in sell_reason_stats] ] for t in sell_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
@ -392,10 +406,10 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
Generate summary table per strategy Generate summary table per strategy
:param stake_currency: stake-currency - used to correctly name headers :param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades used for backtest :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 :return: pretty printed table with tabulate as string
""" """
floatfmt = _get_line_floatfmt() floatfmt = _get_line_floatfmt(stake_currency)
headers = _get_line_header('Strategy', stake_currency) headers = _get_line_header('Strategy', stake_currency)
output = [[ output = [[
@ -409,8 +423,8 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
def text_table_add_metrics(strat_results: Dict) -> str: def text_table_add_metrics(strat_results: Dict) -> str:
if len(strat_results['trades']) > 0: if len(strat_results['trades']) > 0:
best_trade = max(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_percent']) worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
metrics = [ metrics = [
('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
@ -424,9 +438,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
('Worst Pair', f"{strat_results['worst_pair']['key']} " ('Worst Pair', f"{strat_results['worst_pair']['key']} "
f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), 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']} " ('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)}%"), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"),
('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"),
@ -435,6 +449,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('', ''), # Empty line to improve readability ('', ''), # Empty line to improve readability
('Abs Profit Min', round_coin_value(strat_results['csum_min'],
strat_results['stake_currency'])),
('Abs Profit Max', round_coin_value(strat_results['csum_max'],
strat_results['stake_currency'])),
('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),

View File

@ -171,6 +171,10 @@ class Order(_DECL_BASE):
""" """
Get all non-closed orders - useful when trying to batch-update orders Get all non-closed orders - useful when trying to batch-update orders
""" """
if not isinstance(order, dict):
logger.warning(f"{order} is not a valid response object.")
return
filtered_orders = [o for o in orders if o.order_id == order.get('id')] filtered_orders = [o for o in orders if o.order_id == order.get('id')]
if filtered_orders: if filtered_orders:
oobj = filtered_orders[0] oobj = filtered_orders[0]
@ -302,6 +306,11 @@ class Trade(_DECL_BASE):
'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'close_profit_abs': self.close_profit_abs, # Deprecated '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_ratio': self.close_profit,
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs, 'profit_abs': self.close_profit_abs,

View File

@ -53,7 +53,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
data_format=config.get('dataformat_ohlcv', 'json'), data_format=config.get('dataformat_ohlcv', 'json'),
) )
if startup_candles: if startup_candles and data:
min_date, max_date = get_timerange(data) min_date, max_date = get_timerange(data)
logger.info(f"Loading data from {min_date} to {max_date}") logger.info(f"Loading data from {min_date} to {max_date}")
timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')),
@ -67,14 +67,16 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
if not filename.is_dir() and not filename.is_file(): if not filename.is_dir() and not filename.is_file():
logger.warning("Backtest file is missing skipping trades.") logger.warning("Backtest file is missing skipping trades.")
no_trades = True no_trades = True
try:
trades = load_trades( trades = load_trades(
config['trade_source'], config['trade_source'],
db_url=config.get('db_url'), db_url=config.get('db_url'),
exportfilename=filename, exportfilename=filename,
no_trades=no_trades, no_trades=no_trades,
strategy=config.get('strategy'), strategy=config.get('strategy'),
) )
except ValueError as e:
raise OperationalException(e) from e
trades = trim_dataframe(trades, timerange, 'open_date') trades = trim_dataframe(trades, timerange, 'open_date')
return {"ohlcv": data, return {"ohlcv": data,
@ -175,7 +177,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
# Trades can be empty # Trades can be empty
if trades is not None and len(trades) > 0: if trades is not None and len(trades) > 0:
# Create description for sell summarizing the trade # 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['sell_reason']}, "
f"{row['trade_duration']} min", f"{row['trade_duration']} min",
axis=1) axis=1)
@ -195,9 +197,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
) )
trade_sells = go.Scatter( trade_sells = go.Scatter(
x=trades.loc[trades['profit_percent'] > 0, "close_date"], x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
y=trades.loc[trades['profit_percent'] > 0, "close_rate"], y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
text=trades.loc[trades['profit_percent'] > 0, "desc"], text=trades.loc[trades['profit_ratio'] > 0, "desc"],
mode='markers', mode='markers',
name='Sell - Profit', name='Sell - Profit',
marker=dict( marker=dict(
@ -208,9 +210,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
) )
) )
trade_sells_loss = go.Scatter( trade_sells_loss = go.Scatter(
x=trades.loc[trades['profit_percent'] <= 0, "close_date"], x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
y=trades.loc[trades['profit_percent'] <= 0, "close_rate"], y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
text=trades.loc[trades['profit_percent'] <= 0, "desc"], text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
mode='markers', mode='markers',
name='Sell - Loss', name='Sell - Loss',
marker=dict( marker=dict(

View File

@ -30,10 +30,10 @@ class AgeFilter(IPairList):
if self._min_days_listed < 1: if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1") raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > exchange.ohlcv_candle_limit: if self._min_days_listed > exchange.ohlcv_candle_limit('1d'):
raise OperationalException("AgeFilter requires min_days_listed to not exceed " raise OperationalException("AgeFilter requires min_days_listed to not exceed "
"exchange max request size " "exchange max request size "
f"({exchange.ohlcv_candle_limit})") f"({exchange.ohlcv_candle_limit('1d')})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -168,7 +168,7 @@ class IPairList(LoggingMixin, ABC):
# Check if market is active # Check if market is active
market = markets[pair] market = markets[pair]
if not market_is_active(market): if not market_is_active(market):
logger.info(f"Ignoring {pair} from whitelist. Market is not active.") self.log_once(f"Ignoring {pair} from whitelist. Market is not active.", logger.info)
continue continue
if pair not in sanitized_whitelist: if pair not in sanitized_whitelist:
sanitized_whitelist.append(pair) sanitized_whitelist.append(pair)

View File

@ -32,10 +32,10 @@ class RangeStabilityFilter(IPairList):
if self._days < 1: if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > exchange.ohlcv_candle_limit: if self._days > exchange.ohlcv_candle_limit('1d'):
raise OperationalException("RangeStabilityFilter requires lookback_days to not " raise OperationalException("RangeStabilityFilter requires lookback_days to not "
"exceed exchange max request size " "exceed exchange max request size "
f"({exchange.ohlcv_candle_limit})") f"({exchange.ohlcv_candle_limit('1d')})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -59,17 +59,6 @@ class PairListManager():
"""The expanded blacklist (including wildcard expansion)""" """The expanded blacklist (including wildcard expansion)"""
return expand_pairlist(self._blacklist, self._exchange.get_markets().keys()) 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 @property
def name_list(self) -> List[str]: def name_list(self) -> List[str]:
"""Get list of loaded Pairlist Handler names""" """Get list of loaded Pairlist Handler names"""
@ -153,10 +142,8 @@ class PairListManager():
:return: pairlist - whitelisted pairs :return: pairlist - whitelisted pairs
""" """
try: try:
if keep_invalid:
whitelist = self.expanded_whitelist_keep_invalid whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid)
else:
whitelist = self.expanded_whitelist
except ValueError as err: except ValueError as err:
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
return [] return []

View File

@ -58,13 +58,13 @@ class StoplossGuard(IProtection):
SellType.STOPLOSS_ON_EXCHANGE.value) SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit < 0)] and trade.close_profit < 0)]
if len(trades) > self._trade_limit: if len(trades) < self._trade_limit:
self.log_once(f"Trading stopped due to {self._trade_limit} " return False, None, None
f"stoplosses within {self._lookback_period} minutes.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason()
return False, None, None self.log_once(f"Trading stopped due to {self._trade_limit} "
f"stoplosses within {self._lookback_period} minutes.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason()
def global_stop(self, date_now: datetime) -> ProtectionReturn: def global_stop(self, date_now: datetime) -> ProtectionReturn:
""" """

View File

@ -68,6 +68,7 @@ class StrategyResolver(IResolver):
("trailing_stop_positive", None, None), ("trailing_stop_positive", None, None),
("trailing_stop_positive_offset", 0.0, None), ("trailing_stop_positive_offset", 0.0, None),
("trailing_only_offset_is_reached", None, None), ("trailing_only_offset_is_reached", None, None),
("use_custom_stoploss", None, None),
("process_only_new_candles", None, None), ("process_only_new_candles", None, None),
("order_types", None, None), ("order_types", None, None),
("order_time_in_force", None, None), ("order_time_in_force", None, None),

View File

@ -113,7 +113,7 @@ class Daily(BaseModel):
class ShowConfig(BaseModel): class ShowConfig(BaseModel):
dry_run: str dry_run: bool
stake_currency: str stake_currency: str
stake_amount: Union[float, str] stake_amount: Union[float, str]
max_open_trades: int max_open_trades: int
@ -123,6 +123,7 @@ class ShowConfig(BaseModel):
trailing_stop_positive: Optional[float] trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float] trailing_stop_positive_offset: Optional[float]
trailing_only_offset_is_reached: Optional[bool] trailing_only_offset_is_reached: Optional[bool]
use_custom_stoploss: Optional[bool]
timeframe: str timeframe: str
timeframe_ms: int timeframe_ms: int
timeframe_min: int timeframe_min: int

View File

@ -167,7 +167,7 @@ def reload_config(rpc: RPC = Depends(get_rpc)):
@router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) @router.get('/pair_candles', response_model=PairHistory, tags=['candle data'])
def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)): def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Depends(get_rpc)):
return rpc._rpc_analysed_dataframe(pair, timeframe, limit) return rpc._rpc_analysed_dataframe(pair, timeframe, limit)

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Freqtrade UI</title>
<style>
body {
background-color: #3c3c3c;
color: #dedede;
text-align: center;
}
.main-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
h1 {
margin-top: 10rem;
}
</style>
</head>
<body>
<div class="main-container">
<h1>Freqtrade UI not installed.</h1>
<p>Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.</p>
<p>You can then refresh this page and you should see the UI.</p>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -0,0 +1,31 @@
from pathlib import Path
from fastapi import APIRouter
from fastapi.exceptions import HTTPException
from starlette.responses import FileResponse
router_ui = APIRouter()
@router_ui.get('/favicon.ico', include_in_schema=False)
async def favicon():
return FileResponse(Path(__file__).parent / 'ui/favicon.ico')
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
async def index_html(rest_of_path: str):
"""
Emulate path fallback to index.html.
"""
if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
raise HTTPException(status_code=404, detail="Not Found")
uibase = Path(__file__).parent / 'ui/installed/'
if (uibase / rest_of_path).is_file():
return FileResponse(str(uibase / rest_of_path))
index_file = uibase / 'index.html'
if not index_file.is_file():
return FileResponse(str(uibase.parent / 'fallback_file.html'))
# Fall back to index.html, as indicated by vue router docs
return FileResponse(str(index_file))

View File

@ -2,6 +2,7 @@ import logging
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Any, Dict from typing import Any, Dict
import rapidjson
import uvicorn import uvicorn
from fastapi import Depends, FastAPI from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -14,6 +15,17 @@ from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FTJSONResponse(JSONResponse):
media_type = "application/json"
def render(self, content: Any) -> bytes:
"""
Use rapidjson for responses
Handles NaN and Inf / -Inf in a javascript way by default.
"""
return rapidjson.dumps(content).encode("utf-8")
class ApiServer(RPCHandler): class ApiServer(RPCHandler):
_rpc: RPC _rpc: RPC
@ -32,6 +44,7 @@ class ApiServer(RPCHandler):
self.app = FastAPI(title="Freqtrade API", self.app = FastAPI(title="Freqtrade API",
docs_url='/docs' if api_config.get('enable_openapi', False) else None, docs_url='/docs' if api_config.get('enable_openapi', False) else None,
redoc_url=None, redoc_url=None,
default_response_class=FTJSONResponse,
) )
self.configure_app(self.app, self._config) self.configure_app(self.app, self._config)
@ -57,12 +70,16 @@ class ApiServer(RPCHandler):
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
from freqtrade.rpc.api_server.api_v1 import router as api_v1 from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
from freqtrade.rpc.api_server.web_ui import router_ui
app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1_public, prefix="/api/v1")
app.include_router(api_v1, prefix="/api/v1", app.include_router(api_v1, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)], dependencies=[Depends(http_basic_or_jwt_token)],
) )
app.include_router(router_login, prefix="/api/v1", tags=["auth"]) app.include_router(router_login, prefix="/api/v1", tags=["auth"])
# UI Router MUST be last!
app.include_router(router_ui, prefix='')
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View File

@ -9,7 +9,7 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
import arrow import arrow
from numpy import NAN, int64, mean from numpy import NAN, inf, int64, mean
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange
@ -129,6 +129,7 @@ class RPC:
'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'use_custom_stoploss': config.get('use_custom_stoploss'),
'bot_name': config.get('bot_name', 'freqtrade'), 'bot_name': config.get('bot_name', 'freqtrade'),
'timeframe': config.get('timeframe'), 'timeframe': config.get('timeframe'),
'timeframe_ms': timeframe_to_msecs(config['timeframe'] 'timeframe_ms': timeframe_to_msecs(config['timeframe']
@ -377,7 +378,7 @@ class RPC:
# Prepare data to display # Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0 profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0)
profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_fiat = self._fiat_converter.convert_amount(
@ -387,7 +388,7 @@ class RPC:
) if self._fiat_converter else 0 ) if self._fiat_converter else 0
profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_coin_sum = round(sum(profit_all_coin), 8)
profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0 profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0)
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
profit_all_fiat = self._fiat_converter.convert_amount( profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum, profit_all_coin_sum,
@ -450,7 +451,7 @@ class RPC:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate = tickers.get(pair, {}).get('bid', None) rate = tickers.get(pair, {}).get('bid', None)
if rate: if rate:
if pair.startswith(stake_currency): if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate rate = 1.0 / rate
est_stake = rate * balance.total est_stake = rate * balance.total
except (ExchangeError): except (ExchangeError):
@ -589,7 +590,8 @@ class RPC:
raise RPCException(f'position for {pair} already open - id: {trade.id}') raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount # gen stake amount
stakeamount = self._freqtrade.get_trade_stake_amount(pair) stakeamount = self._freqtrade.wallets.get_trade_stake_amount(
pair, self._freqtrade.get_free_open_trades())
# execute buy # execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price): if self._freqtrade.execute_buy(pair, stakeamount, price):
@ -745,6 +747,7 @@ class RPC:
sell_mask = (dataframe['sell'] == 1) sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum()) sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
dataframe = dataframe.replace([inf, -inf], NAN)
dataframe = dataframe.replace({NAN: None}) dataframe = dataframe.replace({NAN: None})
res = { res = {
@ -773,7 +776,8 @@ class RPC:
}) })
return res return res
def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: def _rpc_analysed_dataframe(self, pair: str, timeframe: str,
limit: Optional[int]) -> Dict[str, Any]:
_data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
pair, timeframe) pair, timeframe)

View File

@ -18,6 +18,7 @@ from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
@ -189,14 +190,14 @@ class Telegram(RPCHandler):
else: else:
msg['stake_amount_fiat'] = 0 msg['stake_amount_fiat'] = 0
message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n"
"*Amount:* `{amount:.8f}`\n" f"*Amount:* `{msg['amount']:.8f}`\n"
"*Open Rate:* `{limit:.8f}`\n" f"*Open Rate:* `{msg['limit']:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
"*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
if msg.get('fiat_currency', None): if msg.get('fiat_currency', None):
message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`" message += ")`"
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
@ -365,7 +366,7 @@ class Telegram(RPCHandler):
) )
stats_tab = tabulate( stats_tab = tabulate(
[[day['date'], [[day['date'],
f"{day['abs_profit']:.8f} {stats['stake_currency']}", f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{day['trade_count']} trades"] for day in stats['data']], f"{day['trade_count']} trades"] for day in stats['data']],
headers=[ headers=[
@ -415,18 +416,18 @@ class Telegram(RPCHandler):
# Message to display # Message to display
if stats['closed_trade_count'] > 0: if stats['closed_trade_count'] > 0:
markdown_msg = ("*ROI:* Closed trades\n" markdown_msg = ("*ROI:* Closed trades\n"
f"∙ `{profit_closed_coin:.8f} {stake_cur} " f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
f"({profit_closed_percent_mean:.2f}%) " f"({profit_closed_percent_mean:.2f}%) "
f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
else: else:
markdown_msg = "`No closed trade` \n" markdown_msg = "`No closed trade` \n"
markdown_msg += (f"*ROI:* All trades\n" markdown_msg += (f"*ROI:* All trades\n"
f"∙ `{profit_all_coin:.8f} {stake_cur} " f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
f"({profit_all_percent_mean:.2f}%) " f"({profit_all_percent_mean:.2f}%) "
f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
f"*Total Trade Count:* `{trade_count}`\n" f"*Total Trade Count:* `{trade_count}`\n"
f"*First Trade opened:* `{first_trade_date}`\n" f"*First Trade opened:* `{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}\n`" f"*Latest Trade opened:* `{latest_trade_date}\n`"
@ -494,15 +495,17 @@ class Telegram(RPCHandler):
"Starting capital: " "Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
) )
for currency in result['currencies']: for curr in result['currencies']:
if currency['est_stake'] > 0.0001: if curr['est_stake'] > 0.0001:
curr_output = ("*{currency}:*\n" curr_output = (
"\t`Available: {free: .8f}`\n" f"*{curr['currency']}:*\n"
"\t`Balance: {balance: .8f}`\n" f"\t`Available: {curr['free']:.8f}`\n"
"\t`Pending: {used: .8f}`\n" f"\t`Balance: {curr['balance']:.8f}`\n"
"\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency) f"\t`Pending: {curr['used']:.8f}`\n"
f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else: else:
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) curr_output = f"*{curr['currency']}:* not showing <1$ amount \n"
# Handle overflowing messsage length # Handle overflowing messsage length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@ -512,8 +515,9 @@ class Telegram(RPCHandler):
output += curr_output output += curr_output
output += ("\n*Estimated Value*:\n" output += ("\n*Estimated Value*:\n"
"\t`{stake}: {total: .8f}`\n" f"\t`{result['stake']}: {result['total']: .8f}`\n"
"\t`{symbol}: {value: .2f}`\n").format(**result) f"\t`{result['symbol']}: "
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
self._send_msg(output) self._send_msg(output)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@ -910,7 +914,7 @@ class Telegram(RPCHandler):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
reply_markup = ReplyKeyboardMarkup(self._keyboard) reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
try: try:
try: try:
self._updater.bot.send_message( self._updater.bot.send_message(

View File

@ -530,8 +530,8 @@ class IStrategy(ABC):
current_time=date)) current_time=date))
if (ask_strategy.get('sell_profit_only', False) if (ask_strategy.get('sell_profit_only', False)
and trade.calc_profit(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)): and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
# Negative profits and sell_profit_only - ignore sell signal # sell_profit_only and profit doesn't reach the offset - ignore sell signal
sell_signal = False sell_signal = False
else: else:
sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True) sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)

View File

@ -5,7 +5,7 @@ import numpy as np # noqa
import pandas as pd # noqa import pandas as pd # noqa
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import IStrategy
# -------------------------------- # --------------------------------
# Add your lib to import here # Add your lib to import here

View File

@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from math import exp from math import exp
from typing import Dict
from pandas import DataFrame from pandas import DataFrame
@ -35,12 +36,13 @@ class SampleHyperOptLoss(IHyperOptLoss):
@staticmethod @staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int, def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float: *args, **kwargs) -> float:
""" """
Objective function, returns smaller number for better results Objective function, returns smaller number for better results
""" """
total_profit = results.profit_percent.sum() total_profit = results['profit_ratio'].sum()
trade_duration = results.trade_duration.mean() trade_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8) trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT) profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)

View File

@ -17,7 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
class SampleStrategy(IStrategy): class SampleStrategy(IStrategy):
""" """
This is a sample strategy to inspire you. This is a sample strategy to inspire you.
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md More information in https://www.freqtrade.io/en/latest/strategy-customization/
You can: You can:
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies

View File

@ -40,7 +40,7 @@
"# Location of the data\n", "# Location of the data\n",
"data_location = Path(config['user_data_dir'], 'data', 'binance')\n", "data_location = Path(config['user_data_dir'], 'data', 'binance')\n",
"# Pair to analyze - Only use one pair here\n", "# Pair to analyze - Only use one pair here\n",
"pair = \"BTC_USDT\"" "pair = \"BTC/USDT\""
] ]
}, },
{ {
@ -54,7 +54,9 @@
"\n", "\n",
"candles = load_pair_history(datadir=data_location,\n", "candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n", " timeframe=config[\"timeframe\"],\n",
" pair=pair)\n", " pair=pair,\n",
" data_format = \"hdf5\",\n",
" )\n",
"\n", "\n",
"# Confirm success\n", "# Confirm success\n",
"print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n", "print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n",

View File

@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple
import arrow import arrow
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import DependencyException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -118,3 +120,79 @@ class Wallets:
def get_all_balances(self) -> Dict[str, Any]: def get_all_balances(self) -> Dict[str, Any]:
return self._wallets return self._wallets
def _get_available_stake_amount(self) -> float:
"""
Return the total currently available balance in stake currency,
respecting tradable_balance_ratio.
Calculated as
(<open_trade stakes> + free amount ) * tradable_balance_ratio - <open_trade stakes>
"""
val_tied_up = Trade.total_open_trades_stakes()
# Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade.
# (tied up + current free) * ratio) - tied up
available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) *
self._config['tradable_balance_ratio']) - val_tied_up
return available_amount
def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float:
"""
Calculate stake amount for "unlimited" stake amount
:return: 0 if max number of trades reached, else stake_amount to use.
"""
if not free_open_trades:
return 0
available_amount = self._get_available_stake_amount()
return available_amount / free_open_trades
def _check_available_stake_amount(self, stake_amount: float) -> float:
"""
Check if stake amount can be fulfilled with the available balance
for the stake currency
:return: float: Stake amount
"""
available_amount = self._get_available_stake_amount()
if self._config['amend_last_stake_amount']:
# Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio
# Otherwise the remaining amount is too low to trade.
if available_amount > (stake_amount * self._config['last_stake_amount_min_ratio']):
stake_amount = min(stake_amount, available_amount)
else:
stake_amount = 0
if available_amount < stake_amount:
raise DependencyException(
f"Available balance ({available_amount} {self._config['stake_currency']}) is "
f"lower than stake amount ({stake_amount} {self._config['stake_currency']})"
)
return stake_amount
def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float:
"""
Calculate stake amount for the trade
:return: float: Stake amount
:raise: DependencyException if the available stake amount is too low
"""
stake_amount: float
# Ensure wallets are uptodate.
self.update()
if edge:
stake_amount = edge.stake_amount(
pair,
self.get_free(self._config['stake_currency']),
self.get_total(self._config['stake_currency']),
Trade.total_open_trades_stakes()
)
else:
stake_amount = self._config['stake_amount']
if stake_amount == UNLIMITED_STAKE_AMOUNT:
stake_amount = self._calculate_unlimited_stake_amount(free_open_trades)
return self._check_available_stake_amount(stake_amount)

View File

@ -8,29 +8,30 @@ nav:
- Freqtrade Basics: bot-basics.md - Freqtrade Basics: bot-basics.md
- Configuration: configuration.md - Configuration: configuration.md
- Strategy Customization: strategy-customization.md - Strategy Customization: strategy-customization.md
- Plugins: plugins.md
- Stoploss: stoploss.md - Stoploss: stoploss.md
- Start the bot: bot-usage.md - Start the bot: bot-usage.md
- Control the bot: - Control the bot:
- Telegram: telegram-usage.md - Telegram: telegram-usage.md
- Web Hook: webhook-config.md - Web Hook: webhook-config.md
- REST API: rest-api.md - REST API & FreqUI: rest-api.md
- Data Downloading: data-download.md - Data Downloading: data-download.md
- Backtesting: backtesting.md - Backtesting: backtesting.md
- Hyperopt: hyperopt.md - Hyperopt: hyperopt.md
- Edge Positioning: edge.md - Utility Sub-commands: utils.md
- Plugins: plugins.md
- Utility Subcommands: utils.md
- FAQ: faq.md
- Data Analysis: - Data Analysis:
- Jupyter Notebooks: data-analysis.md - Jupyter Notebooks: data-analysis.md
- Strategy analysis: strategy_analysis_example.md - Strategy analysis: strategy_analysis_example.md
- Plotting: plotting.md - Plotting: plotting.md
- SQL Cheatsheet: sql_cheatsheet.md
- Exchange-specific Notes: exchanges.md - Exchange-specific Notes: exchanges.md
- Advanced Post-installation Tasks: advanced-setup.md - Advanced Topics:
- Advanced Strategy: strategy-advanced.md - Advanced Post-installation Tasks: advanced-setup.md
- Advanced Hyperopt: advanced-hyperopt.md - Edge Positioning: edge.md
- Sandbox Testing: sandbox-testing.md - Advanced Strategy: strategy-advanced.md
- Advanced Hyperopt: advanced-hyperopt.md
- Sandbox Testing: sandbox-testing.md
- FAQ: faq.md
- SQL Cheat-sheet: sql_cheatsheet.md
- Updating Freqtrade: updating.md - Updating Freqtrade: updating.md
- Deprecated Features: deprecated.md - Deprecated Features: deprecated.md
- Contributors Guide: developer.md - Contributors Guide: developer.md

View File

@ -8,7 +8,7 @@ flake8==3.8.4
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.2.1 flake8-tidy-imports==4.2.1
mypy==0.790 mypy==0.790
pytest==6.2.1 pytest==6.2.2
pytest-asyncio==0.14.0 pytest-asyncio==0.14.0
pytest-cov==2.11.1 pytest-cov==2.11.1
pytest-mock==3.5.1 pytest-mock==3.5.1

View File

@ -2,9 +2,9 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.6.0 scipy==1.6.1
scikit-learn==0.24.1 scikit-learn==0.24.1
scikit-optimize==0.8.1 scikit-optimize==0.8.1
filelock==3.0.12 filelock==3.0.12
joblib==1.0.0 joblib==1.0.1
progressbar2==3.53.1 progressbar2==3.53.1

View File

@ -1,25 +1,27 @@
numpy==1.19.5 numpy==1.20.1
pandas==1.2.1 pandas==1.2.2
ccxt==1.40.99 ccxt==1.42.19
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.6
aiohttp==3.7.3 aiohttp==3.7.3
SQLAlchemy==1.3.22 SQLAlchemy==1.3.23
python-telegram-bot==13.1 python-telegram-bot==13.3
arrow==0.17.0 arrow==0.17.0
cachetools==4.2.1 cachetools==4.2.1
requests==2.25.1 requests==2.25.1
urllib3==1.26.2 urllib3==1.26.3
wrapt==1.12.1 wrapt==1.12.1
jsonschema==3.2.0 jsonschema==3.2.0
TA-Lib==0.4.19 TA-Lib==0.4.19
tabulate==0.8.7 tabulate==0.8.9
pycoingecko==1.4.0 pycoingecko==1.4.0
jinja2==2.11.2 jinja2==2.11.3
tables==3.6.1 tables==3.6.1
blosc==1.10.2 blosc==1.10.2
# find first, C search in arrays # find first, C search in arrays
py_find_1st==1.1.4 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.0 python-rapidjson==1.0
@ -29,11 +31,12 @@ sdnotify==0.3.2
# API Server # API Server
fastapi==0.63.0 fastapi==0.63.0
uvicorn==0.13.3 uvicorn==0.13.4
pyjwt==2.0.1 pyjwt==2.0.1
aiofiles==0.6.0
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.4 colorama==0.4.4
# Building config files interactively # Building config files interactively
questionary==1.9.0 questionary==1.9.0
prompt-toolkit==3.0.14 prompt-toolkit==3.0.16

View File

@ -379,7 +379,7 @@ def main(args):
print_commands() print_commands()
return return
print(getattr(client, command)(*args["command_arguments"])) print(json.dumps(getattr(client, command)(*args["command_arguments"])))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -19,7 +19,7 @@ if readme_file.is_file():
readme_long = (Path(__file__).parent / "README.md").read_text() readme_long = (Path(__file__).parent / "README.md").read_text()
# Requirements used for submodules # Requirements used for submodules
api = ['flask', 'flask-jwt-extended', 'flask-cors'] api = ['fastapi', 'uvicorn', 'pyjwt', 'aiofiles']
plot = ['plotly>=4.0'] plot = ['plotly>=4.0']
hyperopt = [ hyperopt = [
'scipy', 'scipy',

View File

@ -1,16 +1,20 @@
import re import re
from io import BytesIO
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
from zipfile import ZipFile
import arrow import arrow
import pytest import pytest
from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data,
start_hyperopt_list, start_hyperopt_show, start_list_data, start_hyperopt_list, start_hyperopt_show, start_install_ui,
start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_data, start_list_exchanges, start_list_hyperopts,
start_list_strategies, start_list_timeframes, start_new_hyperopt, start_list_markets, start_list_strategies, start_list_timeframes,
start_new_strategy, start_show_trades, start_test_pairlist, start_new_hyperopt, start_new_strategy, start_show_trades,
start_trading) start_test_pairlist, start_trading)
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
get_ui_download_url, read_ui_version)
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -546,7 +550,7 @@ def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog):
start_new_hyperopt(get_args(args)) start_new_hyperopt(get_args(args))
def test_start_new_hyperopt_no_arg(mocker, caplog): def test_start_new_hyperopt_no_arg(mocker):
args = [ args = [
"new-hyperopt", "new-hyperopt",
] ]
@ -555,6 +559,107 @@ def test_start_new_hyperopt_no_arg(mocker, caplog):
start_new_hyperopt(get_args(args)) start_new_hyperopt(get_args(args))
def test_start_install_ui(mocker):
clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir')
get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url',
return_value=('https://example.com/whatever', '0.0.1'))
download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui')
mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None)
args = [
"install-ui",
]
start_install_ui(get_args(args))
assert clean_mock.call_count == 1
assert get_url_mock.call_count == 1
assert download_mock.call_count == 1
clean_mock.reset_mock()
get_url_mock.reset_mock()
download_mock.reset_mock()
args = [
"install-ui",
"--erase",
]
start_install_ui(get_args(args))
assert clean_mock.call_count == 1
assert get_url_mock.call_count == 1
assert download_mock.call_count == 0
def test_clean_ui_subdir(mocker, tmpdir, caplog):
mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir",
side_effect=[True, True])
mocker.patch("freqtrade.commands.deploy_commands.Path.is_file",
side_effect=[False, True])
rd_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.rmdir")
ul_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.unlink")
mocker.patch("freqtrade.commands.deploy_commands.Path.glob",
return_value=[Path('test1'), Path('test2'), Path('.gitkeep')])
folder = Path(tmpdir) / "uitests"
clean_ui_subdir(folder)
assert log_has("Removing UI directory content.", caplog)
assert rd_mock.call_count == 1
assert ul_mock.call_count == 1
def test_download_and_install_ui(mocker, tmpdir):
# Create zipfile
requests_mock = MagicMock()
file_like_object = BytesIO()
with ZipFile(file_like_object, mode='w') as zipfile:
for file in ('test1.txt', 'hello/', 'test2.txt'):
zipfile.writestr(file, file)
file_like_object.seek(0)
requests_mock.content = file_like_object.read()
mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock)
mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir",
side_effect=[True, False])
wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes")
folder = Path(tmpdir) / "uitests_dl"
folder.mkdir(exist_ok=True)
assert read_ui_version(folder) is None
download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22')
assert wb_mock.call_count == 2
assert read_ui_version(folder) == '22'
def test_get_ui_download_url(mocker):
response = MagicMock()
response.json = MagicMock(
side_effect=[[{'assets_url': 'http://whatever.json', 'name': '0.0.1'}],
[{'browser_download_url': 'http://download.zip'}]])
get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get",
return_value=response)
x, last_version = get_ui_download_url()
assert get_mock.call_count == 2
assert last_version == '0.0.1'
assert x == 'http://download.zip'
def test_get_ui_download_url_direct(mocker):
response = MagicMock()
response.json = MagicMock(
side_effect=[[{
'assets_url': 'http://whatever.json',
'name': '0.0.1',
'assets': [{'browser_download_url': 'http://download11.zip'}]}]])
get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get",
return_value=response)
x, last_version = get_ui_download_url()
assert get_mock.call_count == 1
assert last_version == '0.0.1'
assert x == 'http://download11.zip'
def test_download_data_keyboardInterrupt(mocker, caplog, markets): def test_download_data_keyboardInterrupt(mocker, caplog, markets):
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
MagicMock(side_effect=KeyboardInterrupt)) MagicMock(side_effect=KeyboardInterrupt))
@ -822,6 +927,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
] ]
pargs = get_args(args) pargs = get_args(args)
pargs['config'] = None pargs['config'] = None
@ -835,6 +941,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
"hyperopt-list", "hyperopt-list",
"--best", "--best",
"--no-details", "--no-details",
"--no-color",
] ]
pargs = get_args(args) pargs = get_args(args)
pargs['config'] = None pargs['config'] = None
@ -849,6 +956,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
"hyperopt-list", "hyperopt-list",
"--profitable", "--profitable",
"--no-details", "--no-details",
"--no-color",
] ]
pargs = get_args(args) pargs = get_args(args)
pargs['config'] = None pargs['config'] = None
@ -862,6 +970,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--profitable", "--profitable",
"--no-color",
] ]
pargs = get_args(args) pargs = get_args(args)
pargs['config'] = None pargs['config'] = None
@ -891,6 +1000,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
"hyperopt-list", "hyperopt-list",
"--profitable", "--profitable",
"--no-details", "--no-details",
"--no-color",
"--max-trades", "20", "--max-trades", "20",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -906,6 +1016,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
"hyperopt-list", "hyperopt-list",
"--profitable", "--profitable",
"--no-details", "--no-details",
"--no-color",
"--min-avg-profit", "0.11", "--min-avg-profit", "0.11",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -920,6 +1031,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
"--max-avg-profit", "0.10", "--max-avg-profit", "0.10",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -934,6 +1046,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
"--min-total-profit", "0.4", "--min-total-profit", "0.4",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -948,6 +1061,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
"--max-total-profit", "0.4", "--max-total-profit", "0.4",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -962,6 +1076,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
"--min-objective", "0.1", "--min-objective", "0.1",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -991,6 +1106,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
"hyperopt-list", "hyperopt-list",
"--profitable", "--profitable",
"--no-details", "--no-details",
"--no-color",
"--min-avg-time", "2000", "--min-avg-time", "2000",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -1005,6 +1121,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
"--max-avg-time", "1500", "--max-avg-time", "1500",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -1019,6 +1136,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
args = [ args = [
"hyperopt-list", "hyperopt-list",
"--no-details", "--no-details",
"--no-color",
"--export-csv", "test_file.csv", "--export-csv", "test_file.csv",
] ]
pargs = get_args(args) pargs = get_args(args)
@ -1168,7 +1286,7 @@ def test_start_list_data(testdatadir, capsys):
pargs['config'] = None pargs['config'] = None
start_list_data(pargs) start_list_data(pargs)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Found 16 pair / timeframe combinations." in captured.out assert "Found 17 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe |\n" in captured.out assert "\n| Pair | Timeframe |\n" in captured.out
assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out

View File

@ -73,7 +73,6 @@ def patched_configuration_load_config_file(mocker, config) -> None:
def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: 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_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_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())

View File

@ -7,14 +7,14 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD,
analyze_trade_parallelism, calculate_csum,
calculate_market_change, calculate_max_drawdown, calculate_market_change, calculate_max_drawdown,
combine_dataframes_with_mean, create_cum_profit, combine_dataframes_with_mean, create_cum_profit,
extract_trades_of_period, get_latest_backtest_filename, extract_trades_of_period, get_latest_backtest_filename,
get_latest_hyperopt_file, load_backtest_data, load_trades, get_latest_hyperopt_file, load_backtest_data, load_trades,
load_trades_from_db) load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.history import load_data, load_pair_history
from freqtrade.optimize.backtesting import BacktestResult
from tests.conftest import create_mock_trades from tests.conftest import create_mock_trades
from tests.conftest_trades import MOCK_TRADE_COUNT from tests.conftest_trades import MOCK_TRADE_COUNT
@ -55,7 +55,7 @@ def test_load_backtest_data_old_format(testdatadir):
filename = testdatadir / "backtest-result_test.json" filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame) 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 assert len(bt_data) == 179
# Test loading from string (must yield same result) # Test loading from string (must yield same result)
@ -71,7 +71,7 @@ def test_load_backtest_data_new_format(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame) assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"]) assert set(bt_data.columns) == set(BT_DATA_COLUMNS_MID)
assert len(bt_data) == 179 assert len(bt_data) == 179
# Test loading from string (must yield same result) # Test loading from string (must yield same result)
@ -95,7 +95,7 @@ def test_load_backtest_data_multi(testdatadir):
for strategy in ('DefaultStrategy', 'TestStrategy'): for strategy in ('DefaultStrategy', 'TestStrategy'):
bt_data = load_backtest_data(filename, strategy=strategy) bt_data = load_backtest_data(filename, strategy=strategy)
assert isinstance(bt_data, DataFrame) assert isinstance(bt_data, DataFrame)
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 assert len(bt_data) == 179
# Test loading from string (must yield same result) # Test loading from string (must yield same result)
@ -122,7 +122,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
assert isinstance(trades, DataFrame) assert isinstance(trades, DataFrame)
assert "pair" in trades.columns assert "pair" in trades.columns
assert "open_date" 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: for col in BT_DATA_COLUMNS:
if col not in ['index', 'open_at_end']: if col not in ['index', 'open_at_end']:
@ -143,7 +143,7 @@ def test_extract_trades_of_period(testdatadir):
trades = DataFrame( trades = DataFrame(
{'pair': [pair, pair, pair, pair], {'pair': [pair, pair, pair, pair],
'profit_percent': [0.0, 0.1, -0.2, -0.5], 'profit_ratio': [0.0, 0.1, -0.2, -0.5],
'profit_abs': [0.0, 1, -2, -5], 'profit_abs': [0.0, 1, -2, -5],
'open_date': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, 'open_date': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime,
Arrow(2017, 11, 14, 9, 41, 0).datetime, Arrow(2017, 11, 14, 9, 41, 0).datetime,
@ -285,6 +285,20 @@ def test_calculate_max_drawdown(testdatadir):
drawdown, h, low = calculate_max_drawdown(DataFrame()) drawdown, h, low = calculate_max_drawdown(DataFrame())
def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data)
assert isinstance(csum_min, float)
assert isinstance(csum_max, float)
assert csum_min < 0.01
assert csum_max > 0.02
with pytest.raises(ValueError, match='Trade dataframe empty.'):
csum_min, csum_max = calculate_csum(DataFrame())
def test_calculate_max_drawdown2(): def test_calculate_max_drawdown2():
values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024, values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
-0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872, -0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,

View File

@ -646,7 +646,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'), ('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'),
('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'), ('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'),
('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'), ('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'),
('UNITTEST/BTC', '8m')} ('UNITTEST/BTC', '8m'), ('NOPAIR/XXX', '4m')}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir) paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir)
assert set(paircombs) == {('UNITTEST/BTC', '8m')} assert set(paircombs) == {('UNITTEST/BTC', '8m')}
@ -672,6 +672,18 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
assert unlinkmock.call_count == 1 assert unlinkmock.call_count == 1
def test_jsondatahandler_ohlcv_load(testdatadir, caplog):
dh = JsonDataHandler(testdatadir)
df = dh.ohlcv_load('XRP/ETH', '5m')
assert len(df) == 711
# Failure case (empty array)
df1 = dh.ohlcv_load('NOPAIR/XXX', '4m')
assert len(df1) == 0
assert log_has("Could not load data for NOPAIR/XXX.", caplog)
assert df.columns.equals(df1.columns)
def test_jsondatahandler_trades_load(testdatadir, caplog): def test_jsondatahandler_trades_load(testdatadir, caplog):
dh = JsonGzDataHandler(testdatadir) dh = JsonGzDataHandler(testdatadir)
logmsg = "Old trades format detected - converting" logmsg = "Old trades format detected - converting"

View File

@ -209,7 +209,7 @@ def test_nonexisting_stoploss(mocker, edge_conf):
assert edge.stoploss('N/O') == -0.1 assert edge.stoploss('N/O') == -0.1
def test_stake_amount(mocker, edge_conf): def test_edge_stake_amount(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf) freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
@ -217,20 +217,33 @@ def test_stake_amount(mocker, edge_conf):
'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
} }
)) ))
free = 100 assert edge._capital_ratio == 0.5
total = 100 assert edge.stake_amount('E/F', free_capital=100, total_capital=100,
in_trade = 25 capital_in_trade=25) == 31.25
assert edge.stake_amount('E/F', free, total, in_trade) == 31.25
free = 20 assert edge.stake_amount('E/F', free_capital=20, total_capital=100,
total = 100 capital_in_trade=25) == 20
in_trade = 25
assert edge.stake_amount('E/F', free, total, in_trade) == 20
free = 0 assert edge.stake_amount('E/F', free_capital=0, total_capital=100,
total = 100 capital_in_trade=25) == 0
in_trade = 25
assert edge.stake_amount('E/F', free, total, in_trade) == 0 # Test with increased allowed_risk
# Result should be no more than allowed capital
edge._allowed_risk = 0.4
edge._capital_ratio = 0.5
assert edge.stake_amount('E/F', free_capital=100, total_capital=100,
capital_in_trade=25) == 62.5
assert edge.stake_amount('E/F', free_capital=100, total_capital=100,
capital_in_trade=0) == 50
edge._capital_ratio = 1
# Full capital is available
assert edge.stake_amount('E/F', free_capital=100, total_capital=100,
capital_in_trade=0) == 100
# Full capital is available
assert edge.stake_amount('E/F', free_capital=0, total_capital=100,
capital_in_trade=0) == 0
def test_nonexisting_stake_amount(mocker, edge_conf): def test_nonexisting_stake_amount(mocker, edge_conf):

View File

@ -5,10 +5,12 @@ However, these tests should give a good idea to determine if a new exchange is
suitable to run with freqtrade. suitable to run with freqtrade.
""" """
from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
import pytest import pytest
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf from tests.conftest import get_default_conf
@ -18,7 +20,7 @@ EXCHANGES = {
'bittrex': { 'bittrex': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
'hasQuoteVolume': False, 'hasQuoteVolume': False,
'timeframe': '5m', 'timeframe': '1h',
}, },
'binance': { 'binance': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
@ -120,7 +122,12 @@ class TestCCXTExchange():
ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) ohlcv = exchange.refresh_latest_ohlcv([pair_tf])
assert isinstance(ohlcv, dict) assert isinstance(ohlcv, dict)
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
assert len(exchange.klines(pair_tf)) > 200 # assert len(exchange.klines(pair_tf)) > 200
# Assume 90% uptime ...
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(timeframe) * 0.90
# Check if last-timeframe is within the last 2 intervals
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
# TODO: tests fetch_trades (?) # TODO: tests fetch_trades (?)

View File

@ -305,6 +305,136 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio
assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
exchange = get_patched_exchange(mocker, default_conf, id="binance")
stoploss = -0.05
markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}}
# no pair found
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
with pytest.raises(ValueError, match=r'.*get market information.*'):
exchange.get_min_pair_stake_amount('BNB/BTC', 1, stoploss)
# no 'limits' section
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result is None
# empty 'limits' section
markets["ETH/BTC"]["limits"] = {}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result is None
# no cost Min
markets["ETH/BTC"]["limits"] = {
'cost': {"min": None},
'amount': {}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result is None
# no amount Min
markets["ETH/BTC"]["limits"] = {
'cost': {},
'amount': {"min": None}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result is None
# empty 'cost'/'amount' section
markets["ETH/BTC"]["limits"] = {
'cost': {},
'amount': {}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result is None
# min cost is set
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 2},
'amount': {}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result == 2 / 0.9
# min amount is set
markets["ETH/BTC"]["limits"] = {
'cost': {},
'amount': {'min': 2}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == 2 * 2 / 0.9
# min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 2},
'amount': {'min': 2}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == max(2, 2 * 2) / 0.9
# min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 8},
'amount': {'min': 2}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == max(8, 2 * 2) / 0.9
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
exchange = get_patched_exchange(mocker, default_conf, id="binance")
stoploss = -0.05
markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}}
# Real Binance data
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 0.0001},
'amount': {'min': 0.001}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8)
def test_set_sandbox(default_conf, mocker): def test_set_sandbox(default_conf, mocker):
""" """
Test working scenario Test working scenario
@ -373,28 +503,25 @@ def test__load_markets(default_conf, mocker, caplog):
expected_return = {'ETH/BTC': 'available'} expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=expected_return) 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'] 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 assert ex.markets == expected_return
def test_reload_markets(default_conf, mocker, caplog): def test_reload_markets(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}} initial_markets = {'ETH/BTC': {}}
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
def load_markets(*args, **kwargs):
exchange._api.markets = updated_markets
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = load_markets api_mock.load_markets = MagicMock(return_value=initial_markets)
type(api_mock).markets = initial_markets
default_conf['exchange']['markets_refresh_interval'] = 10 default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
mock_markets=False) mock_markets=False)
exchange._load_async_markets = MagicMock() exchange._load_async_markets = MagicMock()
exchange._last_markets_refresh = arrow.utcnow().int_timestamp exchange._last_markets_refresh = arrow.utcnow().int_timestamp
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
@ -403,6 +530,7 @@ def test_reload_markets(default_conf, mocker, caplog):
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0 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 # more than 10 minutes have passed, reload is executed
exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60
exchange.reload_markets() exchange.reload_markets()
@ -429,7 +557,7 @@ def test_reload_markets_exception(default_conf, mocker, caplog):
def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
default_conf['stake_currency'] = stake_currency default_conf['stake_currency'] = stake_currency
api_mock = MagicMock() 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'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
}) })
@ -443,7 +571,7 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog):
def test_validate_stake_currency_error(default_conf, mocker, caplog): def test_validate_stake_currency_error(default_conf, mocker, caplog):
default_conf['stake_currency'] = 'XRP' default_conf['stake_currency'] = 'XRP'
api_mock = MagicMock() 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'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
}) })
@ -489,7 +617,7 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={ type(api_mock).load_markets = MagicMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'ETH/BTC': {'quote': 'BTC'},
'LTC/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'},
@ -540,7 +668,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
def test_validate_pairs_restricted(default_conf, mocker, caplog): def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock() 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'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC', 'info': {'IsRestricted': True}}, 'XRP/BTC': {'quote': 'BTC', 'info': {'IsRestricted': True}},
'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ... 'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ...
@ -558,7 +686,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
api_mock = MagicMock() 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'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'},
@ -574,7 +702,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog):
api_mock = MagicMock() api_mock = MagicMock()
default_conf['stake_currency'] = '' 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'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'},
@ -585,12 +713,13 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf) Exchange(default_conf)
assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD') default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD')
api_mock = MagicMock() 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'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'USDT'}, 'HELLO-WORLD': {'quote': 'USDT'},
@ -1288,7 +1417,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8
ret = exchange.get_historic_ohlcv(pair, "5m", int(( ret = exchange.get_historic_ohlcv(pair, "5m", int((
arrow.utcnow().int_timestamp - since) * 1000)) arrow.utcnow().int_timestamp - since) * 1000))
@ -1344,7 +1473,7 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name):
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8
ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int(( ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int((
arrow.utcnow().int_timestamp - since) * 1000)) arrow.utcnow().int_timestamp - since) * 1000))
@ -1943,9 +2072,9 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap
def test_cancel_order(default_conf, mocker, exchange_name): def test_cancel_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False default_conf['dry_run'] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123) api_mock.cancel_order = MagicMock(return_value={'id': '123'})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == {'id': '123'}
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
@ -1962,9 +2091,9 @@ def test_cancel_order(default_conf, mocker, exchange_name):
def test_cancel_stoploss_order(default_conf, mocker, exchange_name): def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False default_conf['dry_run'] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123) api_mock.cancel_order = MagicMock(return_value={'id': '123'})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123 assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == {'id': '123'}
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
@ -2289,6 +2418,19 @@ def test_get_markets_error(default_conf, mocker):
ex.get_markets('LTC', 'USDT', True, False) ex.get_markets('LTC', 'USDT', True, False)
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
timeframes = ('1m', '5m', '1h')
expected = exchange._ft_has['ohlcv_candle_limit']
for timeframe in timeframes:
if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
# This should only run for bittrex
assert exchange_name == 'bittrex'
assert exchange.ohlcv_candle_limit(timeframe) == expected
def test_timeframe_to_minutes(): def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10 assert timeframe_to_minutes("10m") == 10
@ -2333,6 +2475,9 @@ def test_timeframe_to_prev_date():
date = datetime.now(tz=timezone.utc) date = datetime.now(tz=timezone.utc)
assert timeframe_to_prev_date("5m") < date assert timeframe_to_prev_date("5m") < date
# Does not round
time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)
assert timeframe_to_prev_date('5m', time) == time
def test_timeframe_to_next_date(): def test_timeframe_to_next_date():

View File

@ -37,7 +37,7 @@ def hyperopt_results():
return pd.DataFrame( return pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], '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], 'profit_abs': [-0.2, 0.4, 0.6],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10],
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],

View File

@ -510,7 +510,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
) )
assert len(results) == len(data.trades) assert 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): for c, trade in enumerate(data.trades):
res = results.iloc[c] res = results.iloc[c]

View File

@ -341,12 +341,14 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest')
mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats')
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results')
sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats')
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC'])) PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['timeframe'] = '1m' default_conf['timeframe'] = '1m'
default_conf['datadir'] = testdatadir default_conf['datadir'] = testdatadir
default_conf['export'] = None default_conf['export'] = 'trades'
default_conf['exportfilename'] = 'export.txt'
default_conf['timerange'] = '-1510694220' default_conf['timerange'] = '-1510694220'
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -361,6 +363,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
assert log_has(line, caplog) assert log_has(line, caplog)
assert backtesting.strategy.dp._pairlists is not None assert backtesting.strategy.dp._pairlists is not None
assert backtesting.strategy.bot_loop_start.call_count == 1 assert backtesting.strategy.bot_loop_start.call_count == 1
assert sbs.call_count == 1
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
@ -445,7 +448,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf) 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 default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
@ -469,21 +472,28 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
expected = pd.DataFrame( expected = pd.DataFrame(
{'pair': [pair, pair], {'pair': [pair, pair],
'profit_percent': [0.0, 0.0], 'stake_amount': [0.001, 0.001],
'profit_abs': [0.0, 0.0], 'amount': [0.00957442, 0.0097064],
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True 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, 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime,
Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True),
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541], 'close_rate': [0.104969, 0.103541],
'close_fee': [0.0025, 0.0025], 'fee_open': [0.0025, 0.0025],
'amount': [0.00957442, 0.0097064], 'fee_close': [0.0025, 0.0025],
'trade_duration': [235, 40], 'trade_duration': [235, 40],
'open_at_end': [False, False], 'profit_ratio': [0.0, 0.0],
'sell_reason': [SellType.ROI, SellType.ROI] '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) pd.testing.assert_frame_equal(results, expected)
data_pair = processed[pair] data_pair = processed[pair]
@ -629,7 +639,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
# 100 buys signals # 100 buys signals
assert len(results) == 100 assert len(results) == 100
# One trade was force-closed at the end # 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']) @pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC'])
@ -737,7 +747,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir):
def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
patch_exchange(mocker) 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', mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['UNITTEST/BTC'])) PropertyMock(return_value=['UNITTEST/BTC']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
@ -803,7 +813,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
patch_exchange(mocker) patch_exchange(mocker)
backtestmock = MagicMock(side_effect=[ backtestmock = MagicMock(side_effect=[
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], 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], 'profit_abs': [0.0, 0.0],
'open_date': pd.to_datetime(['2018-01-29 18:40:00', 'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', ], utc=True '2018-01-30 03:30:00', ], utc=True
@ -811,13 +821,13 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'close_date': pd.to_datetime(['2018-01-29 20:45:00', 'close_date': pd.to_datetime(['2018-01-29 20:45:00',
'2018-01-30 05:35:00', ], utc=True), '2018-01-30 05:35:00', ], utc=True),
'trade_duration': [235, 40], 'trade_duration': [235, 40],
'open_at_end': [False, False], 'is_open': [False, False],
'open_rate': [0.104445, 0.10302485], 'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541], 'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI] 'sell_reason': [SellType.ROI, SellType.ROI]
}), }),
pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], 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], 'profit_abs': [0.01, 0.02, 0.2],
'open_date': pd.to_datetime(['2018-01-29 18:40:00', 'open_date': pd.to_datetime(['2018-01-29 18:40:00',
'2018-01-30 03:30:00', '2018-01-30 03:30:00',
@ -827,7 +837,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'2018-01-30 05:35:00', '2018-01-30 05:35:00',
'2018-01-30 08:30:00'], utc=True), '2018-01-30 08:30:00'], utc=True),
'trade_duration': [47, 40, 20], 'trade_duration': [47, 40, 20],
'open_at_end': [False, False, False], 'is_open': [False, False, False],
'open_rate': [0.104445, 0.10302485, 0.122541], 'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541], 'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]

View File

@ -251,9 +251,9 @@ def test_start_no_data(mocker, hyperopt_conf) -> None:
def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf))) hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf)))
patched_configuration_load_config_file(mocker, hyperopt_conf) patched_configuration_load_config_file(mocker, hyperopt_conf)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.__init__', hyperopt_mock)
patch_exchange(mocker) patch_exchange(mocker)
args = [ args = [
@ -427,7 +427,7 @@ def test_format_results(hyperopt):
('LTC/BTC', 1, 1, 123), ('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246) ('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) df = pd.DataFrame.from_records(trades, columns=labels)
results_metrics = hyperopt._calculate_results_metrics(df) results_metrics = hyperopt._calculate_results_metrics(df)
results_explanation = hyperopt._format_results_explanation_string(results_metrics) results_explanation = hyperopt._format_results_explanation_string(results_metrics)
@ -567,7 +567,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
trades = [ trades = [
('TRX/BTC', 0.023117, 0.000233, 100) ('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) backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch( mocker.patch(

View File

@ -60,9 +60,9 @@ def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results)
def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() 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 = 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) hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600, 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: def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() 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 = 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'}) default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) 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: def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() 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 = 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'}) default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) 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: def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() 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 = 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'}) default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) 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: def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() 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 = 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'}) default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) 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: def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy() 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 = 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'}) default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) hl = HyperOptLossResolver.load_hyperoptloss(default_conf)

View File

@ -27,7 +27,7 @@ def test_text_table_bt_results():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC'], 'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2], 'profit_ratio': [0.1, 0.2],
'profit_abs': [0.2, 0.4], 'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30], 'trade_duration': [10, 30],
'wins': [2, 0], 'wins': [2, 0],
@ -59,7 +59,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
results = {'DefStrat': { results = {'DefStrat': {
'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"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], "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime, Arrow(2017, 11, 14, 21, 36, 00).datetime,
@ -72,7 +72,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"trade_duration": [123, 34, 31, 14], "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, "sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL] SellType.ROI, SellType.FORCE_SELL]
}), }),
@ -103,7 +103,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
results = {'DefStrat': { results = {'DefStrat': {
'results': pd.DataFrame( 'results': pd.DataFrame(
{"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], {"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], "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003],
"open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime, Arrow(2017, 11, 14, 21, 36, 00).datetime,
@ -179,7 +179,7 @@ def test_generate_pair_metrics():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC'], 'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2], 'profit_ratio': [0.1, 0.2],
'profit_abs': [0.2, 0.4], 'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30], 'trade_duration': [10, 30],
'wins': [2, 0], 'wins': [2, 0],
@ -227,7 +227,7 @@ def test_text_table_sell_reason():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], '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], 'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10],
'wins': [2, 0, 0], 'wins': [2, 0, 0],
@ -259,7 +259,7 @@ def test_generate_sell_reason_stats():
results = pd.DataFrame( results = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], '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], 'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10],
'wins': [2, 0, 0], 'wins': [2, 0, 0],
@ -295,7 +295,7 @@ def test_text_table_strategy(default_conf):
results['TestStrategy1'] = {'results': pd.DataFrame( results['TestStrategy1'] = {'results': pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], '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], 'profit_abs': [0.2, 0.4, 0.5],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10],
'wins': [2, 0, 0], 'wins': [2, 0, 0],
@ -307,7 +307,7 @@ def test_text_table_strategy(default_conf):
results['TestStrategy2'] = {'results': pd.DataFrame( results['TestStrategy2'] = {'results': pd.DataFrame(
{ {
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], '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], 'profit_abs': [0.4, 0.4, 0.5],
'trade_duration': [15, 30, 15], 'trade_duration': [15, 30, 15],
'wins': [4, 1, 0], 'wins': [4, 1, 0],

View File

@ -126,6 +126,20 @@ def test_load_pairlist_noexist(mocker, markets, default_conf):
default_conf, {}, 1) default_conf, {}, 1)
def test_load_pairlist_verify_multi(mocker, markets, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
plm = PairListManager(freqtrade.exchange, default_conf)
# Call different versions one after the other, should always consider what was passed in
# and have no side-effects (therefore the same check multiple times)
assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC']
assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', 'BUUU/BTC'], print) == ['ETH/BTC', 'XRP/BTC']
assert plm.verify_whitelist(['XRP/BTC', 'BUUU/BTC'], print) == ['XRP/BTC']
assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC']
assert plm.verify_whitelist(['ETH/USDT', 'XRP/USDT', ], print) == ['ETH/USDT', ]
assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC']
def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf):
freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf)

View File

@ -83,7 +83,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
"method": "StoplossGuard", "method": "StoplossGuard",
"lookback_period": 60, "lookback_period": 60,
"stop_duration": 40, "stop_duration": 40,
"trade_limit": 2 "trade_limit": 3
}] }]
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
message = r"Trading stopped due to .*" message = r"Trading stopped due to .*"
@ -136,7 +136,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
default_conf['protections'] = [{ default_conf['protections'] = [{
"method": "StoplossGuard", "method": "StoplossGuard",
"lookback_period": 60, "lookback_period": 60,
"trade_limit": 1, "trade_limit": 2,
"stop_duration": 60, "stop_duration": 60,
"only_per_pair": only_per_pair "only_per_pair": only_per_pair
}] }]

View File

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

Some files were not shown because too many files have changed in this diff Show More