Merge pull request #4609 from freqtrade/new_release

New release 2021.3
This commit is contained in:
Matthias 2021-03-28 11:32:33 +02:00 committed by GitHub
commit 2cf36ca545
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 2255 additions and 1110 deletions

View File

@ -102,7 +102,7 @@ jobs:
mypy freqtrade scripts mypy freqtrade scripts
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
@ -194,7 +194,7 @@ jobs:
mypy freqtrade scripts mypy freqtrade scripts
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
@ -257,7 +257,7 @@ jobs:
mypy freqtrade scripts mypy freqtrade scripts
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
@ -288,7 +288,7 @@ jobs:
mkdocs build mkdocs build
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: lazy-actions/slatify@v3.0.0
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
@ -311,7 +311,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: lazy-actions/slatify@v3.0.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}
@ -398,7 +398,7 @@ jobs:
- name: Slack Notification - name: Slack Notification
uses: homoluctus/slatify@v1.8.0 uses: lazy-actions/slatify@v3.0.0
if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: always() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
with: with:
type: ${{ job.status }} type: ${{ job.status }}

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 apt-get install -y libhdf5-serial-dev \
&& apt-get clean \
&& pip install -e . --no-cache-dir \
&& freqtrade install-ui && freqtrade install-ui
ENTRYPOINT ["freqtrade"] ENTRYPOINT ["freqtrade"]

View File

@ -41,13 +41,13 @@
"ETH/BTC", "ETH/BTC",
"LTC/BTC", "LTC/BTC",
"ETC/BTC", "ETC/BTC",
"DASH/BTC", "RVN/BTC",
"ZEC/BTC", "CRO/BTC",
"XLM/BTC", "XLM/BTC",
"XRP/BTC", "XRP/BTC",
"TRX/BTC", "TRX/BTC",
"ADA/BTC", "ADA/BTC",
"XMR/BTC" "DOT/BTC"
], ],
"pair_blacklist": [ "pair_blacklist": [
"DOGE/BTC" "DOGE/BTC"

View File

@ -49,6 +49,8 @@
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market", "emergencysell": "market",
"forcesell": "market",
"forcebuy": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@ -111,7 +113,7 @@
"password": "", "password": "",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": false, "enableRateLimit": true,
"rateLimit": 500, "rateLimit": 500,
"aiohttp_trust_env": false "aiohttp_trust_env": false
}, },

View File

@ -6,7 +6,7 @@ class.
## Derived hyperopt classes ## Derived hyperopt classes
Custom hyperop classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies). Custom hyperopt classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies).
Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace: Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace:
@ -32,6 +32,51 @@ or
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... $ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ...
``` ```
## Sharing methods with your strategy
Hyperopt classes provide access to the Strategy via the `strategy` class attribute.
This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users.
``` python
from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
buy_params = {
'rsi-value': 30,
'adx-value': 35,
}
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return self.buy_strategy_generator(self.buy_params, dataframe, metadata)
@staticmethod
def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) &
dataframe['adx'] > params['adx-value']) &
dataframe['volume'] > 0
)
, 'buy'] = 1
return dataframe
class MyAwesomeHyperOpt(IHyperOpt):
...
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
# Call strategy's buy strategy generator
return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata)
return populate_buy_trend
```
## Creating and using a custom loss function ## Creating and using a custom loss function
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.

View File

@ -16,6 +16,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--max-open-trades INT] [--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--enable-protections] [--eps] [--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH] [--export EXPORT] [--export-filename PATH]
@ -48,6 +49,9 @@ optional arguments:
Enable protections for backtesting.Will slow Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will backtesting down by a considerable amount, but will
include configured protections include configured protections
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...] --strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to Provide a space-separated list of strategies to
backtest. Please note that ticker-interval needs to be backtest. Please note that ticker-interval needs to be
@ -91,8 +95,7 @@ Strategy arguments:
## 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
real data. This is what we call real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting).
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default. Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/<exchange>` by default.
If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`. If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`.
@ -100,6 +103,8 @@ For details on downloading, please refer to the [Data Downloading](data-download
The result of backtesting will confirm if your bot has better odds of making a profit than a loss. The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation.
!!! 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.
@ -107,38 +112,56 @@ The result of backtesting will confirm if your bot has better odds of making a p
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.
### Run a backtesting against the currencies listed in your config file ### Starting balance
#### With 5 min candle (OHLCV) data (per default) Backtesting will require a starting balance, which can be provided as `--dry-run-wallet <balance>` or `--starting-balance <balance>` command line argument, or via `dry_run_wallet` configuration setting.
This amount must be higher than `stake_amount`, otherwise the bot will not be able to simulate any trade.
### Dynamic stake amount
Backtesting supports [dynamic stake amount](configuration.md#dynamic-stake-amount) by configuring `stake_amount` as `"unlimited"`, which will split the starting balance into `max_open_trades` pieces.
Profits from early trades will result in subsequent higher stake amounts, resulting in compounding of profits over the backtesting period.
### Example backtesting commands
With 5 min candle (OHLCV) data (per default)
```bash ```bash
freqtrade backtesting freqtrade backtesting --strategy AwesomeStrategy
``` ```
#### With 1 min candle (OHLCV) data Where `--strategy AwesomeStrategy` / `-s AwesomeStrategy` refers to the class name of the strategy, which is within a python file in the `user_data/strategies` directory.
---
With 1 min candle (OHLCV) data
```bash ```bash
freqtrade backtesting --timeframe 1m freqtrade backtesting --strategy AwesomeStrategy --timeframe 1m
``` ```
#### Using a different on-disk historical candle (OHLCV) data source ---
Providing a custom starting balance of 1000 (in stake currency)
```bash
freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000
```
---
Using a different on-disk historical candle (OHLCV) data source
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
You can then use this data for backtesting as follows: You can then use this data for backtesting as follows:
```bash ```bash
freqtrade --datadir user_data/data/bittrex-20180101 backtesting freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101
``` ```
#### With a (custom) strategy file ---
```bash Comparing multiple Strategies
freqtrade backtesting -s SampleStrategy
```
Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
#### Comparing multiple Strategies
```bash ```bash
freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m
@ -146,23 +169,29 @@ freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timefram
Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies. Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
#### Exporting trades to file ---
Exporting trades to file
```bash ```bash
freqtrade backtesting --export trades --config config.json --strategy SampleStrategy freqtrade backtesting --strategy backtesting --export trades --config config.json
``` ```
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
#### Exporting trades to file specifying a custom filename ---
Exporting trades to file specifying a custom filename
```bash ```bash
freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json freqtrade backtesting --strategy backtesting --export trades --export-filename=backtest_samplestrategy.json
``` ```
Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period). Please also read about the [strategy startup period](strategy-customization.md#strategy-startup-period).
#### Supplying custom fee value ---
Supplying custom fee value
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting.
@ -177,26 +206,26 @@ freqtrade backtesting --fee 0.001
!!! Note !!! Note
Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info. Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info.
#### Running backtest with smaller testset by using timerange ---
Use the `--timerange` argument to change how much of the testset you want to use. Running backtest with smaller test-set by using timerange
Use the `--timerange` argument to change how much of the test-set you want to use.
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata. For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your input data.
```bash ```bash
freqtrade backtesting --timerange=20190501- freqtrade backtesting --timerange=20190501-
``` ```
You can also specify particular dates or a range span indexed by start and stop. You can also specify particular date ranges.
The full timerange specification: The full timerange specification:
- Use tickframes till 2018/01/31: `--timerange=-20180131` - Use data until 2018/01/31: `--timerange=-20180131`
- Use tickframes since 2018/01/31: `--timerange=20180131-` - Use data since 2018/01/31: `--timerange=20180131-`
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` - Use data since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
- Use tickframes between POSIX timestamps 1527595200 1527618600: - Use data between POSIX / epoch timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
`--timerange=1527595200-1527618600`
## Understand the backtesting result ## Understand the backtesting result
@ -248,19 +277,30 @@ A backtesting result will look like that:
| Max open trades | 3 | | Max open trades | 3 |
| | | | | |
| Total trades | 429 | | Total trades | 429 |
| Total Profit % | 152.41% | | Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| Trades per day | 3.575 | | Trades per day | 3.575 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | | | | |
| Best Pair | LSK/BTC 26.26% | | Best Pair | LSK/BTC 26.26% |
| Worst Pair | ZEC/BTC -10.18% | | Worst Pair | ZEC/BTC -10.18% |
| Best Trade | LSK/BTC 4.25% | | Best Trade | LSK/BTC 4.25% |
| Worst Trade | ZEC/BTC -10.25% | | Worst Trade | ZEC/BTC -10.25% |
| Best day | 25.27% | | Best day | 0.00076 BTC |
| Worst day | -30.67% | | Worst day | -0.00036 BTC |
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| | | | | |
| Max Drawdown | 50.63% | | Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
| Drawdown | 50.63% |
| Drawdown | 0.0015 BTC |
| Drawdown high | 0.0013 BTC |
| Drawdown low | -0.0002 BTC |
| Drawdown Start | 2019-02-15 14:10:00 | | Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 | | Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% | | Market change | -5.88% |
@ -281,9 +321,9 @@ here:
The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses. The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses.
The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`). The column `Tot Profit %` shows instead the total profit % in relation to the starting balance.
In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`. In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`.
Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set. Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
@ -324,19 +364,30 @@ It contains some useful key metrics about performance of your strategy on backte
| Max open trades | 3 | | Max open trades | 3 |
| | | | | |
| Total trades | 429 | | Total trades | 429 |
| Total Profit % | 152.41% | | Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
| Trades per day | 3.575 | | Trades per day | 3.575 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | | | | |
| Best Pair | LSK/BTC 26.26% | | Best Pair | LSK/BTC 26.26% |
| Worst Pair | ZEC/BTC -10.18% | | Worst Pair | ZEC/BTC -10.18% |
| Best Trade | LSK/BTC 4.25% | | Best Trade | LSK/BTC 4.25% |
| Worst Trade | ZEC/BTC -10.25% | | Worst Trade | ZEC/BTC -10.25% |
| Best day | 25.27% | | Best day | 0.00076 BTC |
| Worst day | -30.67% | | Worst day | -0.00036 BTC |
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 | | Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 | | Avg. Duration Loser | 6:55:00 |
| | | | | |
| Max Drawdown | 50.63% | | Min balance | 0.00945123 BTC |
| Max balance | 0.01846651 BTC |
| Drawdown | 50.63% |
| Drawdown | 0.0015 BTC |
| Drawdown high | 0.0013 BTC |
| Drawdown low | -0.0002 BTC |
| Drawdown Start | 2019-02-15 14:10:00 | | Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 | | Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% | | Market change | -5.88% |
@ -347,13 +398,21 @@ 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`) - or number of pairs in the pairlist (whatever is lower). - `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. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. - `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
- `Final balance`: Final balance - starting balance + absolute profit.
- `Absolute profit`: Profit made in stake currency.
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital Starting capital) / Starting capital`.
- `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).
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `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 single winning trade and biggest single losing trade.
- `Best day` / `Worst day`: Best and worst day based on daily profit. - `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
@ -418,6 +477,5 @@ Detailed output for all strategies one after the other will be available, so mak
## Next step ## Next step
Great, your strategy is profitable. What if the bot can give your the Great, your strategy is profitable. What if the bot can give your the optimal parameters to use for your strategy?
optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md) Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)

View File

@ -4,14 +4,14 @@ 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. * **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).
* Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...). * **Timeframe**: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
* Indicators: Technical indicators (SMA, EMA, RSI, ...). * **Indicators**: Technical indicators (SMA, EMA, RSI, ...).
* Limit order: Limit orders which execute at the defined limit price or better. * **Limit order**: Limit orders which execute at the defined limit price or better.
* Market order: Guaranteed to fill, may move price depending on the order size. * **Market order**: Guaranteed to fill, may move price depending on the order size.
## Fee handling ## Fee handling
@ -53,6 +53,7 @@ This loop will be repeated again and again until the bot is stopped.
* Calls `bot_loop_start()` once. * Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair). * Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair) * Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair)
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy)
* Loops per candle simulating entry and exit points. * Loops per candle simulating entry and exit points.
* Generate backtest report output * Generate backtest report output

View File

@ -56,6 +56,7 @@ optional arguments:
usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade trade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH]
[--db-url PATH] [--sd-notify] [--dry-run] [--db-url PATH] [--sd-notify] [--dry-run]
[--dry-run-wallet DRY_RUN_WALLET]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -66,6 +67,9 @@ optional arguments:
--sd-notify Notify systemd service manager. --sd-notify Notify systemd service manager.
--dry-run Enforce dry-run for trading (removes Exchange secrets --dry-run Enforce dry-run for trading (removes Exchange secrets
and simulates trades). and simulates trades).
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -40,8 +40,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| Parameter | Description | | Parameter | Description |
|------------|-------------| |------------|-------------|
| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1. | `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
| `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String | `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Positive float or `"unlimited"`. | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio) | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
@ -49,7 +49,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String | `timeframe` | The timeframe (former ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** String
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> **Datatype:** String
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in Dry Run mode.<br>*Defaults to `1000`.* <br> **Datatype:** Float
| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions. <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict | `minimal_roi` | **Required.** Set the threshold as ratio the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
@ -58,15 +58,17 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Float
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`). | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio) | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`). | `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled).
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean | `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
@ -97,6 +99,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `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
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean | `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
@ -141,8 +144,6 @@ Values set in the configuration file always overwrite values set in the strategy
* `process_only_new_candles` * `process_only_new_candles`
* `order_types` * `order_types`
* `order_time_in_force` * `order_time_in_force`
* `stake_currency`
* `stake_amount`
* `unfilledtimeout` * `unfilledtimeout`
* `disable_dataframe_checks` * `disable_dataframe_checks`
* `protections` * `protections`
@ -156,6 +157,23 @@ Values set in the configuration file always overwrite values set in the strategy
There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below. There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below.
#### Minimum trade stake
The minimum stake amount will depend by exchange and pair, and is usually listed in the exchange support pages.
Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.4$.
The minimum stake amount to buy this pair is therefore `20 * 0.6 ~= 12`.
This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case.
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`).
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.
!!! Warning
Since the limits on exchanges are usually stable and are not updated often, some pairs can show pretty high minimum limits, simply because the price increased a lot since the last limit adjustment by the exchange.
#### Available balance #### Available balance
By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade. By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade.
@ -218,11 +236,14 @@ To allow the bot to trade all the available `stake_currency` in your account (mi
"tradable_balance_ratio": 0.99, "tradable_balance_ratio": 0.99,
``` ```
!!! Note !!! Tip "Compounding profits"
This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available), and will result in profit compounding.
!!! Note "When using Dry-Run Mode" !!! Note "When using Dry-Run Mode"
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time.
It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
--8<-- "includes/pricing.md"
### Understand minimal_roi ### Understand minimal_roi
@ -275,7 +296,7 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy
### Understand order_types ### Understand order_types
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
This allows to buy using limit orders, sell using This allows to buy using limit orders, sell using
limit-orders, and create stoplosses using market orders. It also allows to set the limit-orders, and create stoplosses using market orders. It also allows to set the
@ -287,7 +308,7 @@ the buy order is fulfilled.
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
`stoploss_on_exchange`) need to be present, otherwise the bot will fail to start. `stoploss_on_exchange`) need to be present, otherwise the bot will fail to start.
For information on (`emergencysell`,`stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
Syntax for Strategy: Syntax for Strategy:
@ -296,6 +317,8 @@ order_types = {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market", "emergencysell": "market",
"forcebuy": "market",
"forcesell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": False, "stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60, "stoploss_on_exchange_interval": 60,
@ -310,6 +333,8 @@ Configuration:
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market", "emergencysell": "market",
"forcebuy": "market",
"forcesell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@ -410,26 +435,6 @@ This configuration enables binance, as well as rate limiting to avoid bans from
Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings.
We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.
#### Advanced Freqtrade Exchange configuration
Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behaviours.
Available options are listed in the exchange-class as `_ft_has_default`.
For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call):
```json
"exchange": {
"name": "kraken",
"_ft_has_params": {
"order_time_in_force": ["gtc", "fok"],
"ohlcv_candle_limit": 200
}
```
!!! Warning
Please make sure to fully understand the impacts of these settings before modifying them.
### What values can be used for fiat_display_currency? ### What values can be used for fiat_display_currency?
The `fiat_display_currency` configuration parameter sets the base currency to use for the The `fiat_display_currency` configuration parameter sets the base currency to use for the
@ -449,8 +454,6 @@ The valid values are:
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
``` ```
--8<-- "includes/pricing.md"
## Using Dry-run mode ## Using 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

View File

@ -1,5 +1,7 @@
# Using Freqtrade with Docker # Using Freqtrade with Docker
This page explains how to run the bot with Docker. It is not meant to work out of the box. You'll still need to read through the documentation and understand how to properly configure it.
## Install Docker ## Install Docker
Start by downloading and installing Docker CE for your platform: Start by downloading and installing Docker CE for your platform:
@ -75,7 +77,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a
1. The configuration is now available as `user_data/config.json` 1. The configuration is now available as `user_data/config.json`
2. Copy a custom strategy to the directory `user_data/strategies/` 2. Copy a custom strategy to the directory `user_data/strategies/`
3. add the Strategy' class name to the `docker-compose.yml` file 3. Add the Strategy' class name to the `docker-compose.yml` file
The `SampleStrategy` is run by default. The `SampleStrategy` is run by default.
@ -90,6 +92,9 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li
docker-compose up -d docker-compose up -d
``` ```
!!! Warning "Default configuration"
While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot.
#### Monitoring the bot #### Monitoring the bot
You can check for running instances with `docker-compose ps`. You can check for running instances with `docker-compose ps`.

View File

@ -40,6 +40,10 @@ Due to the heavy rate-limiting applied by Kraken, the following configuration se
}, },
``` ```
!!! Warning "Downloading data from kraken"
Downloading kraken data will require significantly more memory (RAM) than any other exchange, as the trades-data needs to be converted into candles on your machine.
It will also take a long time, as freqtrade will need to download every single trade that happened on the exchange for the pair / timerange combination, therefore please be patient.
## Bittrex ## Bittrex
### Order types ### Order types
@ -92,9 +96,6 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
} }
``` ```
!!! Note
Older versions of freqtrade may require this key to be added to `"ccxt_async_config"` as well.
## All exchanges ## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
@ -117,3 +118,23 @@ Whether your exchange returns incomplete candles or not can be checked using [th
Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle.
However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy.
### Advanced Freqtrade Exchange configuration
Advanced options can be configured using the `_ft_has_params` setting, which will override Defaults and exchange-specific behavior.
Available options are listed in the exchange-class as `_ft_has_default`.
For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call):
```json
"exchange": {
"name": "kraken",
"_ft_has_params": {
"order_time_in_force": ["gtc", "fok"],
"ohlcv_candle_limit": 200
}
```
!!! Warning
Please make sure to fully understand the impacts of these settings before modifying them.

View File

@ -43,7 +43,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--max-open-trades INT] [--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--hyperopt NAME] [--hyperopt-path PATH] [--eps] [--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--dmmp] [--enable-protections] [-e INT] [--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS] [--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT] [--random-state INT] [--min-trades INT]
@ -82,6 +83,9 @@ optional arguments:
Enable protections for backtesting.Will slow Enable protections for backtesting.Will slow
backtesting down by a considerable amount, but will backtesting down by a considerable amount, but will
include configured protections include configured protections
--dry-run-wallet DRY_RUN_WALLET, --starting-balance DRY_RUN_WALLET
Starting balance, used for backtesting / hyperopt and
dry-runs.
-e INT, --epochs INT Specify number of epochs (default: 100). -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} ...] --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]
Specify which parameters to hyperopt. Space-separated Specify which parameters to hyperopt. Space-separated
@ -161,7 +165,7 @@ Depending on the space you want to optimize, only some of the below are required
* fill `sell_indicator_space` - for sell signal optimization * fill `sell_indicator_space` - for sell signal optimization
!!! Note !!! Note
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work.
Optional in hyperopt - can also be loaded from a strategy (recommended): Optional in hyperopt - can also be loaded from a strategy (recommended):
@ -279,7 +283,7 @@ So let's write the buy strategy using these values:
""" """
Define the buy strategy parameters to be used by Hyperopt. Define the buy strategy parameters to be used by Hyperopt.
""" """
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
if 'adx-enabled' in params and params['adx-enabled']: if 'adx-enabled' in params and params['adx-enabled']:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -103,6 +103,10 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting
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. 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.
When not using orderbook (`ask_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 `ask_strategy.bid_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 `side` and last price.
### Market order pricing ### Market order pricing
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.

View File

@ -5,12 +5,8 @@
<!-- Place this tag where you want the button to render. --> <!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a> <a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a> <a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/stable.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a> <a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/stable.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
<!-- Place this tag where you want the button to render. -->
<a class="github-button" href="https://github.com/freqtrade" data-size="large" aria-label="Follow @freqtrade on GitHub">Follow @freqtrade</a>
## Introduction ## Introduction

View File

@ -6,22 +6,22 @@ This file was automatically generated - do not edit
{% set site_url = site_url ~ "/index.html" %} {% set site_url = site_url ~ "/index.html" %}
{% endif %} {% endif %}
<header class="md-header" data-md-component="header"> <header class="md-header" data-md-component="header">
<nav class="md-header-nav md-grid" aria-label="{{ lang.t('header.title') }}"> <nav class="md-header__inner md-grid" aria-label="{{ lang.t('header.title') }}">
<a href="{{ site_url }}" title="{{ config.site_name | e }}" class="md-header-nav__button md-logo" <a href="{{ site_url }}" title="{{ config.site_name | e }}" class="md-header__button md-logo"
aria-label="{{ config.site_name }}"> aria-label="{{ config.site_name }}">
{% include "partials/logo.html" %} {% include "partials/logo.html" %}
</a> </a>
<label class="md-header-nav__button md-icon" for="__drawer"> <label class="md-header__button md-icon" for="__drawer">
{% include ".icons/material/menu" ~ ".svg" %} {% include ".icons/material/menu" ~ ".svg" %}
</label> </label>
<div class="md-header-nav__title" data-md-component="header-title"> <div class="md-header__title" data-md-component="header-title">
<div class="md-header-nav__ellipsis"> <div class="md-header__ellipsis">
<div class="md-header-nav__topic"> <div class="md-header__topic">
<span class="md-ellipsis"> <span class="md-ellipsis">
{{ config.site_name }} {{ config.site_name }}
</span> </span>
</div> </div>
<div class="md-header-nav__topic"> <div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis"> <span class="md-ellipsis">
{% if page and page.meta and page.meta.title %} {% if page and page.meta and page.meta.title %}
{{ page.meta.title }} {{ page.meta.title }}
@ -32,20 +32,41 @@ This file was automatically generated - do not edit
</div> </div>
</div> </div>
</div> </div>
<div class="md-header__options">
{% if config.extra.alternate %}
<div class="md-select">
{% set icon = config.theme.icon.alternate or "material/translate" %}
<span class="md-header__button md-icon">
{% include ".icons/" ~ icon ~ ".svg" %}
</span>
<div class="md-select__inner">
<ul class="md-select__list">
{% for alt in config.extra.alternate %}
<li class="md-select__item">
<a href="{{ alt.link | url }}" class="md-select__link">
{{ alt.name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
{% if "search" in config["plugins"] %} {% if "search" in config["plugins"] %}
<label class="md-header-nav__button md-icon" for="__search"> <label class="md-header__button md-icon" for="__search">
{% include ".icons/material/magnify.svg" %} {% include ".icons/material/magnify.svg" %}
</label> </label>
{% include "partials/search.html" %} {% include "partials/search.html" %}
{% endif %} {% endif %}
{% if config.repo_url %} {% if config.repo_url %}
<div class="md-header-nav__source"> <div class="md-header__source">
{% include "partials/source.html" %} {% include "partials/source.html" %}
</div> </div>
{% endif %} {% endif %}
</nav> </nav>
<!-- Place this tag in your head or just before your close body tag. --> <!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script> <script async defer src="https://buttons.github.io/buttons.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" <script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
</header> </header>

View File

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

View File

@ -131,6 +131,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `status` | Lists all open trades. | `status` | Lists all open trades.
| `count` | Displays number of trades used and available. | `count` | Displays number of trades used and available.
| `locks` | Displays currently locked pairs. | `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
@ -182,6 +183,11 @@ count
daily daily
Return the amount of open trades. Return the amount of open trades.
delete_lock
Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
delete_trade delete_trade
Delete trade from the database. Delete trade from the database.
Tries to close open orders. Requires manual handling of this asset on the exchange. Tries to close open orders. Requires manual handling of this asset on the exchange.
@ -202,6 +208,9 @@ forcesell
:param tradeid: Id of the trade (can be received via status command) :param tradeid: Id of the trade (can be received via status command)
locks
Return current locks
logs logs
Show latest logs. Show latest logs.

View File

@ -6,6 +6,10 @@ With some configuration, freqtrade (in combination with ccxt) provides access to
This document is an overview to configure Freqtrade to be used with sandboxes. This document is an overview to configure Freqtrade to be used with sandboxes.
This can be useful to developers and trader alike. This can be useful to developers and trader alike.
!!! Warning
Sandboxes usually have very low volume, and either a very wide spread, or no orders available at all.
Therefore, sandboxes will usually not do a good job of showing you how a strategy would work in real trading.
## Exchanges known to have a sandbox / testnet ## Exchanges known to have a sandbox / testnet
* [binance](https://testnet.binance.vision/) * [binance](https://testnet.binance.vision/)

View File

@ -51,6 +51,14 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
### forcesell
`forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API.
### forcebuy
`forcebuy` is an optional value, which defaults to the same value as `buy` and is used when sending a `/forcebuy` command from Telegram or from the Rest API.
### emergencysell ### emergencysell
`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.

View File

@ -11,14 +11,73 @@ If you're just getting started, please be familiar with the methods described in
!!! Tip !!! Tip
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
## Storing information
Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python
class AwesomeStrategy(IStrategy):
# Create custom dictionary
custom_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if not metadata["pair"] in self.custom_info:
# Create empty entry for this pair
self.custom_info[metadata["pair"]] = {}
if "crosstime" in self.custom_info[metadata["pair"]]:
self.custom_info[metadata["pair"]]["crosstime"] += 1
else:
self.custom_info[metadata["pair"]]["crosstime"] = 1
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
***
### Storing custom information using DatetimeIndex from `dataframe`
Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
```python
import talib.abstract as ta
class AwesomeStrategy(IStrategy):
# Create custom dictionary
custom_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# using "ATR" here as example
dataframe['atr'] = ta.ATR(dataframe)
if self.dp.runmode.value in ('backtest', 'hyperopt'):
# add indicator mapped to correct DatetimeIndex to custom_info
self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date')
return dataframe
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
See `custom_stoploss` examples below on how to access the saved dataframe columns
## Custom stoploss ## Custom stoploss
A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
The method must return a stoploss value (float / number) with a relative ratio below the current price. The method must return a stoploss value (float / number) as a percentage of the current price.
E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD.
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
@ -87,9 +146,9 @@ class AwesomeStrategy(IStrategy):
current_rate: float, 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_utc:
return -0.05 return -0.05
elif current_time - timedelta(minutes=60) > trade.open_date: elif current_time - timedelta(minutes=60) > trade.open_date_utc:
return -0.10 return -0.10
return 1 return 1
``` ```
@ -148,18 +207,26 @@ class AwesomeStrategy(IStrategy):
return max(min(desired_stoploss, 0.05), 0.025) return max(min(desired_stoploss, 0.05), 0.025)
``` ```
#### Absolute stoploss #### Calculating stoploss relative to open price
The below example sets absolute profit levels based on the current profit. Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
#### Stepped stoploss
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
* Use the regular stoploss until 20% profit is reached * Use the regular stoploss until 20% profit is reached
* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. * Once profit is > 20% - set stoploss to 7% above open price.
* Once profit is > 25% - stoploss will be 15%. * Once profit is > 25% - set stoploss to 15% above open price.
* Once profit is > 20% - stoploss will be set to 7%. * Once profit is > 40% - set stoploss to 25% above open price.
``` python ``` python
from datetime import datetime from datetime import datetime
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy import stoploss_from_open
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -170,15 +237,66 @@ class AwesomeStrategy(IStrategy):
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, 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 # evaluate highest to lowest, so that highest possible stop is used
if current_profit > 0.40: if current_profit > 0.40:
return (-0.25 + current_profit) return stoploss_from_open(0.25, current_profit)
if current_profit > 0.25: elif current_profit > 0.25:
return (-0.15 + current_profit) return stoploss_from_open(0.15, current_profit)
if current_profit > 0.20: elif current_profit > 0.20:
return (-0.7 + current_profit) return stoploss_from_open(0.07, current_profit)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1 return 1
``` ```
#### Custom stoploss using an indicator from dataframe example
Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info`
!!! Warning
only use .iat[-1] in live mode, not in backtesting/hyperopt
otherwise you will look into the future
see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info.
``` python
from freqtrade.persistence import Trade
from freqtrade.state import RunMode
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
result = 1
if self.custom_info and pair in self.custom_info and trade:
# using current_time directly (like below) will only work in backtesting.
# so check "runmode" to make sure that it's only used in backtesting/hyperopt
if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'):
relative_sl = self.custom_info[pair].loc[current_time]['atr']
# in live / dry-run, it'll be really the current time
else:
# but we can just use the last entry from an already analyzed dataframe instead
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
timeframe=self.timeframe)
# WARNING
# only use .iat[-1] in live mode, not in backtesting/hyperopt
# otherwise you will look into the future
# see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies
relative_sl = dataframe['atr'].iat[-1]
if (relative_sl is not None):
# new stoploss relative to current_rate
new_stoploss = (current_rate-relative_sl)/current_rate
# turn into relative negative offset required by `custom_stoploss` return implementation
result = new_stoploss - 1
return result
```
--- ---
@ -199,7 +317,7 @@ It applies a tight timeout for higher priced assets, while allowing more time to
The function must return either `True` (cancel order) or `False` (keep order alive). The function must return either `True` (cancel order) or `False` (keep order alive).
``` python ``` python
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -213,21 +331,21 @@ class AwesomeStrategy(IStrategy):
} }
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
return True return True
elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
return True return True
return False return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3):
return True return True
elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24): elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24):
return True return True
return False return False
``` ```

View File

@ -300,38 +300,7 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
The Metadata-dict should not be modified and does not persist information across multiple calls. The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the section [Storing information](#Storing-information) Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information)
### Storing information
Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python
class AwesomeStrategy(IStrategy):
# Create custom dictionary
cust_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if not metadata["pair"] in self.cust_info:
# Create empty entry for this pair
self.cust_info[metadata["pair"]] = {}
if "crosstime" in self.cust_info[metadata["pair"]]:
self.cust_info[metadata["pair"]]["crosstime"] += 1
else:
self.cust_info[metadata["pair"]]["crosstime"] = 1
```
!!! Warning
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
***
## Additional data (informative_pairs) ## Additional data (informative_pairs)
@ -467,6 +436,26 @@ if self.dp:
dataframe['best_ask'] = ob['asks'][0][0] dataframe['best_ask'] = ob['asks'][0][0]
``` ```
The orderbook structure is aligned with the order structure from [ccxt](https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure), so the result will look as follows:
``` js
{
'bids': [
[ price, amount ], // [ float, float ]
[ price, amount ],
...
],
'asks': [
[ price, amount ],
[ price, amount ],
//...
],
//...
}
```
Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position.
!!! Warning "Warning about backtesting" !!! 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, as the method will return uptodate values. 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.
@ -618,6 +607,43 @@ All columns of the informative dataframe will be available on the returning data
*** ***
### *stoploss_from_open()*
Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price.
??? Example "Returning a stoploss relative to the open price from the custom stoploss function"
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
``` python
from datetime import datetime
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, stoploss_from_open
class AwesomeStrategy(IStrategy):
# ... populate_* methods
use_custom_stoploss = True
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# once the profit has risin above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit)
return 1
```
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
## Additional data (Wallets) ## Additional data (Wallets)
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
@ -709,7 +735,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished.
!!! Warning !!! Warning
Locking pairs is not available during backtesting. Manually locking pairs is not available during backtesting, only locks via Protections are allowed.
#### Pair locking example #### Pair locking example

View File

@ -83,10 +83,13 @@ Example configuration showing the different settings:
"sell": "on", "sell": "on",
"buy_cancel": "silent", "buy_cancel": "silent",
"sell_cancel": "on" "sell_cancel": "on"
} },
"balance_dust_level": 0.01
}, },
``` ```
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
## Create a custom keyboard (command shortcut buttons) ## Create a custom keyboard (command shortcut buttons)
Telegram allows us to create a custom keyboard with buttons for commands. Telegram allows us to create a custom keyboard with buttons for commands.
@ -143,6 +146,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `/count` | Displays number of trades used and available | `/count` | Displays number of trades used and available
| `/locks` | Show currently locked pairs. | `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).

View File

@ -40,6 +40,21 @@ Sample configuration (tested using IFTTT).
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url. The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration:
```json
"webhook": {
"enabled": true,
"url": "https://<YOURSUBDOMAIN>.cloud.mattermost.com/hooks/<YOURHOOK>",
"format": "json",
"webhookstatus": {
"text": "Status: {status}"
}
},
```
The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel.
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
### Webhookbuy ### Webhookbuy

View File

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

View File

@ -14,18 +14,18 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
"max_open_trades", "stake_amount", "fee"] "max_open_trades", "stake_amount", "fee"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"enable_protections", "enable_protections", "dry_run_wallet",
"strategy_list", "export", "exportfilename"] "strategy_list", "export", "exportfilename"]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "use_max_market_positions", "position_stacking", "use_max_market_positions",
"enable_protections", "enable_protections", "dry_run_wallet",
"epochs", "spaces", "print_all", "epochs", "spaces", "print_all",
"print_colorized", "print_json", "hyperopt_jobs", "print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades", "hyperopt_random_state", "hyperopt_min_trades",

View File

@ -93,10 +93,10 @@ def ask_user_config() -> Dict[str, Any]:
"message": "Select exchange", "message": "Select exchange",
"choices": [ "choices": [
"binance", "binance",
"binanceje",
"binanceus", "binanceus",
"bittrex", "bittrex",
"kraken", "kraken",
"ftx",
Separator(), Separator(),
"other", "other",
], ],
@ -173,6 +173,9 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
arguments=selections) arguments=selections)
logger.info(f"Writing config to `{config_path}`.") logger.info(f"Writing config to `{config_path}`.")
logger.info(
"Please make sure to check the configuration contents and adjust settings to your needs.")
config_path.write_text(config_text) config_path.write_text(config_text)

View File

@ -110,6 +110,11 @@ AVAILABLE_CLI_OPTIONS = {
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
action='store_true', action='store_true',
), ),
"dry_run_wallet": Arg(
'--dry-run-wallet', '--starting-balance',
help='Starting balance, used for backtesting / hyperopt and dry-runs.',
type=float,
),
# Optimize common # Optimize common
"timeframe": Arg( "timeframe": Arg(
'-i', '--timeframe', '--ticker-interval', '-i', '--timeframe', '--ticker-interval',
@ -128,7 +133,6 @@ AVAILABLE_CLI_OPTIONS = {
"stake_amount": Arg( "stake_amount": Arg(
'--stake-amount', '--stake-amount',
help='Override the value of the `stake_amount` configuration setting.', help='Override the value of the `stake_amount` configuration setting.',
type=float,
), ),
# Backtesting # Backtesting
"position_stacking": Arg( "position_stacking": Arg(

View File

@ -17,7 +17,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
""" """
List hyperopt epochs previously evaluated List hyperopt epochs previously evaluated
""" """
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_tools import HyperoptTools
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
@ -47,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
config.get('hyperoptexportfilename')) config.get('hyperoptexportfilename'))
# Previous evaluations # Previous evaluations
epochs = Hyperopt.load_previous_results(results_file) epochs = HyperoptTools.load_previous_results(results_file)
total_epochs = len(epochs) total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions) epochs = hyperopt_filter_epochs(epochs, filteroptions)
@ -57,18 +57,19 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if not export_csv: if not export_csv:
try: try:
print(Hyperopt.get_result_table(config, epochs, total_epochs, print(HyperoptTools.get_result_table(config, epochs, total_epochs,
not filteroptions['only_best'], print_colorized, 0)) not filteroptions['only_best'],
print_colorized, 0))
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
if epochs and not no_details: if epochs and not no_details:
sorted_epochs = sorted(epochs, key=itemgetter('loss')) sorted_epochs = sorted(epochs, key=itemgetter('loss'))
results = sorted_epochs[0] results = sorted_epochs[0]
Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) HyperoptTools.print_epoch_details(results, total_epochs, print_json, no_header)
if epochs and export_csv: if epochs and export_csv:
Hyperopt.export_csv_file( HyperoptTools.export_csv_file(
config, epochs, total_epochs, not filteroptions['only_best'], export_csv config, epochs, total_epochs, not filteroptions['only_best'], export_csv
) )
@ -77,7 +78,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
""" """
Show details of a hyperopt epoch previously evaluated Show details of a hyperopt epoch previously evaluated
""" """
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_tools import HyperoptTools
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
@ -105,7 +106,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
} }
# Previous evaluations # Previous evaluations
epochs = Hyperopt.load_previous_results(results_file) epochs = HyperoptTools.load_previous_results(results_file)
total_epochs = len(epochs) total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions) epochs = hyperopt_filter_epochs(epochs, filteroptions)
@ -124,8 +125,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if epochs: if epochs:
val = epochs[n] val = epochs[n]
Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, HyperoptTools.print_epoch_details(val, total_epochs, print_json, no_header,
header_str="Epoch details") header_str="Epoch details")
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:

View File

@ -3,7 +3,8 @@ from typing import Any, Dict
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -22,11 +23,13 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
RunMode.BACKTEST: 'backtesting', RunMode.BACKTEST: 'backtesting',
RunMode.HYPEROPT: 'hyperoptimization', RunMode.HYPEROPT: 'hyperoptimization',
} }
if (method in no_unlimited_runmodes.keys() and if method in no_unlimited_runmodes.keys():
config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT): if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
raise DependencyException( and config['stake_amount'] > config['dry_run_wallet']):
f'The value of `stake_amount` cannot be set as "{constants.UNLIMITED_STAKE_AMOUNT}" ' wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
f'for {no_unlimited_runmodes[method]}') stake = round_coin_value(config['stake_amount'], config['stake_currency'])
raise OperationalException(f"Starting balance ({wallet}) "
f"is smaller than stake_amount {stake}.")
return config return config

View File

@ -47,6 +47,8 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
conf_schema = deepcopy(constants.CONF_SCHEMA) conf_schema = deepcopy(constants.CONF_SCHEMA)
if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE): if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
elif conf.get('runmode', RunMode.OTHER) in (RunMode.BACKTEST, RunMode.HYPEROPT):
conf_schema['required'] = constants.SCHEMA_BACKTEST_REQUIRED
else: else:
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
try: try:
@ -72,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
# validating trailing stoploss # validating trailing stoploss
_validate_trailing_stoploss(conf) _validate_trailing_stoploss(conf)
_validate_price_config(conf)
_validate_edge(conf) _validate_edge(conf)
_validate_whitelist(conf) _validate_whitelist(conf)
_validate_protections(conf) _validate_protections(conf)
@ -93,6 +96,19 @@ def _validate_unlimited_amount(conf: Dict[str, Any]) -> None:
raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.")
def _validate_price_config(conf: Dict[str, Any]) -> None:
"""
When using market orders, price sides must be using the "other" side of the price
"""
if (conf.get('order_types', {}).get('buy') == 'market'
and conf.get('bid_strategy', {}).get('price_side') != 'ask'):
raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".')
if (conf.get('order_types', {}).get('sell') == 'market'
and conf.get('ask_strategy', {}).get('price_side') != 'bid'):
raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".')
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get('stoploss') == 0.0: if conf.get('stoploss') == 0.0:

View File

@ -214,9 +214,6 @@ class Configuration:
self._args_to_config( self._args_to_config(
config, argname='enable_protections', config, argname='enable_protections',
logstring='Parameter --enable-protections detected, enabling Protections. ...') logstring='Parameter --enable-protections detected, enabling Protections. ...')
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
config.update({'use_max_market_positions': False}) config.update({'use_max_market_positions': False})
@ -228,11 +225,23 @@ class Configuration:
'overriding max_open_trades to: %s ...', config.get('max_open_trades')) 'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
elif config['runmode'] in NON_UTIL_MODES: elif config['runmode'] in NON_UTIL_MODES:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
if self.args.get('stake_amount', None):
# Convert explicitly to float to support CLI argument for both unlimited and value
try:
self.args['stake_amount'] = float(self.args['stake_amount'])
except ValueError:
pass
self._args_to_config(config, argname='stake_amount', self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake-amount detected, ' logstring='Parameter --stake-amount detected, '
'overriding stake_amount to: {} ...') 'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='dry_run_wallet',
logstring='Parameter --dry-run-wallet detected, '
'overriding dry_run_wallet to: {} ...')
self._args_to_config(config, argname='fee', self._args_to_config(config, argname='fee',
logstring='Parameter --fee detected, ' logstring='Parameter --fee detected, '
'setting fee to: {} ...') 'setting fee to: {} ...')

View File

@ -7,6 +7,8 @@ from typing import Optional
import arrow import arrow
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -103,5 +105,8 @@ class TimeRange:
stop = int(stops) // 1000 stop = int(stops) // 1000
else: else:
stop = int(stops) stop = int(stops)
if start > stop > 0:
raise OperationalException(
f'Start date is after stop date for timerange "{text}"')
return TimeRange(stype[0], stype[1], start, stop) return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text) raise OperationalException(f'Incorrect syntax for timerange "{text}"')

View File

@ -54,6 +54,11 @@ DECIMALS_PER_COIN = {
'ETH': 5, 'ETH': 5,
} }
DUST_PER_COIN = {
'BTC': 0.0001,
'ETH': 0.01
}
# Soure files with destination directories within user-directory # Soure files with destination directories within user-directory
USER_DATA_FILES = { USER_DATA_FILES = {
@ -160,6 +165,12 @@ CONF_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'}, 'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'},
'bid_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False,
},
'use_order_book': {'type': 'boolean'}, 'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'integer', 'minimum': 1}, 'order_book_min': {'type': 'integer', 'minimum': 1},
'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
@ -174,6 +185,8 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'}, 'stoploss_on_exchange': {'type': 'boolean'},
@ -230,6 +243,7 @@ CONF_SCHEMA = {
'enabled': {'type': 'boolean'}, 'enabled': {'type': 'boolean'},
'token': {'type': 'string'}, 'token': {'type': 'string'},
'chat_id': {'type': 'string'}, 'chat_id': {'type': 'string'},
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
'notification_settings': { 'notification_settings': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -243,7 +257,7 @@ CONF_SCHEMA = {
} }
} }
}, },
'required': ['enabled', 'token', 'chat_id'] 'required': ['enabled', 'token', 'chat_id'],
}, },
'webhook': { 'webhook': {
'type': 'object', 'type': 'object',
@ -370,6 +384,16 @@ SCHEMA_TRADE_REQUIRED = [
'dataformat_trades', 'dataformat_trades',
] ]
SCHEMA_BACKTEST_REQUIRED = [
'exchange',
'max_open_trades',
'stake_currency',
'stake_amount',
'dry_run_wallet',
'dataformat_ohlcv',
'dataformat_trades',
]
SCHEMA_MINIMAL_REQUIRED = [ SCHEMA_MINIMAL_REQUIRED = [
'exchange', 'exchange',
'dry_run', 'dry_run',

View File

@ -10,7 +10,7 @@ import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.misc import json_load from freqtrade.misc import json_load
from freqtrade.persistence import Trade, init_db from freqtrade.persistence import LocalTrade, Trade, init_db
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -224,7 +224,7 @@ 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: def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame:
""" """
Convert list of Trade objects to pandas Dataframe Convert list of Trade objects to pandas Dataframe
:param trades: List of trade objects :param trades: List of trade objects
@ -360,13 +360,14 @@ 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_ratio' value_col: str = 'profit_ratio'
) -> Tuple[float, pd.Timestamp, pd.Timestamp]: ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float]:
""" """
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_ratio) :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_ratio') :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, highvalue, lowvalue) with absolute max drawdown,
high and low time and high and low value.
:raise: ValueError if trade-dataframe was found empty. :raise: ValueError if trade-dataframe was found empty.
""" """
if len(trades) == 0: if len(trades) == 0:
@ -382,13 +383,17 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
raise ValueError("No losing trade, therefore no drawdown.") raise ValueError("No losing trade, therefore no drawdown.")
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 high_val = max_drawdown_df.loc[max_drawdown_df.iloc[:idxmin]
['high_value'].idxmax(), 'cumulative']
low_val = max_drawdown_df.loc[idxmin, 'cumulative']
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date, high_val, low_val
def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]: def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[float, float]:
""" """
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane 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) :param trades: DataFrame containing trades (requires columns close_date and profit_percent)
:param starting_balance: Add starting balance to results, to show the wallets high / low points
:return: Tuple (float, float) with cumsum of profit_abs :return: Tuple (float, float) with cumsum of profit_abs
:raise: ValueError if trade-dataframe was found empty. :raise: ValueError if trade-dataframe was found empty.
""" """
@ -397,7 +402,7 @@ def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
csum_df = pd.DataFrame() csum_df = pd.DataFrame()
csum_df['sum'] = trades['profit_abs'].cumsum() csum_df['sum'] = trades['profit_abs'].cumsum()
csum_min = csum_df['sum'].min() csum_min = csum_df['sum'].min() + starting_balance
csum_max = csum_df['sum'].max() csum_max = csum_df['sum'].max() + starting_balance
return csum_min, csum_max return csum_min, csum_max

View File

@ -147,6 +147,9 @@ class Exchange:
""" """
Destructor - clean up async stuff Destructor - clean up async stuff
""" """
self.close()
def close(self):
logger.debug("Exchange object destroyed, closing async loop") logger.debug("Exchange object destroyed, closing async loop")
if self._api_async and inspect.iscoroutinefunction(self._api_async.close): if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
asyncio.get_event_loop().run_until_complete(self._api_async.close()) asyncio.get_event_loop().run_until_complete(self._api_async.close())
@ -308,8 +311,8 @@ class Exchange:
self._markets = 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:
logger.warning('Unable to initialize markets. Reason: %s', e) logger.exception('Unable to initialize markets.')
def reload_markets(self) -> None: def reload_markets(self) -> None:
"""Reload markets both sync and async if refresh interval has passed """ """Reload markets both sync and async if refresh interval has passed """
@ -528,16 +531,16 @@ class Exchange:
return None return None
# reserve some percent defined in config (5% default) + stoploss # reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent', amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
DEFAULT_AMOUNT_RESERVE_PERCENT) DEFAULT_AMOUNT_RESERVE_PERCENT)
amount_reserve_percent += stoploss amount_reserve_percent += abs(stoploss)
# it should not be more than 50% # it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5) amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
# The value returned should satisfy both limits: for amount (base currency) and # The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here. # for cost (quote, stake currency), so max() is used here.
# See also #2575 at github. # See also #2575 at github.
return max(min_stake_amounts) / amount_reserve_percent 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]:
@ -1053,7 +1056,8 @@ class Exchange:
:param order: Order dict as returned from fetch_order() :param order: Order dict as returned from fetch_order()
:return: True if order has been cancelled without being filled, False otherwise. :return: True if order has been cancelled without being filled, False otherwise.
""" """
return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0 return (order.get('status') in ('closed', 'canceled', 'cancelled')
and order.get('filled') == 0.0)
@retrier @retrier
def cancel_order(self, order_id: str, pair: str) -> Dict: def cancel_order(self, order_id: str, pair: str) -> Dict:
@ -1228,6 +1232,8 @@ class Exchange:
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1, def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
price: float = 1, taker_or_maker: str = 'maker') -> float: price: float = 1, taker_or_maker: str = 'maker') -> float:
try: try:
if self._config['dry_run'] and self._config.get('fee', None) is not None:
return self._config['fee']
# validate that markets are loaded before trying to get fee # validate that markets are loaded before trying to get fee
if self._api.markets is None or len(self._api.markets) == 0: if self._api.markets is None or len(self._api.markets) == 0:
self._api.load_markets() self._api.load_markets()

View File

@ -432,7 +432,7 @@ class FreqtradeBot(LoggingMixin):
ticker = self.exchange.fetch_ticker(pair) ticker = self.exchange.fetch_ticker(pair)
ticker_rate = ticker[bid_strategy['price_side']] ticker_rate = ticker[bid_strategy['price_side']]
if ticker['last'] and ticker_rate > ticker['last']: if ticker['last'] and ticker_rate > ticker['last']:
balance = self.config['bid_strategy']['ask_last_balance'] balance = bid_strategy['ask_last_balance']
ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
used_rate = ticker_rate used_rate = ticker_rate
@ -520,7 +520,8 @@ class FreqtradeBot(LoggingMixin):
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
return False return False
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool: def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None,
forcebuy: bool = False) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
@ -548,6 +549,10 @@ class FreqtradeBot(LoggingMixin):
amount = stake_amount / buy_limit_requested amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
if forcebuy:
# Forcebuy can define a different ordertype
order_type = self.strategy.order_types.get('forcebuy', order_type)
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force): time_in_force=time_in_force):
@ -740,7 +745,13 @@ class FreqtradeBot(LoggingMixin):
logger.warning("Sell Price at location from orderbook could not be determined.") logger.warning("Sell Price at location from orderbook could not be determined.")
raise PricingError from e raise PricingError from e
else: else:
rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']] ticker = self.exchange.fetch_ticker(pair)
ticker_rate = ticker[ask_strategy['price_side']]
if ticker['last'] and ticker_rate < ticker['last']:
balance = ask_strategy.get('bid_last_balance', 0.0)
ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
rate = ticker_rate
if rate is None: if rate is None:
raise PricingError(f"Sell-Rate for {pair} was empty.") raise PricingError(f"Sell-Rate for {pair} was empty.")
self._sell_rate_cache[pair] = rate self._sell_rate_cache[pair] = rate
@ -932,7 +943,7 @@ class FreqtradeBot(LoggingMixin):
Check and execute sell Check and execute sell
""" """
should_sell = self.strategy.should_sell( should_sell = self.strategy.should_sell(
trade, sell_rate, datetime.utcnow(), buy, sell, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
@ -1018,13 +1029,13 @@ class FreqtradeBot(LoggingMixin):
was_trade_fully_canceled = False was_trade_fully_canceled = False
# Cancelled orders may have the status of 'canceled' or 'closed' # Cancelled orders may have the status of 'canceled' or 'closed'
if order['status'] not in ('canceled', 'closed'): if order['status'] not in ('cancelled', 'canceled', 'closed'):
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount) trade.amount)
# Avoid race condition where the order could not be cancelled coz its already filled. # Avoid race condition where the order could not be cancelled coz its already filled.
# Simply bailing here is the only safe way - as this order will then be # Simply bailing here is the only safe way - as this order will then be
# handled in the next iteration. # handled in the next iteration.
if corder.get('status') not in ('canceled', 'closed'): if corder.get('status') not in ('cancelled', 'canceled', 'closed'):
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
return False return False
else: else:
@ -1156,6 +1167,10 @@ class FreqtradeBot(LoggingMixin):
if sell_reason == SellType.EMERGENCY_SELL: if sell_reason == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market") order_type = self.strategy.order_types.get("emergencysell", "market")
if sell_reason == SellType.FORCE_SELL:
# Force sells (default to the sell_type defined in the strategy,
# but we allow this value to be changed)
order_type = self.strategy.order_types.get("forcesell", order_type)
amount = self._safe_sell_amount(trade.pair, trade.amount) amount = self._safe_sell_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell'] time_in_force = self.strategy.order_time_in_force['sell']

View File

@ -17,17 +17,18 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import trade_list_to_dataframe 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 DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
store_backtest_stats) store_backtest_stats)
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import LocalTrade, PairLocks, Trade
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -114,6 +115,8 @@ class Backtesting:
if self.config.get('enable_protections', False): if self.config.get('enable_protections', False):
self.protections = ProtectionManager(self.config) self.protections = ProtectionManager(self.config)
self.wallets = Wallets(self.config, self.exchange, log=False)
# Get maximum required startup period # Get maximum required startup period
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
# Load one (first) strategy # Load one (first) strategy
@ -124,7 +127,7 @@ class Backtesting:
PairLocks.use_db = True PairLocks.use_db = True
Trade.use_db = True Trade.use_db = True
def _set_strategy(self, strategy): def _set_strategy(self, strategy: IStrategy):
""" """
Load strategy into backtesting Load strategy into backtesting
""" """
@ -171,10 +174,8 @@ class Backtesting:
PairLocks.use_db = False PairLocks.use_db = False
PairLocks.timeframe = self.config['timeframe'] PairLocks.timeframe = self.config['timeframe']
Trade.use_db = False Trade.use_db = False
if enable_protections: PairLocks.reset_locks()
# Reset persisted data - used for protections only Trade.reset_trades()
PairLocks.reset_locks()
Trade.reset_trades()
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
""" """
@ -203,10 +204,10 @@ class Backtesting:
# Convert from Pandas to list for performance reasons # Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.) # (Looping Pandas is slow.)
data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] data[pair] = df_analyzed.values.tolist()
return data return data
def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple, def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
""" """
Get close rate for backtesting result Get close rate for backtesting result
@ -246,24 +247,67 @@ 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[Trade]: def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX], sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_row[BUY_IDX], sell_row[SELL_IDX], sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
if sell.sell_flag: if sell.sell_flag:
trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = sell.sell_type.value
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
trade.close_date = sell_row[DATE_IDX] # Confirm trade exit:
trade.sell_reason = sell.sell_type time_in_force = self.strategy.order_time_in_force['sell']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate,
time_in_force=time_in_force,
sell_reason=sell.sell_type.value):
return None
trade.close(closerate, show_msg=False) trade.close(closerate, show_msg=False)
return trade return trade
return None return None
def handle_left_open(self, open_trades: Dict[str, List[Trade]], def _enter_trade(self, pair: str, row: List, max_open_trades: int,
data: Dict[str, List[Tuple]]) -> List[Trade]: open_trade_count: int) -> Optional[LocalTrade]:
try:
stake_amount = self.wallets.get_trade_stake_amount(
pair, max_open_trades - open_trade_count, None)
except DependencyException:
return None
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['sell']
# Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX],
time_in_force=time_in_force):
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
# Enter trade
trade = LocalTrade(
pair=pair,
open_rate=row[OPEN_IDX],
open_date=row[DATE_IDX],
stake_amount=stake_amount,
amount=round(stake_amount / row[OPEN_IDX], 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
exchange='backtesting',
)
return trade
return None
def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
data: Dict[str, List[Tuple]]) -> List[LocalTrade]:
""" """
Handling of left open trades at the end of backtesting Handling of left open trades at the end of backtesting
""" """
@ -274,13 +318,16 @@ class Backtesting:
sell_row = data[pair][-1] sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX] trade.close_date = sell_row[DATE_IDX]
trade.sell_reason = SellType.FORCE_SELL trade.sell_reason = SellType.FORCE_SELL.value
trade.close(sell_row[OPEN_IDX], show_msg=False) trade.close(sell_row[OPEN_IDX], show_msg=False)
trade.is_open = True LocalTrade.close_bt_trade(trade)
trades.append(trade) # Deepcopy object to have wallets update correctly
trade1 = deepcopy(trade)
trade1.is_open = True
trades.append(trade1)
return trades return trades
def backtest(self, processed: Dict, stake_amount: float, def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0, position_stacking: bool = False, max_open_trades: int = 0, position_stacking: bool = False,
enable_protections: bool = False) -> DataFrame: enable_protections: bool = False) -> DataFrame:
@ -292,7 +339,6 @@ class Backtesting:
Avoid extensive logging in this method and functions it calls. Avoid extensive logging in this method and functions it calls.
:param processed: a processed dictionary with format {pair, data} :param processed: a processed dictionary with format {pair, data}
:param stake_amount: amount to use for each trade
:param start_date: backtesting timerange start datetime :param start_date: backtesting timerange start datetime
:param end_date: backtesting timerange end datetime :param end_date: backtesting timerange end datetime
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
@ -300,11 +346,7 @@ class Backtesting:
:param enable_protections: Should protections be enabled? :param enable_protections: Should protections be enabled?
:return: DataFrame with trades (results of backtesting) :return: DataFrame with trades (results of backtesting)
""" """
logger.debug(f"Run backtest, stake_amount: {stake_amount}, " trades: List[LocalTrade] = []
f"start_date: {start_date}, end_date: {end_date}, "
f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}"
)
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
@ -315,7 +357,7 @@ class Backtesting:
indexes: Dict = {} indexes: Dict = {}
tmp = start_date + timedelta(minutes=self.timeframe_min) tmp = start_date + timedelta(minutes=self.timeframe_min)
open_trades: Dict[str, List] = defaultdict(list) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0 open_trade_count = 0
# Loop timerange and get candle for each pair at that point in time # Loop timerange and get candle for each pair at that point in time
@ -346,28 +388,18 @@ class Backtesting:
and tmp != end_date and tmp != end_date
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
# Enter trade trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start)
trade = Trade( if trade:
pair=pair, # TODO: hacky workaround to avoid opening > max_open_trades
open_rate=row[OPEN_IDX], # This emulates previous behaviour - not sure if this is correct
open_date=row[DATE_IDX], # Prevents buying if the trade-slot was freed in this candle
stake_amount=stake_amount, open_trade_count_start += 1
amount=round(stake_amount / row[OPEN_IDX], 8), open_trade_count += 1
fee_open=self.fee, # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
fee_close=self.fee, open_trades[pair].append(trade)
is_open=True, LocalTrade.add_bt_trade(trade)
)
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct
# Prevents buying if the trade-slot was freed in this candle
open_trade_count_start += 1
open_trade_count += 1
# logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
open_trades[pair].append(trade)
Trade.trades.append(trade)
for trade in open_trades[pair]: for trade in open_trades[pair]:
# since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions. # also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(trade, row) trade_entry = self._get_sell_trade_entry(trade, row)
# Sell occured # Sell occured
@ -375,6 +407,8 @@ class Backtesting:
# logger.debug(f"{pair} - Backtesting sell {trade}") # logger.debug(f"{pair} - Backtesting sell {trade}")
open_trade_count -= 1 open_trade_count -= 1
open_trades[pair].remove(trade) open_trades[pair].remove(trade)
LocalTrade.close_bt_trade(trade)
trades.append(trade_entry) trades.append(trade_entry)
if enable_protections: if enable_protections:
self.protections.stop_per_pair(pair, row[DATE_IDX]) self.protections.stop_per_pair(pair, row[DATE_IDX])
@ -384,6 +418,7 @@ class Backtesting:
tmp += timedelta(minutes=self.timeframe_min) tmp += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data) trades += self.handle_left_open(open_trades, data=data)
self.wallets.update()
return trade_list_to_dataframe(trades) return trade_list_to_dataframe(trades)
@ -417,7 +452,6 @@ class Backtesting:
# Execute backtest and store results # Execute backtest and store results
results = self.backtest( results = self.backtest(
processed=preprocessed, processed=preprocessed,
stake_amount=self.config['stake_amount'],
start_date=min_date.datetime, start_date=min_date.datetime,
end_date=max_date.datetime, end_date=max_date.datetime,
max_open_trades=max_open_trades, max_open_trades=max_open_trades,
@ -428,7 +462,8 @@ class Backtesting:
self.all_results[self.strategy.get_strategy_name()] = { self.all_results[self.strategy.get_strategy_name()] = {
'results': results, 'results': results,
'config': self.strategy.config, 'config': self.strategy.config,
'locks': PairLocks.locks, 'locks': PairLocks.get_all_locks(),
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_start_time': int(backtest_start_time.timestamp()),
'backtest_end_time': int(backtest_end_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()),
} }
@ -443,16 +478,14 @@ class Backtesting:
data, timerange = self.load_bt_data() data, timerange = self.load_bt_data()
min_date = None
max_date = None
for strat in self.strategylist: for strat in self.strategylist:
min_date, max_date = self.backtest_one_strategy(strat, data, timerange) min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
if len(self.strategylist) > 0:
stats = generate_backtest_stats(data, self.all_results,
min_date=min_date, max_date=max_date)
stats = generate_backtest_stats(data, self.all_results, if self.config.get('export', False):
min_date=min_date, max_date=max_date) store_backtest_stats(self.config['exportfilename'], stats)
if self.config.get('export', False): # Show backtest results
store_backtest_stats(self.config['exportfilename'], stats) show_backtest_results(self.config, stats)
# Show backtest results
show_backtest_results(self.config, stats)

View File

@ -4,36 +4,31 @@
This module contains the hyperopt logic This module contains the hyperopt logic
""" """
import io
import locale import locale
import logging import logging
import random import random
import warnings import warnings
from collections import OrderedDict
from datetime import datetime from datetime import datetime
from math import ceil from math import ceil
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from pprint import pformat
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import progressbar import progressbar
import rapidjson
import tabulate
from colorama import Fore, Style from colorama import Fore, Style
from colorama import init as colorama_init from colorama import init as colorama_init
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
from pandas import DataFrame, isna, json_normalize from pandas import DataFrame
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.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural
from freqtrade.misc import file_dump_json, plural, round_dict
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
@ -73,12 +68,15 @@ class Hyperopt:
self.backtesting = Backtesting(self.config) self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.custom_hyperopt.__class__.strategy = self.backtesting.strategy
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
strategy = str(self.config['strategy'])
self.results_file = (self.config['user_data_dir'] / self.results_file = (self.config['user_data_dir'] /
'hyperopt_results' / f'hyperopt_results_{time_now}.pickle') 'hyperopt_results' /
f'strategy_{strategy}_hyperopt_results_{time_now}.pickle')
self.data_pickle_file = (self.config['user_data_dir'] / self.data_pickle_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl') 'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0) self.total_epochs = config.get('epochs', 0)
@ -166,15 +164,6 @@ class Hyperopt:
file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)},
log=False) log=False)
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
"""
logger.info("Reading epochs from '%s'", results_file)
data = load(results_file)
return data
def _get_params_details(self, params: Dict) -> Dict: def _get_params_details(self, params: Dict) -> Dict:
""" """
Return the params for each space Return the params for each space
@ -197,102 +186,16 @@ class Hyperopt:
return result return result
@staticmethod
def print_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
"""
Display details of the hyperopt result
"""
params = results.get('params_details', {})
# Default header string
if header_str is None:
header_str = "Best result"
if not no_header:
explanation_str = Hyperopt._format_explanation_string(results, total_epochs)
print(f"\n{header_str}:\n\n{explanation_str}\n")
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
Hyperopt._params_update_for_json(result_dict, params, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
Hyperopt._params_pretty_print(params, 'buy', "Buy hyperspace params:")
Hyperopt._params_pretty_print(params, 'sell', "Sell hyperspace params:")
Hyperopt._params_pretty_print(params, 'roi', "ROI table:")
Hyperopt._params_pretty_print(params, 'stoploss', "Stoploss:")
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
def _params_update_for_json(result_dict, params, space: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space)
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
# in the dict.
result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in space_params.items()
)
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space, 5)
params_result = f"\n# {header}\n"
if space == 'stoploss':
params_result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
minimal_roi_result = rapidjson.dumps(
OrderedDict(
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
params_result += f'{k} = {v}\n'
else:
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
params_result = params_result.replace("\n", "\n ")
print(params_result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
@staticmethod
def is_best_loss(results, current_best_loss: float) -> bool:
return results['loss'] < current_best_loss
def print_results(self, results) -> None: def print_results(self, results) -> None:
""" """
Log results if it is better than any previous evaluation Log results if it is better than any previous evaluation
TODO: this should be moved to HyperoptTools too
""" """
is_best = results['is_best'] is_best = results['is_best']
if self.print_all or is_best: if self.print_all or is_best:
print( print(
self.get_result_table( HyperoptTools.get_result_table(
self.config, results, self.total_epochs, self.config, results, self.total_epochs,
self.print_all, self.print_colorized, self.print_all, self.print_colorized,
self.hyperopt_table_header self.hyperopt_table_header
@ -300,164 +203,6 @@ class Hyperopt:
) )
self.hyperopt_table_header = 2 self.hyperopt_table_header = 2
@staticmethod
def _format_explanation_string(results, total_epochs) -> str:
return (("*" if results['is_initial_point'] else " ") +
f"{results['current_epoch']:5d}/{total_epochs}: " +
f"{results['results_explanation']} " +
f"Objective: {results['loss']:.5f}")
@staticmethod
def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
print_colorized: bool, remove_header: int) -> str:
"""
Log result table
"""
if not results:
return ''
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
if 'results_metrics.winsdrawslosses' not in trials.columns:
# Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A'
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '* '
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str)
trials['Epoch'] = trials['Epoch'].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
axis=1
)
trials = trials.drop(columns=['Total profit'])
if print_colorized:
for i in range(len(trials)):
if trials.loc[i]['is_profit']:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
if remove_header > 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='orgtbl',
headers='keys', stralign="right"
)
table = table.split("\n", remove_header)[remove_header]
elif remove_header < 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
table = "\n".join(table.split("\n")[0:remove_header])
else:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
return table
@staticmethod
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
csv_file: str) -> None:
"""
Log result to csv-file
"""
if not results:
return
# Verification for overwrite
if Path(csv_file).is_file():
logger.error(f"CSV file already exists: {csv_file}")
return
try:
io.open(csv_file, 'w+').close()
except IOError:
logger.error(f"Failed to create CSV file: {csv_file}")
return
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit', 'Stake currency',
'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Epoch'] = trials['Epoch'].astype(str)
trials['Trades'] = trials['Trades'].astype(str)
trials['Total profit'] = trials['Total profit'].apply(
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
)
trials['Profit'] = trials['Profit'].apply(
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
logger.info(f"CSV file created: {csv_file}")
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
Tell if the space value is contained in the configuration Tell if the space value is contained in the configuration
@ -537,7 +282,6 @@ class Hyperopt:
backtesting_results = self.backtesting.backtest( backtesting_results = self.backtesting.backtest(
processed=processed, processed=processed,
stake_amount=self.config['stake_amount'],
start_date=min_date.datetime, start_date=min_date.datetime,
end_date=max_date.datetime, end_date=max_date.datetime,
max_open_trades=self.max_open_trades, max_open_trades=self.max_open_trades,
@ -622,22 +366,6 @@ class Hyperopt:
return parallel(delayed( return parallel(delayed(
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked) wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
@staticmethod
def load_previous_results(results_file: Path) -> List:
"""
Load data for epochs from the file if we have one
"""
epochs: List = []
if results_file.is_file() and results_file.stat().st_size > 0:
epochs = Hyperopt._read_results(results_file)
# Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
raise OperationalException(
"The file with Hyperopt results is incompatible with this version "
"of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
return epochs
def _set_random_state(self, random_state: Optional[int]) -> int: def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1) return random_state or random.randint(1, 2**16 - 1)
@ -661,7 +389,10 @@ class Hyperopt:
dump(preprocessed, self.data_pickle_file) dump(preprocessed, self.data_pickle_file)
# We don't need exchange instance anymore while running hyperopt # We don't need exchange instance anymore while running hyperopt
self.backtesting.exchange = None # type: ignore self.backtesting.exchange.close()
self.backtesting.exchange._api = None # type: ignore
self.backtesting.exchange._api_async = None # type: ignore
# self.backtesting.exchange = None # type: ignore
self.backtesting.pairlists = None # type: ignore self.backtesting.pairlists = None # type: ignore
self.backtesting.strategy.dp = None # type: ignore self.backtesting.strategy.dp = None # type: ignore
IStrategy.dp = None # type: ignore IStrategy.dp = None # type: ignore
@ -727,7 +458,7 @@ class Hyperopt:
logger.debug(f"Optimizer epoch evaluated: {val}") logger.debug(f"Optimizer epoch evaluated: {val}")
is_best = self.is_best_loss(val, self.current_best_loss) is_best = HyperoptTools.is_best_loss(val, self.current_best_loss)
# This value is assigned here and not in the optimization method # This value is assigned here and not in the optimization method
# to keep proper order in the list of results. That's because # to keep proper order in the list of results. That's because
# evaluations can take different time. Here they are aligned in the # evaluations can take different time. Here they are aligned in the
@ -755,7 +486,7 @@ class Hyperopt:
if self.epochs: if self.epochs:
sorted_epochs = sorted(self.epochs, key=itemgetter('loss')) sorted_epochs = sorted(self.epochs, key=itemgetter('loss'))
best_epoch = sorted_epochs[0] best_epoch = sorted_epochs[0]
self.print_epoch_details(best_epoch, self.total_epochs, self.print_json) HyperoptTools.print_epoch_details(best_epoch, self.total_epochs, self.print_json)
else: else:
# This is printed when Ctrl+C is pressed quickly, before first epochs have # This is printed when Ctrl+C is pressed quickly, before first epochs have
# a chance to be evaluated. # a chance to be evaluated.

View File

@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict from freqtrade.misc import round_dict
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -34,6 +35,7 @@ class IHyperOpt(ABC):
""" """
ticker_interval: str # DEPRECATED ticker_interval: str # DEPRECATED
timeframe: str timeframe: str
strategy: IStrategy
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
self.config = config self.config = config

View File

@ -0,0 +1,294 @@
import io
import logging
from collections import OrderedDict
from pathlib import Path
from pprint import pformat
from typing import Dict, List
import rapidjson
import tabulate
from colorama import Fore, Style
from joblib import load
from pandas import isna, json_normalize
from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_dict
logger = logging.getLogger(__name__)
class HyperoptTools():
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
"""
logger.info("Reading epochs from '%s'", results_file)
data = load(results_file)
return data
@staticmethod
def load_previous_results(results_file: Path) -> List:
"""
Load data for epochs from the file if we have one
"""
epochs: List = []
if results_file.is_file() and results_file.stat().st_size > 0:
epochs = HyperoptTools._read_results(results_file)
# Detection of some old format, without 'is_best' field saved
if epochs[0].get('is_best') is None:
raise OperationalException(
"The file with HyperoptTools results is incompatible with this version "
"of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.")
return epochs
@staticmethod
def print_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
"""
Display details of the hyperopt result
"""
params = results.get('params_details', {})
# Default header string
if header_str is None:
header_str = "Best result"
if not no_header:
explanation_str = HyperoptTools._format_explanation_string(results, total_epochs)
print(f"\n{header_str}:\n\n{explanation_str}\n")
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
HyperoptTools._params_update_for_json(result_dict, params, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:")
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:")
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
def _params_update_for_json(result_dict, params, space: str) -> None:
if space in params:
space_params = HyperoptTools._space_params(params, space)
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
# in the dict.
result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in space_params.items()
)
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
space_params = HyperoptTools._space_params(params, space, 5)
params_result = f"\n# {header}\n"
if space == 'stoploss':
params_result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
minimal_roi_result = rapidjson.dumps(
OrderedDict(
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}"
elif space == 'trailing':
for k, v in space_params.items():
params_result += f'{k} = {v}\n'
else:
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
params_result = params_result.replace("\n", "\n ")
print(params_result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
d = params[space]
# Round floats to `r` digits after the decimal point if requested
return round_dict(d, r) if r else d
@staticmethod
def is_best_loss(results, current_best_loss: float) -> bool:
return results['loss'] < current_best_loss
@staticmethod
def _format_explanation_string(results, total_epochs) -> str:
return (("*" if results['is_initial_point'] else " ") +
f"{results['current_epoch']:5d}/{total_epochs}: " +
f"{results['results_explanation']} " +
f"Objective: {results['loss']:.5f}")
@staticmethod
def get_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
print_colorized: bool, remove_header: int) -> str:
"""
Log result table
"""
if not results:
return ''
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
if 'results_metrics.winsdrawslosses' not in trials.columns:
# Ensure compatibility with older versions of hyperopt results
trials['results_metrics.winsdrawslosses'] = 'N/A'
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.winsdrawslosses',
'results_metrics.avg_profit', 'results_metrics.total_profit',
'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']]
trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit',
'Total profit', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '* '
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str)
trials['Epoch'] = trials['Epoch'].apply(
lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
axis=1
)
trials = trials.drop(columns=['Total profit'])
if print_colorized:
for i in range(len(trials)):
if trials.loc[i]['is_profit']:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best:
for j in range(len(trials.loc[i])-3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
if remove_header > 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='orgtbl',
headers='keys', stralign="right"
)
table = table.split("\n", remove_header)[remove_header]
elif remove_header < 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
table = "\n".join(table.split("\n")[0:remove_header])
else:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
return table
@staticmethod
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
csv_file: str) -> None:
"""
Log result to csv-file
"""
if not results:
return
# Verification for overwrite
if Path(csv_file).is_file():
logger.error(f"CSV file already exists: {csv_file}")
return
try:
io.open(csv_file, 'w+').close()
except IOError:
logger.error(f"Failed to create CSV file: {csv_file}")
return
trials = json_normalize(results, max_level=1)
trials['Best'] = ''
trials['Stake currency'] = config['stake_currency']
base_metrics = ['Best', 'current_epoch', 'results_metrics.trade_count',
'results_metrics.avg_profit', 'results_metrics.median_profit',
'results_metrics.total_profit',
'Stake currency', 'results_metrics.profit', 'results_metrics.duration',
'loss', 'is_initial_point', 'is_best']
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
'Stake currency', 'Profit', 'Avg duration', 'Objective',
'is_initial_point', 'is_best']
param_columns = list(results[0]['params_dict'].keys())
trials.columns = base_columns + param_columns
trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best'
trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Epoch'] = trials['Epoch'].astype(str)
trials['Trades'] = trials['Trades'].astype(str)
trials['Total profit'] = trials['Total profit'].apply(
lambda x: '{:,.8f}'.format(x) if x != 0.0 else ""
)
trials['Profit'] = trials['Profit'].apply(
lambda x: '{:,.2f}'.format(x) if not isna(x) else ""
)
trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x) if not isna(x) else ""
)
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f} m'.format(x) if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x) if x != 100000 else ""
)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
logger.info(f"CSV file created: {csv_file}")

View File

@ -8,7 +8,7 @@ from numpy import int64
from pandas import DataFrame 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, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change, from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
calculate_max_drawdown) calculate_max_drawdown)
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
@ -56,12 +56,13 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
'Wins', 'Draws', 'Losses'] 'Wins', 'Draws', 'Losses']
def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: str) -> Dict: def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
""" """
Generate one result dict, with "first_column" as key. Generate one result dict, with "first_column" as key.
""" """
profit_sum = result['profit_ratio'].sum() profit_sum = result['profit_ratio'].sum()
profit_total = profit_sum / max_open_trades # (end-capital - starting capital) / starting capital
profit_total = result['profit_abs'].sum() / starting_balance
return { return {
'key': first_column, 'key': first_column,
@ -88,13 +89,13 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
} }
def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_balance: int,
results: DataFrame, skip_nan: bool = False) -> List[Dict]: results: DataFrame, skip_nan: bool = False) -> List[Dict]:
""" """
Generates and returns a list for the given backtest data and the results dataframe Generates and returns a list for the given backtest data and the results dataframe
:param data: Dict of <pair: dataframe> containing data that was used during backtesting. :param data: Dict of <pair: dataframe> containing data that was used during backtesting.
: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 :param starting_balance: Starting balance
:param results: Dataframe containing the backtest results :param results: Dataframe containing the backtest results
:param skip_nan: Print "left open" open trades :param skip_nan: Print "left open" open trades
:return: List of Dicts containing the metrics per pair :return: List of Dicts containing the metrics per pair
@ -107,10 +108,10 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
if skip_nan and result['profit_abs'].isnull().all(): if skip_nan and result['profit_abs'].isnull().all():
continue continue
tabular_data.append(_generate_result_line(result, max_open_trades, pair)) tabular_data.append(_generate_result_line(result, starting_balance, pair))
# Append Total # Append Total
tabular_data.append(_generate_result_line(results, max_open_trades, 'TOTAL')) tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL'))
return tabular_data return tabular_data
@ -132,7 +133,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
tabular_data.append( tabular_data.append(
{ {
'sell_reason': reason.value, 'sell_reason': reason,
'trades': count, 'trades': count,
'wins': len(result[result['profit_abs'] > 0]), 'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]), 'draws': len(result[result['profit_abs'] == 0]),
@ -159,7 +160,7 @@ def generate_strategy_metrics(all_results: Dict) -> List[Dict]:
tabular_data = [] tabular_data = []
for strategy, results in all_results.items(): for strategy, results in all_results.items():
tabular_data.append(_generate_result_line( tabular_data.append(_generate_result_line(
results['results'], results['config']['max_open_trades'], strategy) results['results'], results['config']['dry_run_wallet'], strategy)
) )
return tabular_data return tabular_data
@ -195,13 +196,18 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
return { return {
'backtest_best_day': 0, 'backtest_best_day': 0,
'backtest_worst_day': 0, 'backtest_worst_day': 0,
'backtest_best_day_abs': 0,
'backtest_worst_day_abs': 0,
'winning_days': 0, 'winning_days': 0,
'draw_days': 0, 'draw_days': 0,
'losing_days': 0, 'losing_days': 0,
'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_ratio'].sum() daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
worst_rel = min(daily_profit_rel)
best_rel = max(daily_profit_rel)
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)
@ -212,8 +218,10 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
losing_trades = results.loc[results['profit_ratio'] < 0] losing_trades = results.loc[results['profit_ratio'] < 0]
return { return {
'backtest_best_day': best, 'backtest_best_day': best_rel,
'backtest_worst_day': worst, 'backtest_worst_day': worst_rel,
'backtest_best_day_abs': best,
'backtest_worst_day_abs': worst,
'winning_days': winning_days, 'winning_days': winning_days,
'draw_days': draw_days, 'draw_days': draw_days,
'losing_days': losing_days, 'losing_days': losing_days,
@ -246,15 +254,16 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
continue continue
config = content['config'] config = content['config']
max_open_trades = min(config['max_open_trades'], len(btdata.keys())) max_open_trades = min(config['max_open_trades'], len(btdata.keys()))
starting_balance = config['dry_run_wallet']
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,
max_open_trades=max_open_trades, starting_balance=starting_balance,
results=results, skip_nan=False) results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
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, starting_balance=starting_balance,
results=results.loc[results['is_open']], results=results.loc[results['is_open']],
skip_nan=True) skip_nan=True)
daily_stats = generate_daily_stats(results) daily_stats = generate_daily_stats(results)
@ -275,8 +284,10 @@ 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),
'total_volume': float(results['stake_amount'].sum()),
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0, 'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_total': results['profit_ratio'].sum() / max_open_trades, 'profit_total': results['profit_abs'].sum() / starting_balance,
'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,
@ -292,6 +303,10 @@ 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'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'starting_balance': starting_balance,
'dry_run_wallet': starting_balance,
'final_balance': content['final_balance'],
'max_open_trades': max_open_trades, 'max_open_trades': max_open_trades,
'max_open_trades_setting': (config['max_open_trades'] 'max_open_trades_setting': (config['max_open_trades']
if config['max_open_trades'] != float('inf') else -1), if config['max_open_trades'] != float('inf') else -1),
@ -316,17 +331,23 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
result['strategy'][strategy] = strat_stats result['strategy'][strategy] = strat_stats
try: try:
max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( max_drawdown, _, _, _, _ = calculate_max_drawdown(
results, value_col='profit_ratio') results, value_col='profit_ratio')
drawdown_abs, drawdown_start, drawdown_end, high_val, low_val = calculate_max_drawdown(
results, value_col='profit_abs')
strat_stats.update({ strat_stats.update({
'max_drawdown': max_drawdown, 'max_drawdown': max_drawdown,
'max_drawdown_abs': drawdown_abs,
'drawdown_start': drawdown_start, 'drawdown_start': drawdown_start,
'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_start_ts': drawdown_start.timestamp() * 1000,
'drawdown_end': drawdown_end, 'drawdown_end': drawdown_end,
'drawdown_end_ts': drawdown_end.timestamp() * 1000, 'drawdown_end_ts': drawdown_end.timestamp() * 1000,
'max_drawdown_low': low_val,
'max_drawdown_high': high_val,
}) })
csum_min, csum_max = calculate_csum(results) csum_min, csum_max = calculate_csum(results, starting_balance)
strat_stats.update({ strat_stats.update({
'csum_min': csum_min, 'csum_min': csum_min,
'csum_max': csum_max 'csum_max': csum_max
@ -335,6 +356,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
except ValueError: except ValueError:
strat_stats.update({ strat_stats.update({
'max_drawdown': 0.0, 'max_drawdown': 0.0,
'max_drawdown_abs': 0.0,
'max_drawdown_low': 0.0,
'max_drawdown_high': 0.0,
'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc),
'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),
@ -431,8 +455,19 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Max open trades', strat_results['max_open_trades']), ('Max open trades', strat_results['max_open_trades']),
('', ''), # Empty line to improve readability ('', ''), # Empty line to improve readability
('Total trades', strat_results['total_trades']), ('Total trades', strat_results['total_trades']),
('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Starting balance', round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])),
('Final balance', round_coin_value(strat_results['final_balance'],
strat_results['stake_currency'])),
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
('Trades per day', strat_results['trades_per_day']), ('Trades per day', strat_results['trades_per_day']),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],
strat_results['stake_currency'])),
('', ''), # Empty line to improve readability ('', ''), # Empty line to improve readability
('Best Pair', f"{strat_results['best_pair']['key']} " ('Best Pair', f"{strat_results['best_pair']['key']} "
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
@ -442,20 +477,28 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Worst trade', f"{worst_trade['pair']} " ('Worst trade', f"{worst_trade['pair']} "
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"), f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), strat_results['stake_currency'])),
('Worst day', round_coin_value(strat_results['backtest_worst_day_abs'],
strat_results['stake_currency'])),
('Days win/draw/lose', f"{strat_results['winning_days']} / " ('Days win/draw/lose', f"{strat_results['winning_days']} / "
f"{strat_results['draw_days']} / {strat_results['losing_days']}"), f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('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'], ('Min balance', round_coin_value(strat_results['csum_min'],
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Abs Profit Max', round_coin_value(strat_results['csum_max'], ('Max balance', round_coin_value(strat_results['csum_max'],
strat_results['stake_currency'])), strat_results['stake_currency'])),
('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
strat_results['stake_currency'])),
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
strat_results['stake_currency'])),
('Drawdown low', round_coin_value(strat_results['max_drawdown_low'],
strat_results['stake_currency'])),
('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)),
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
@ -463,7 +506,17 @@ def text_table_add_metrics(strat_results: Dict) -> str:
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
else: else:
return '' start_balance = round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])
stake_amount = round_coin_value(
strat_results['stake_amount'], strat_results['stake_currency']
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
message = ("No trades made. "
f"Your starting balance was {start_balance}, "
f"and your stake was {stake_amount}."
)
return message
def show_backtest_results(config: Dict, backtest_stats: Dict): def show_backtest_results(config: Dict, backtest_stats: Dict):

View File

@ -1,4 +1,5 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db from freqtrade.persistence.models import (LocalTrade, Order, Trade, clean_dry_run_db, cleanup_db,
init_db)
from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.pairlock_middleware import PairLocks

View File

@ -141,7 +141,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
if 'orders' not in previous_tables: if 'orders' not in previous_tables and 'trades' in previous_tables:
logger.info('Moving open orders to Orders table.') logger.info('Moving open orders to Orders table.')
migrate_open_orders_to_trades(engine) migrate_open_orders_to_trades(engine)
else: else:

View File

@ -199,67 +199,69 @@ class Order(_DECL_BASE):
return Order.query.filter(Order.ft_is_open.is_(True)).all() return Order.query.filter(Order.ft_is_open.is_(True)).all()
class Trade(_DECL_BASE): class LocalTrade():
""" """
Trade database model. Trade database model.
Also handles updating and querying trades Used in backtesting - must be aligned to Trade model!
""" """
__tablename__ = 'trades' use_db: bool = False
use_db: bool = True
# Trades container for backtesting # Trades container for backtesting
trades: List['Trade'] = [] trades: List['LocalTrade'] = []
trades_open: List['LocalTrade'] = []
total_profit: float = 0
id = Column(Integer, primary_key=True) id: int = 0
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") orders: List[Order] = []
exchange = Column(String, nullable=False) exchange: str = ''
pair = Column(String, nullable=False, index=True) pair: str = ''
is_open = Column(Boolean, nullable=False, default=True, index=True) is_open: bool = True
fee_open = Column(Float, nullable=False, default=0.0) fee_open: float = 0.0
fee_open_cost = Column(Float, nullable=True) fee_open_cost: Optional[float] = None
fee_open_currency = Column(String, nullable=True) fee_open_currency: str = ''
fee_close = Column(Float, nullable=False, default=0.0) fee_close: float = 0.0
fee_close_cost = Column(Float, nullable=True) fee_close_cost: Optional[float] = None
fee_close_currency = Column(String, nullable=True) fee_close_currency: str = ''
open_rate = Column(Float) open_rate: float = 0.0
open_rate_requested = Column(Float) open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float) open_trade_value: float = 0.0
close_rate = Column(Float) close_rate: Optional[float] = None
close_rate_requested = Column(Float) close_rate_requested: Optional[float] = None
close_profit = Column(Float) close_profit: Optional[float] = None
close_profit_abs = Column(Float) close_profit_abs: Optional[float] = None
stake_amount = Column(Float, nullable=False) stake_amount: float = 0.0
amount = Column(Float) amount: float = 0.0
amount_requested = Column(Float) amount_requested: Optional[float] = None
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date: datetime
close_date = Column(DateTime) close_date: Optional[datetime] = None
open_order_id = Column(String) open_order_id: Optional[str] = None
# absolute value of the stop loss # absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0) stop_loss: float = 0.0
# percentage value of the stop loss # percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True) stop_loss_pct: float = 0.0
# absolute value of the initial stop loss # absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0) initial_stop_loss: float = 0.0
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True) initial_stop_loss_pct: float = 0.0
# stoploss order id which is on exchange # stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True) stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange # last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True) stoploss_last_update: Optional[datetime] = None
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0) max_rate: float = 0.0
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate: float = 0.0
sell_reason = Column(String, nullable=True) sell_reason: str = ''
sell_order_status = Column(String, nullable=True) sell_order_status: str = ''
strategy = Column(String, nullable=True) strategy: str = ''
timeframe = Column(Integer, nullable=True) timeframe: Optional[int] = None
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) for key in kwargs:
setattr(self, key, kwargs[key])
self.recalc_open_trade_value() self.recalc_open_trade_value()
def __repr__(self): def __repr__(self):
@ -268,6 +270,14 @@ class Trade(_DECL_BASE):
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})') f'open_rate={self.open_rate:.8f}, open_since={open_since})')
@property
def open_date_utc(self):
return self.open_date.replace(tzinfo=timezone.utc)
@property
def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc)
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
return { return {
'trade_id': self.id, 'trade_id': self.id,
@ -306,9 +316,9 @@ 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()) 'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds())
if self.close_date else None), if self.close_date else None),
'trade_duration': (int((self.close_date - self.open_date).total_seconds() // 60) 'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60)
if self.close_date else None), if self.close_date else None),
'profit_ratio': self.close_profit, 'profit_ratio': self.close_profit,
@ -341,8 +351,9 @@ class Trade(_DECL_BASE):
""" """
Resets all trades. Only active for backtesting mode. Resets all trades. Only active for backtesting mode.
""" """
if not Trade.use_db: LocalTrade.trades = []
Trade.trades = [] LocalTrade.trades_open = []
LocalTrade.total_profit = 0
def adjust_min_max_rates(self, current_price: float) -> None: def adjust_min_max_rates(self, current_price: float) -> None:
""" """
@ -410,8 +421,8 @@ class Trade(_DECL_BASE):
if order_type in ('market', 'limit') and order['side'] == 'buy': if order_type in ('market', 'limit') and order['side'] == 'buy':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.')
@ -425,7 +436,7 @@ class Trade(_DECL_BASE):
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.') logger.info(f'{order_type.upper()} is hit for {self}.')
self.close(order['average']) self.close(safe_value_fallback(order, 'average', 'price'))
else: else:
raise ValueError(f'Unknown order type: {order_type}') raise ValueError(f'Unknown order type: {order_type}')
cleanup_db() cleanup_db()
@ -435,7 +446,7 @@ class Trade(_DECL_BASE):
Sets close_rate to the given rate, calculates total profit Sets close_rate to the given rate, calculates total profit
and marks trade as closed and marks trade as closed
""" """
self.close_rate = Decimal(rate) self.close_rate = rate
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow() self.close_date = self.close_date or datetime.utcnow()
@ -480,14 +491,6 @@ class Trade(_DECL_BASE):
def update_order(self, order: Dict) -> None: def update_order(self, order: Dict) -> None:
Order.update_orders(self.orders, order) Order.update_orders(self.orders, order)
def delete(self) -> None:
for order in self.orders:
Order.session.delete(order)
Trade.session.delete(self)
Trade.session.flush()
def _calc_open_trade_value(self) -> float: def _calc_open_trade_value(self) -> float:
""" """
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.
@ -517,7 +520,7 @@ class Trade(_DECL_BASE):
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = sell_trade * Decimal(fee or self.fee_close) fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees) return float(sell_trade - fees)
@ -589,7 +592,7 @@ class Trade(_DECL_BASE):
@staticmethod @staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None, def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None, open_date: datetime = None, close_date: datetime = None,
) -> List['Trade']: ) -> List['LocalTrade']:
""" """
Helper function to query Trades. Helper function to query Trades.
Returns a List of trades, filtered on the parameters given. Returns a List of trades, filtered on the parameters given.
@ -598,30 +601,40 @@ class Trade(_DECL_BASE):
:return: unsorted List[Trade] :return: unsorted List[Trade]
""" """
if Trade.use_db:
trade_filter = [] # Offline mode - without database
if pair: if is_open is not None:
trade_filter.append(Trade.pair == pair) if is_open:
if open_date: sel_trades = LocalTrade.trades_open
trade_filter.append(Trade.open_date > open_date) else:
if close_date: sel_trades = LocalTrade.trades
trade_filter.append(Trade.close_date > close_date)
if is_open is not None:
trade_filter.append(Trade.is_open.is_(is_open))
return Trade.get_trades(trade_filter).all()
else: else:
# Offline mode - without database # Not used during backtesting, but might be used by a strategy
sel_trades = [trade for trade in Trade.trades] sel_trades = [trade for trade in LocalTrade.trades + LocalTrade.trades_open]
if pair:
sel_trades = [trade for trade in sel_trades if trade.pair == pair] if pair:
if open_date: sel_trades = [trade for trade in sel_trades if trade.pair == pair]
sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] if open_date:
if close_date: sel_trades = [trade for trade in sel_trades if trade.open_date > open_date]
sel_trades = [trade for trade in sel_trades if trade.close_date if close_date:
and trade.close_date > close_date] sel_trades = [trade for trade in sel_trades if trade.close_date
if is_open is not None: and trade.close_date > close_date]
sel_trades = [trade for trade in sel_trades if trade.is_open == is_open]
return sel_trades return sel_trades
@staticmethod
def close_bt_trade(trade):
LocalTrade.trades_open.remove(trade)
LocalTrade.trades.append(trade)
LocalTrade.total_profit += trade.close_profit_abs
@staticmethod
def add_bt_trade(trade):
if trade.is_open:
LocalTrade.trades_open.append(trade)
else:
LocalTrade.trades.append(trade)
@staticmethod @staticmethod
def get_open_trades() -> List[Any]: def get_open_trades() -> List[Any]:
@ -663,9 +676,12 @@ class Trade(_DECL_BASE):
Calculates total invested amount in open trades Calculates total invested amount in open trades
in stake currency in stake currency
""" """
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ if Trade.use_db:
.filter(Trade.is_open.is_(True))\ total_open_stake_amount = Trade.session.query(
.scalar() func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar()
else:
total_open_stake_amount = sum(
t.stake_amount for t in Trade.get_trades_proxy(is_open=True))
return total_open_stake_amount or 0 return total_open_stake_amount or 0
@staticmethod @staticmethod
@ -723,6 +739,108 @@ class Trade(_DECL_BASE):
logger.info(f"New stoploss: {trade.stop_loss}.") logger.info(f"New stoploss: {trade.stop_loss}.")
class Trade(_DECL_BASE, LocalTrade):
"""
Trade database model.
Also handles updating and querying trades
Note: Fields must be aligned with LocalTrade class
"""
__tablename__ = 'trades'
use_db: bool = True
id = Column(Integer, primary_key=True)
orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open_currency = Column(String, nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close_currency = Column(String, nullable=True)
open_rate = Column(Float)
open_rate_requested = Column(Float)
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
amount_requested = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True)
# stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True)
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached
min_rate = Column(Float, nullable=True)
sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True)
timeframe = Column(Integer, nullable=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_value()
def delete(self) -> None:
for order in self.orders:
Order.session.delete(order)
Trade.session.delete(self)
Trade.session.flush()
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
) -> List['LocalTrade']:
"""
Helper function to query Trades.
Returns a List of trades, filtered on the parameters given.
In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result.
:return: unsorted List[Trade]
"""
if Trade.use_db:
trade_filter = []
if pair:
trade_filter.append(Trade.pair == pair)
if open_date:
trade_filter.append(Trade.open_date > open_date)
if close_date:
trade_filter.append(Trade.close_date > close_date)
if is_open is not None:
trade_filter.append(Trade.is_open.is_(is_open))
return Trade.get_trades(trade_filter).all()
else:
return LocalTrade.get_trades_proxy(
pair=pair, is_open=is_open,
open_date=open_date,
close_date=close_date
)
class PairLock(_DECL_BASE): class PairLock(_DECL_BASE):
""" """
Pair Locks database model. Pair Locks database model.
@ -765,6 +883,7 @@ class PairLock(_DECL_BASE):
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
return { return {
'id': self.id,
'pair': self.pair, 'pair': self.pair,
'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT),
'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000),

View File

@ -123,3 +123,11 @@ class PairLocks():
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now) return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now)
@staticmethod
def get_all_locks() -> List[PairLock]:
if PairLocks.use_db:
return PairLock.query.all()
else:
return PairLocks.locks

View File

@ -145,7 +145,7 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
Add scatter points indicating max drawdown Add scatter points indicating max drawdown
""" """
try: try:
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades) max_drawdown, highdate, lowdate, _, _ = calculate_max_drawdown(trades)
drawdown = go.Scatter( drawdown = go.Scatter(
x=[highdate, lowdate], x=[highdate, lowdate],

View File

@ -64,7 +64,7 @@ class PriceFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker['last'] is None or ticker['last'] == 0: if ticker.get('last', None) is None or ticker.get('last') == 0:
self.log_once(f"Removed {pair} from whitelist, because " self.log_once(f"Removed {pair} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).", "ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info) logger.info)

View File

@ -28,7 +28,7 @@ class RangeStabilityFilter(IPairList):
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._refresh_period = pairlistconfig.get('refresh_period', 1440)
self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
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")

View File

@ -44,7 +44,8 @@ class CooldownPeriod(IProtection):
trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
if trades: if trades:
# Get latest trade # Get latest trade
trade = sorted(trades, key=lambda t: t.close_date)[-1] # Ignore type error as we know we only get closed trades.
trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
until = self.calculate_lock_end([trade], self._stop_duration) until = self.calculate_lock_end([trade], self._stop_duration)

View File

@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Trade from freqtrade.persistence import LocalTrade
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -93,11 +93,11 @@ class IProtection(LoggingMixin, ABC):
""" """
@staticmethod @staticmethod
def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime: def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime:
""" """
Get lock end time Get lock end time
""" """
max_date: datetime = max([trade.close_date for trade in trades]) max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
# comming from Database, tzinfo is not set. # comming from Database, tzinfo is not set.
if max_date.tzinfo is None: if max_date.tzinfo is None:
max_date = max_date.replace(tzinfo=timezone.utc) max_date = max_date.replace(tzinfo=timezone.utc)

View File

@ -53,7 +53,7 @@ class LowProfitPairs(IProtection):
# Not enough trades in the relevant period # Not enough trades in the relevant period
return False, None, None return False, None, None
profit = sum(trade.close_profit for trade in trades) profit = sum(trade.close_profit for trade in trades if trade.close_profit)
if profit < self._required_profit: if profit < self._required_profit:
self.log_once( self.log_once(
f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} "

View File

@ -55,7 +55,7 @@ class MaxDrawdown(IProtection):
# Drawdown is always positive # Drawdown is always positive
try: try:
drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') drawdown, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
except ValueError: except ValueError:
return False, None, None return False, None, None

View File

@ -56,7 +56,7 @@ class StoplossGuard(IProtection):
trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value) SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit < 0)] and trade.close_profit and trade.close_profit < 0)]
if len(trades) < self._trade_limit: if len(trades) < self._trade_limit:
return False, None, None return False, None, None

View File

@ -1,5 +1,5 @@
from datetime import date, datetime from datetime import date, datetime
from typing import Any, Dict, List, Optional, TypeVar, Union from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
@ -62,14 +62,12 @@ class PerformanceEntry(BaseModel):
class Profit(BaseModel): class Profit(BaseModel):
profit_closed_coin: float profit_closed_coin: float
profit_closed_percent: float
profit_closed_percent_mean: float profit_closed_percent_mean: float
profit_closed_ratio_mean: float profit_closed_ratio_mean: float
profit_closed_percent_sum: float profit_closed_percent_sum: float
profit_closed_ratio_sum: float profit_closed_ratio_sum: float
profit_closed_fiat: float profit_closed_fiat: float
profit_all_coin: float profit_all_coin: float
profit_all_percent: float
profit_all_percent_mean: float profit_all_percent_mean: float
profit_all_ratio_mean: float profit_all_ratio_mean: float
profit_all_percent_sum: float profit_all_percent_sum: float
@ -205,10 +203,12 @@ class TradeResponse(BaseModel):
trades_count: int trades_count: int
ForceBuyResponse = TypeVar('ForceBuyResponse', TradeSchema, StatusMsg) class ForceBuyResponse(BaseModel):
__root__: Union[TradeSchema, StatusMsg]
class LockModel(BaseModel): class LockModel(BaseModel):
id: int
active: bool active: bool
lock_end_time: str lock_end_time: str
lock_end_timestamp: int lock_end_timestamp: int
@ -223,6 +223,11 @@ class Locks(BaseModel):
locks: List[LockModel] locks: List[LockModel]
class DeleteLockRequest(BaseModel):
pair: Optional[str]
lockid: Optional[int]
class Logs(BaseModel): class Logs(BaseModel):
log_count: int log_count: int
logs: List[List] logs: List[List]
@ -267,7 +272,8 @@ class PlotConfig_(BaseModel):
subplots: Optional[Dict[str, Any]] subplots: Optional[Dict[str, Any]]
PlotConfig = TypeVar('PlotConfig', PlotConfig_, Dict) class PlotConfig(BaseModel):
__root__: Union[PlotConfig_, Dict]
class StrategyListResponse(BaseModel): class StrategyListResponse(BaseModel):

View File

@ -11,13 +11,14 @@ from freqtrade.data.history import get_datahandler
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, DeleteTrade, BlacklistResponse, Count, Daily,
ForceBuyPayload, ForceBuyResponse, DeleteLockRequest, DeleteTrade, ForceBuyPayload,
ForceSellPayload, Locks, Logs, OpenTradeSchema, ForceBuyResponse, ForceSellPayload, Locks, Logs,
PairHistory, PerformanceEntry, Ping, PlotConfig, OpenTradeSchema, PairHistory, PerformanceEntry,
Profit, ResultMsg, ShowConfig, Stats, StatusMsg, Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
StrategyListResponse, StrategyResponse, Stats, StatusMsg, StrategyListResponse,
TradeResponse, Version, WhitelistResponse) StrategyResponse, TradeResponse, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -111,9 +112,9 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
trade = rpc._rpc_forcebuy(payload.pair, payload.price) trade = rpc._rpc_forcebuy(payload.pair, payload.price)
if trade: if trade:
return trade.to_json() return ForceBuyResponse.parse_obj(trade.to_json())
else: else:
return {"status": f"Error buying pair {payload.pair}."} return ForceBuyResponse.parse_obj({"status": f"Error buying pair {payload.pair}."})
@router.post('/forcesell', response_model=ResultMsg, tags=['trading']) @router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
@ -136,11 +137,21 @@ def whitelist(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_whitelist() return rpc._rpc_whitelist()
@router.get('/locks', response_model=Locks, tags=['info']) @router.get('/locks', response_model=Locks, tags=['info', 'locks'])
def locks(rpc: RPC = Depends(get_rpc)): def locks(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_locks() return rpc._rpc_locks()
@router.delete('/locks/{lockid}', response_model=Locks, tags=['info', 'locks'])
def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=lockid)
@router.post('/locks/delete', response_model=Locks, tags=['info', 'locks'])
def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair)
@router.get('/logs', response_model=Logs, tags=['info']) @router.get('/logs', response_model=Logs, tags=['info'])
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_get_logs(limit) return rpc._rpc_get_logs(limit)
@ -183,7 +194,7 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
def plot_config(rpc: RPC = Depends(get_rpc)): def plot_config(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_plot_config() return PlotConfig.parse_obj(rpc._rpc_plot_config())
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) @router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])

View File

@ -8,12 +8,33 @@ import uvicorn
class UvicornServer(uvicorn.Server): class UvicornServer(uvicorn.Server):
""" """
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
Removed install_signal_handlers() override based on changes from this commit:
https://github.com/encode/uvicorn/commit/ce2ef45a9109df8eae038c0ec323eb63d644cbc6
Cannot rely on asyncio.get_event_loop() to create new event loop because of this check:
https://github.com/python/cpython/blob/4d7f11e05731f67fd2c07ec2972c6cb9861d52be/Lib/asyncio/events.py#L638
Fix by overriding run() and forcing creation of new event loop if uvloop is available
""" """
def install_signal_handlers(self):
def run(self, sockets=None):
import asyncio
""" """
In the parent implementation, this starts the thread, therefore we must patch it away here. Parent implementation calls self.config.setup_event_loop(),
but we need to create uvloop event loop manually
""" """
pass try:
import uvloop # noqa
except ImportError: # pragma: no cover
from uvicorn.loops.asyncio import asyncio_setup
asyncio_setup()
else:
asyncio.set_event_loop(uvloop.new_event_loop())
loop = asyncio.get_event_loop()
loop.run_until_complete(self.serve(sockets=sockets))
@contextlib.contextmanager @contextlib.contextmanager
def run_in_thread(self): def run_in_thread(self):

View File

@ -10,7 +10,7 @@ router_ui = APIRouter()
@router_ui.get('/favicon.ico', include_in_schema=False) @router_ui.get('/favicon.ico', include_in_schema=False)
async def favicon(): async def favicon():
return FileResponse(Path(__file__).parent / 'ui/favicon.ico') return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico'))
@router_ui.get('/{rest_of_path:path}', include_in_schema=False) @router_ui.get('/{rest_of_path:path}', include_in_schema=False)

View File

@ -3,7 +3,7 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta, timezone
from enum import Enum from enum import Enum
from math import isnan from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
@ -20,6 +20,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State from freqtrade.state import State
@ -288,9 +289,10 @@ class RPC:
""" Returns the X last trades """ """ Returns the X last trades """
if limit > 0: if limit > 0:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.id.desc()).limit(limit) Trade.close_date.desc()).limit(limit)
else: else:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all() trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.close_date.desc()).all()
output = [trade.to_json() for trade in trades] output = [trade.to_json() for trade in trades]
@ -401,14 +403,12 @@ class RPC:
num = float(len(durations) or 1) num = float(len(durations) or 1)
return { return {
'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_coin': profit_closed_coin_sum,
'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
'profit_closed_ratio_mean': profit_closed_ratio_mean, 'profit_closed_ratio_mean': profit_closed_ratio_mean,
'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
'profit_closed_ratio_sum': profit_closed_ratio_sum, 'profit_closed_ratio_sum': profit_closed_ratio_sum,
'profit_closed_fiat': profit_closed_fiat, 'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin_sum, 'profit_all_coin': profit_all_coin_sum,
'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED
'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
'profit_all_ratio_mean': profit_all_ratio_mean, 'profit_all_ratio_mean': profit_all_ratio_mean,
'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
@ -594,7 +594,7 @@ class RPC:
pair, self._freqtrade.get_free_open_trades()) 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, forcebuy=True):
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade return trade
else: else:
@ -663,7 +663,7 @@ class RPC:
} }
def _rpc_locks(self) -> Dict[str, Any]: def _rpc_locks(self) -> Dict[str, Any]:
""" Returns the current locks""" """ Returns the current locks """
locks = PairLocks.get_pair_locks(None) locks = PairLocks.get_pair_locks(None)
return { return {
@ -671,6 +671,25 @@ class RPC:
'locks': [lock.to_json() for lock in locks] 'locks': [lock.to_json() for lock in locks]
} }
def _rpc_delete_lock(self, lockid: Optional[int] = None,
pair: Optional[str] = None) -> Dict[str, Any]:
""" Delete specific lock(s) """
locks = []
if pair:
locks = PairLocks.get_pair_locks(pair)
if lockid:
locks = PairLock.query.filter(PairLock.id == lockid).all()
for lock in locks:
lock.active = False
lock.lock_end_time = datetime.now(timezone.utc)
# session is always the same
PairLock.session.flush()
return self._rpc_locks()
def _rpc_whitelist(self) -> Dict: def _rpc_whitelist(self) -> Dict:
""" Returns the currently active whitelist""" """ Returns the currently active whitelist"""
res = {'method': self._freqtrade.pairlists.name_list, res = {'method': self._freqtrade.pairlists.name_list,

View File

@ -6,6 +6,7 @@ This module manage Telegram communication
import json import json
import logging import logging
from datetime import timedelta from datetime import timedelta
from html import escape
from itertools import chain from itertools import chain
from typing import Any, Callable, Dict, List, Union from typing import Any, Callable, Dict, List, Union
@ -17,6 +18,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import round_coin_value from freqtrade.misc import round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
@ -143,6 +145,7 @@ class Telegram(RPCHandler):
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('locks', self._locks), CommandHandler('locks', self._locks),
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler(['show_config', 'show_conf'], self._show_config),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
@ -190,7 +193,8 @@ class Telegram(RPCHandler):
else: else:
msg['stake_amount_fiat'] = 0 msg['stake_amount_fiat'] = 0
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n" message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
f" (#{msg['trade_id']})\n"
f"*Amount:* `{msg['amount']:.8f}`\n" f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['limit']:.8f}`\n" f"*Open Rate:* `{msg['limit']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
@ -202,7 +206,8 @@ class Telegram(RPCHandler):
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
message = ("\N{WARNING SIGN} *{exchange}:* " message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg)) "Cancelling open buy Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8) msg['amount'] = round(msg['amount'], 8)
@ -213,7 +218,7 @@ class Telegram(RPCHandler):
msg['emoji'] = self._get_sell_emoji(msg) msg['emoji'] = self._get_sell_emoji(msg)
message = ("{emoji} *{exchange}:* Selling {pair}\n" message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
"*Amount:* `{amount:.8f}`\n" "*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n"
@ -233,7 +238,7 @@ class Telegram(RPCHandler):
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
"for {pair}. Reason: {reason}").format(**msg) "for {pair} (#{trade_id}). Reason: {reason}").format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg) message = '*Status:* `{status}`'.format(**msg)
@ -338,8 +343,17 @@ class Telegram(RPCHandler):
statlist, head = self._rpc._rpc_status_table( statlist, head = self._rpc._rpc_status_table(
self._config['stake_currency'], self._config.get('fiat_display_currency', '')) self._config['stake_currency'], self._config.get('fiat_display_currency', ''))
message = tabulate(statlist, headers=head, tablefmt='simple') max_trades_per_msg = 50
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) """
Calculate the number of messages of 50 trades per message
0.99 is used to make sure that there are no extra (empty) messages
As an example with 50 trades, there will be int(50/50 + 0.99) = 1 message
"""
for i in range(0, max(int(len(statlist) / max_trades_per_msg + 0.99), 1)):
message = tabulate(statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg],
headers=head,
tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@ -487,6 +501,10 @@ class Telegram(RPCHandler):
result = self._rpc._rpc_balance(self._config['stake_currency'], result = self._rpc._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', '')) self._config.get('fiat_display_currency', ''))
balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0)
if not balance_dust_level:
balance_dust_level = DUST_PER_COIN.get(self._config['stake_currency'], 1.0)
output = '' output = ''
if self._config['dry_run']: if self._config['dry_run']:
output += ( output += (
@ -496,7 +514,7 @@ class Telegram(RPCHandler):
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
) )
for curr in result['currencies']: for curr in result['currencies']:
if curr['est_stake'] > 0.0001: if curr['est_stake'] > balance_dust_level:
curr_output = ( curr_output = (
f"*{curr['currency']}:*\n" f"*{curr['currency']}:*\n"
f"\t`Available: {curr['free']:.8f}`\n" f"\t`Available: {curr['free']:.8f}`\n"
@ -505,7 +523,8 @@ class Telegram(RPCHandler):
f"\t`Est. {curr['stake']}: " f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else: else:
curr_output = f"*{curr['currency']}:* not showing <1$ amount \n" curr_output = (f"*{curr['currency']}:* not showing <{balance_dust_level} "
f"{curr['stake']} 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:
@ -627,13 +646,13 @@ class Telegram(RPCHandler):
nrecent nrecent
) )
trades_tab = tabulate( trades_tab = tabulate(
[[arrow.get(trade['open_date']).humanize(), [[arrow.get(trade['close_date']).humanize(),
trade['pair'], trade['pair'] + " (#" + str(trade['trade_id']) + ")",
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
for trade in trades['trades']], for trade in trades['trades']],
headers=[ headers=[
'Open Date', 'Close Date',
'Pair', 'Pair (ID)',
f'Profit ({stake_cur})', f'Profit ({stake_cur})',
], ],
tablefmt='simple') tablefmt='simple')
@ -713,19 +732,35 @@ class Telegram(RPCHandler):
Handler for /locks. Handler for /locks.
Returns the currently active locks Returns the currently active locks
""" """
try: locks = self._rpc._rpc_locks()
locks = self._rpc._rpc_locks() message = tabulate([[
message = tabulate([[ lock['id'],
lock['pair'], lock['pair'],
lock['lock_end_time'], lock['lock_end_time'],
lock['reason']] for lock in locks['locks']], lock['reason']] for lock in locks['locks']],
headers=['Pair', 'Until', 'Reason'], headers=['ID', 'Pair', 'Until', 'Reason'],
tablefmt='simple') tablefmt='simple')
message = "<pre>{}</pre>".format(message) message = f"<pre>{escape(message)}</pre>"
logger.debug(message) logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e)) @authorized_only
def _delete_locks(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete_locks.
Returns the currently active locks
"""
arg = context.args[0] if context.args and len(context.args) > 0 else None
lockid = None
pair = None
if arg:
try:
lockid = int(arg)
except ValueError:
pair = arg
self._rpc._rpc_delete_lock(lockid=lockid, pair=pair)
self._locks(update, context)
@authorized_only @authorized_only
def _whitelist(self, update: Update, context: CallbackContext) -> None: def _whitelist(self, update: Update, context: CallbackContext) -> None:
@ -844,6 +879,7 @@ class Telegram(RPCHandler):
"Avg. holding durationsfor buys and sells.`\n" "Avg. holding durationsfor buys and sells.`\n"
"*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n"
"*/locks:* `Show currently locked pairs`\n" "*/locks:* `Show currently locked pairs`\n"
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
"*/balance:* `Show account balance per currency`\n" "*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n" "*/reload_config:* `Reload configuration file` \n"

View File

@ -28,6 +28,12 @@ class Webhook(RPCHandler):
self._url = self._config['webhook']['url'] self._url = self._config['webhook']['url']
self._format = self._config['webhook'].get('format', 'form')
if self._format != 'form' and self._format != 'json':
raise NotImplementedError('Unknown webhook format `{}`, possible values are '
'`form` (default) and `json`'.format(self._format))
def cleanup(self) -> None: def cleanup(self) -> None:
""" """
Cleanup pending module resources. Cleanup pending module resources.
@ -66,7 +72,14 @@ class Webhook(RPCHandler):
def _send_msg(self, payload: dict) -> None: def _send_msg(self, payload: dict) -> None:
"""do the actual call to the webhook""" """do the actual call to the webhook"""
if self._format == 'form':
kwargs = {'data': payload}
elif self._format == 'json':
kwargs = {'json': payload}
else:
raise NotImplementedError('Unknown format: {}'.format(self._format))
try: try:
post(self._url, data=payload) post(self._url, **kwargs)
except RequestException as exc: except RequestException as exc:
logger.warning("Could not call webhook url. Exception: %s", exc) logger.warning("Could not call webhook url. Exception: %s", exc)

View File

@ -2,4 +2,4 @@
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_helper import merge_informative_pair from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open

View File

@ -649,7 +649,7 @@ class IStrategy(ABC):
:return: True if bot should sell at current rate :return: True if bot should sell at current rate
""" """
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60)
_, roi = self.min_roi_reached_entry(trade_dur) _, roi = self.min_roi_reached_entry(trade_dur)
if roi is None: if roi is None:
return False return False
@ -659,7 +659,7 @@ class IStrategy(ABC):
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Populates indicators for given candle (OHLCV) data (for multiple pairs) Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advice_buy or advise_sell! Does not run advise_buy or advise_sell!
Used by optimize operations only, not during dry / live runs. Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run. Using .copy() to get a fresh copy of the dataframe for every strategy run.
Has positive effects on memory usage for whatever reason - also when Has positive effects on memory usage for whatever reason - also when

View File

@ -56,3 +56,30 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
dataframe = dataframe.ffill() dataframe = dataframe.ffill()
return dataframe return dataframe
def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float:
"""
Given the current profit, and a desired stop loss value relative to the open price,
return a stop loss value that is relative to the current price, and which can be
returned from `custom_stoploss`.
The requested stop can be positive for a stop above the open price, or negative for
a stop below the open price. The return value is always >= 0.
Returns 0 if the resulting stop price would be above the current price.
:param open_relative_stop: Desired stop loss percentage relative to open price
:param current_profit: The current profit percentage
:return: Positive stop loss value relative to current price
"""
# formula is undefined for current_profit -1, return maximum value
if current_profit == -1:
return 1
stoploss = 1-((1+open_relative_stop)/(1+current_profit))
# negative stoploss values indicate the requested stop price is higher than the current price
return max(stoploss, 0.0)

View File

@ -57,7 +57,8 @@
"enabled": false, "enabled": false,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
"listen_port": 8080, "listen_port": 8080,
"verbosity": "info", "verbosity": "error",
"enable_openapi": false,
"jwt_secret_key": "somethingrandom", "jwt_secret_key": "somethingrandom",
"CORS_origins": [], "CORS_origins": [],
"username": "", "username": "",

View File

@ -39,6 +39,15 @@ class {{ hyperopt }}(IHyperOpt):
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
""" """
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
{{ buy_space | indent(12) }}
]
@staticmethod @staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
@ -79,12 +88,12 @@ class {{ hyperopt }}(IHyperOpt):
return populate_buy_trend return populate_buy_trend
@staticmethod @staticmethod
def indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching buy strategy parameters. Define your Hyperopt space for searching sell strategy parameters.
""" """
return [ return [
{{ buy_space | indent(12) }} {{ sell_space | indent(12) }}
] ]
@staticmethod @staticmethod
@ -126,11 +135,3 @@ class {{ hyperopt }}(IHyperOpt):
return populate_sell_trend return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
{{ sell_space | indent(12) }}
]

View File

@ -45,6 +45,23 @@ class SampleHyperOpt(IHyperOpt):
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
""" """
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod @staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
@ -92,20 +109,22 @@ class SampleHyperOpt(IHyperOpt):
return populate_buy_trend return populate_buy_trend
@staticmethod @staticmethod
def indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching buy strategy parameters. Define your Hyperopt space for searching sell strategy parameters.
""" """
return [ return [
Integer(10, 25, name='mfi-value'), Integer(75, 100, name='sell-mfi-value'),
Integer(15, 45, name='fastd-value'), Integer(50, 100, name='sell-fastd-value'),
Integer(20, 50, name='adx-value'), Integer(50, 100, name='sell-adx-value'),
Integer(20, 40, name='rsi-value'), Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='mfi-enabled'), Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='fastd-enabled'), Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='adx-enabled'), Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='rsi-enabled'), Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
] ]
@staticmethod @staticmethod
@ -153,56 +172,3 @@ class SampleHyperOpt(IHyperOpt):
return dataframe return dataframe
return populate_sell_trend return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include buy space.
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include sell space.
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
return dataframe

View File

@ -60,6 +60,23 @@ class AdvancedSampleHyperOpt(IHyperOpt):
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
return dataframe return dataframe
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod @staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
@ -106,20 +123,22 @@ class AdvancedSampleHyperOpt(IHyperOpt):
return populate_buy_trend return populate_buy_trend
@staticmethod @staticmethod
def indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching strategy parameters Define your Hyperopt space for searching sell strategy parameters.
""" """
return [ return [
Integer(10, 25, name='mfi-value'), Integer(75, 100, name='sell-mfi-value'),
Integer(15, 45, name='fastd-value'), Integer(50, 100, name='sell-fastd-value'),
Integer(20, 50, name='adx-value'), Integer(50, 100, name='sell-adx-value'),
Integer(20, 40, name='rsi-value'), Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='mfi-enabled'), Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='fastd-enabled'), Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='adx-enabled'), Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='rsi-enabled'), Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
] ]
@staticmethod @staticmethod
@ -168,25 +187,6 @@ class AdvancedSampleHyperOpt(IHyperOpt):
return populate_sell_trend return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters
"""
return [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-bb_upper',
'sell-macd_cross_signal',
'sell-sar_reversal'], name='sell-trigger')
]
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
""" """
@ -267,40 +267,3 @@ class AdvancedSampleHyperOpt(IHyperOpt):
Categorical([True, False], name='trailing_only_offset_is_reached'), Categorical([True, False], name='trailing_only_offset_is_reached'),
] ]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators.
Can be a copy of the corresponding method from the strategy,
or will be loaded from the strategy.
Must align to populate_indicators used (either from this File, or from the strategy)
Only used when --spaces does not include buy
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators.
Can be a copy of the corresponding method from the strategy,
or will be loaded from the strategy.
Must align to populate_indicators used (either from this File, or from the strategy)
Only used when --spaces does not include sell
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
return dataframe

View File

@ -10,7 +10,8 @@ import arrow
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade from freqtrade.persistence import LocalTrade, Trade
from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,8 +27,9 @@ class Wallet(NamedTuple):
class Wallets: class Wallets:
def __init__(self, config: dict, exchange: Exchange) -> None: def __init__(self, config: dict, exchange: Exchange, log: bool = True) -> None:
self._config = config self._config = config
self._log = log
self._exchange = exchange self._exchange = exchange
self._wallets: Dict[str, Wallet] = {} self._wallets: Dict[str, Wallet] = {}
self.start_cap = config['dry_run_wallet'] self.start_cap = config['dry_run_wallet']
@ -64,9 +66,15 @@ class Wallets:
""" """
# Recreate _wallets to reset closed trade balances # Recreate _wallets to reset closed trade balances
_wallets = {} _wallets = {}
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() open_trades = Trade.get_trades_proxy(is_open=True)
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() # If not backtesting...
tot_profit = sum([trade.calc_profit() for trade in closed_trades]) # TODO: potentially remove the ._log workaround to determine backtest mode.
if self._log:
closed_trades = Trade.get_trades_proxy(is_open=False)
tot_profit = sum(
[trade.close_profit_abs for trade in closed_trades if trade.close_profit_abs])
else:
tot_profit = LocalTrade.total_profit
tot_in_trades = sum([trade.stake_amount for trade in open_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades])
current_stake = self.start_cap + tot_profit - tot_in_trades current_stake = self.start_cap + tot_profit - tot_in_trades
@ -111,11 +119,12 @@ class Wallets:
:param require_update: Allow skipping an update if balances were recently refreshed :param require_update: Allow skipping an update if balances were recently refreshed
""" """
if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)):
if self._config['dry_run']: if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE):
self._update_dry()
else:
self._update_live() self._update_live()
logger.info('Wallets synced.') else:
self._update_dry()
if self._log:
logger.info('Wallets synced.')
self._last_wallet_refresh = arrow.utcnow().int_timestamp self._last_wallet_refresh = arrow.utcnow().int_timestamp
def get_all_balances(self) -> Dict[str, Any]: def get_all_balances(self) -> Dict[str, Any]:
@ -154,6 +163,7 @@ class Wallets:
Check if stake amount can be fulfilled with the available balance Check if stake amount can be fulfilled with the available balance
for the stake currency for the stake currency
:return: float: Stake amount :return: float: Stake amount
:raise: DependencyException if balance is lower than stake-amount
""" """
available_amount = self._get_available_stake_amount() available_amount = self._get_available_stake_amount()

View File

@ -1,4 +1,5 @@
site_name: Freqtrade site_name: Freqtrade
repo_url: https://github.com/freqtrade/freqtrade
nav: nav:
- Home: index.md - Home: index.md
- Quickstart with Docker: docker_quickstart.md - Quickstart with Docker: docker_quickstart.md
@ -13,16 +14,16 @@ nav:
- 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
- REST API & FreqUI: rest-api.md - REST API & FreqUI: rest-api.md
- Web Hook: webhook-config.md
- Data Downloading: data-download.md - Data Downloading: data-download.md
- Backtesting: backtesting.md - Backtesting: backtesting.md
- Hyperopt: hyperopt.md - Hyperopt: hyperopt.md
- Utility Sub-commands: utils.md - Utility Sub-commands: utils.md
- Plotting: plotting.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
- Exchange-specific Notes: exchanges.md - Exchange-specific Notes: exchanges.md
- Advanced Topics: - Advanced Topics:
- Advanced Post-installation Tasks: advanced-setup.md - Advanced Post-installation Tasks: advanced-setup.md
@ -50,24 +51,25 @@ extra_javascript:
- https://polyfill.io/v3/polyfill.min.js?features=es6 - https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
markdown_extensions: markdown_extensions:
- admonition - attr_list
- footnotes - admonition
- codehilite: - footnotes
guess_lang: false - codehilite:
- toc: guess_lang: false
permalink: true - toc:
- pymdownx.arithmatex: permalink: true
generic: true - pymdownx.arithmatex:
- pymdownx.details generic: true
- pymdownx.inlinehilite - pymdownx.details
- pymdownx.magiclink - pymdownx.inlinehilite
- pymdownx.pathconverter - pymdownx.magiclink
- pymdownx.smartsymbols - pymdownx.pathconverter
- pymdownx.snippets: - pymdownx.smartsymbols
base_path: docs - pymdownx.snippets:
check_paths: true base_path: docs
- pymdownx.tabbed check_paths: true
- pymdownx.superfences - pymdownx.tabbed
- pymdownx.tasklist: - pymdownx.superfences
custom_checkbox: true - pymdownx.tasklist:
- mdx_truly_sane_lists custom_checkbox: true
- mdx_truly_sane_lists

View File

@ -3,17 +3,17 @@
-r requirements-plot.txt -r requirements-plot.txt
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
coveralls==3.0.0 coveralls==3.0.1
flake8==3.8.4 flake8==3.9.0
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.812
pytest==6.2.2 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
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.7.0 isort==5.8.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.0.7 nbconvert==6.0.7

View File

@ -7,4 +7,5 @@ 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.1 joblib==1.0.1
psutil==5.8.0
progressbar2==3.53.1 progressbar2==3.53.1

View File

@ -1,16 +1,16 @@
numpy==1.20.1 numpy==1.20.1
pandas==1.2.2 pandas==1.2.3
ccxt==1.42.19 ccxt==1.43.89
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.6 cryptography==3.4.6
aiohttp==3.7.3 aiohttp==3.7.4.post0
SQLAlchemy==1.3.23 SQLAlchemy==1.4.2
python-telegram-bot==13.3 python-telegram-bot==13.4.1
arrow==0.17.0 arrow==1.0.3
cachetools==4.2.1 cachetools==4.2.1
requests==2.25.1 requests==2.25.1
urllib3==1.26.3 urllib3==1.26.4
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
@ -39,4 +39,4 @@ aiofiles==0.6.0
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.16 prompt-toolkit==3.0.17

View File

@ -118,6 +118,14 @@ class FtRestClient():
""" """
return self._get("locks") return self._get("locks")
def delete_lock(self, lock_id):
"""Delete (disable) lock from the database.
:param lock_id: ID for the lock to delete
:return: json object
"""
return self._delete("locks/{}".format(lock_id))
def daily(self, days=None): def daily(self, days=None):
"""Return the amount of open trades. """Return the amount of open trades.
@ -174,6 +182,16 @@ class FtRestClient():
""" """
return self._get("show_config") return self._get("show_config")
def ping(self):
"""simple ping"""
configstatus = self.show_config()
if not configstatus:
return {"status": "not_running"}
elif configstatus['state'] == "running":
return {"status": "pong"}
else:
return {"status": "not_running"}
def logs(self, limit=None): def logs(self, limit=None):
"""Show latest logs. """Show latest logs.

View File

@ -28,6 +28,7 @@ hyperopt = [
'filelock', 'filelock',
'joblib', 'joblib',
'progressbar2', 'progressbar2',
'psutil',
] ]
develop = [ develop = [

View File

@ -706,7 +706,7 @@ def test_download_data_timerange(mocker, caplog, markets):
start_download_data(get_args(args)) start_download_data(get_args(args))
assert dl_mock.call_count == 1 assert dl_mock.call_count == 1
# 20days ago # 20days ago
days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp days_ago = arrow.get(arrow.now().shift(days=-20).date()).int_timestamp
assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago
dl_mock.reset_mock() dl_mock.reset_mock()
@ -920,7 +920,7 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=hyperopt_results) MagicMock(return_value=hyperopt_results)
) )
@ -1145,14 +1145,14 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
captured = capsys.readouterr() captured = capsys.readouterr()
log_has("CSV file created: test_file.csv", caplog) log_has("CSV file created: test_file.csv", caplog)
f = Path("test_file.csv") f = Path("test_file.csv")
assert 'Best,1,2,-1.25%,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text() assert 'Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in f.read_text()
assert f.is_file() assert f.is_file()
f.unlink() f.unlink()
def test_hyperopt_show(mocker, capsys, hyperopt_results): def test_hyperopt_show(mocker, capsys, hyperopt_results):
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.load_previous_results', 'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results',
MagicMock(return_value=hyperopt_results) MagicMock(return_value=hyperopt_results)
) )

View File

@ -6,7 +6,7 @@ from copy import deepcopy
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock
import arrow import arrow
import numpy as np import numpy as np
@ -19,7 +19,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade, init_db from freqtrade.persistence import LocalTrade, Trade, init_db
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
@ -64,6 +64,14 @@ def get_args(args):
return Arguments(args).get_parsed_arg() return Arguments(args).get_parsed_arg()
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
def get_mock_coro(return_value):
async def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
def patched_configuration_load_config_file(mocker, config) -> None: def patched_configuration_load_config_file(mocker, config) -> None:
mocker.patch( mocker.patch(
'freqtrade.configuration.configuration.load_config_file', 'freqtrade.configuration.configuration.load_config_file',
@ -183,28 +191,34 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
def create_mock_trades(fee): def create_mock_trades(fee, use_db: bool = True):
""" """
Create some fake trades ... Create some fake trades ...
""" """
def add_trade(trade):
if use_db:
Trade.session.add(trade)
else:
LocalTrade.add_bt_trade(trade)
# Simulate dry_run entries # Simulate dry_run entries
trade = mock_trade_1(fee) trade = mock_trade_1(fee)
Trade.session.add(trade) add_trade(trade)
trade = mock_trade_2(fee) trade = mock_trade_2(fee)
Trade.session.add(trade) add_trade(trade)
trade = mock_trade_3(fee) trade = mock_trade_3(fee)
Trade.session.add(trade) add_trade(trade)
trade = mock_trade_4(fee) trade = mock_trade_4(fee)
Trade.session.add(trade) add_trade(trade)
trade = mock_trade_5(fee) trade = mock_trade_5(fee)
Trade.session.add(trade) add_trade(trade)
trade = mock_trade_6(fee) trade = mock_trade_6(fee)
Trade.session.add(trade) add_trade(trade)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -255,6 +269,7 @@ def get_default_conf(testdatadir):
"20": 0.02, "20": 0.02,
"0": 0.04 "0": 0.04
}, },
"dry_run_wallet": 1000,
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,
@ -1729,7 +1744,7 @@ def import_fails() -> None:
realimport = builtins.__import__ realimport = builtins.__import__
def mockedimport(name, *args, **kwargs): def mockedimport(name, *args, **kwargs):
if name in ["filelock", 'systemd.journal']: if name in ["filelock", 'systemd.journal', 'uvloop']:
raise ImportError(f"No module named '{name}'") raise ImportError(f"No module named '{name}'")
return realimport(name, *args, **kwargs) return realimport(name, *args, **kwargs)
@ -1766,7 +1781,7 @@ def hyperopt_results():
'params_dict': { 'params_dict': {
'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501
'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501
'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'median_profit': -1.2222, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501
'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501
'total_profit': -0.00125625, 'total_profit': -0.00125625,
'current_epoch': 1, 'current_epoch': 1,
@ -1781,7 +1796,7 @@ def hyperopt_results():
'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501
'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501
'stoploss': {'stoploss': -0.338070047333259}}, 'stoploss': {'stoploss': -0.338070047333259}},
'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'median_profit': -1.2222, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501
'total_profit': 6.185e-05, 'total_profit': 6.185e-05,
'current_epoch': 2, 'current_epoch': 2,
@ -1791,7 +1806,7 @@ def hyperopt_results():
'loss': 14.241196856510731, 'loss': 14.241196856510731,
'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501
'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501
'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'median_profit': -1.2222, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501
'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501
'total_profit': -0.13639474, 'total_profit': -0.13639474,
'current_epoch': 3, 'current_epoch': 3,
@ -1801,14 +1816,14 @@ def hyperopt_results():
'loss': 100000, 'loss': 100000,
'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501
'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501
'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False
}, { }, {
'loss': 0.22195522184191518, 'loss': 0.22195522184191518,
'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501
'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501
'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'median_profit': -1.2222, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501
'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501
'total_profit': -0.002480140000000001, 'total_profit': -0.002480140000000001,
'current_epoch': 5, 'current_epoch': 5,
@ -1818,7 +1833,7 @@ def hyperopt_results():
'loss': 0.545315889154162, 'loss': 0.545315889154162,
'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501
'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'median_profit': -1.2222, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501
'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501
'total_profit': -0.0041773, 'total_profit': -0.0041773,
'current_epoch': 6, 'current_epoch': 6,
@ -1830,7 +1845,7 @@ def hyperopt_results():
'params_details': { 'params_details': {
'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501
'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501
'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'median_profit': -1.2222, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501
'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501
'total_profit': -0.06339929, 'total_profit': -0.06339929,
'current_epoch': 7, 'current_epoch': 7,
@ -1840,7 +1855,7 @@ def hyperopt_results():
'loss': 20.0, # noqa: E501 'loss': 20.0, # noqa: E501
'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501
'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501
'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'median_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501
'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501
'total_profit': 0.0, 'total_profit': 0.0,
'current_epoch': 8, 'current_epoch': 8,
@ -1850,7 +1865,7 @@ def hyperopt_results():
'loss': 2.4731817780991223, 'loss': 2.4731817780991223,
'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501
'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501
'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'median_profit': -1.2222, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501
'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501
'total_profit': -0.044050070000000004, # noqa: E501 'total_profit': -0.044050070000000004, # noqa: E501
'current_epoch': 9, 'current_epoch': 9,
@ -1860,7 +1875,7 @@ def hyperopt_results():
'loss': -0.2604606005845212, # noqa: E501 'loss': -0.2604606005845212, # noqa: E501
'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501
'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501
'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'median_profit': -1.2222, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501
'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501
'total_profit': 0.00021629, 'total_profit': 0.00021629,
'current_epoch': 10, 'current_epoch': 10,
@ -1870,7 +1885,7 @@ def hyperopt_results():
'loss': 4.876465945994304, # noqa: E501 'loss': 4.876465945994304, # noqa: E501
'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501
'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501
'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'median_profit': -1.2222, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501
'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501
'total_profit': -0.07436117, 'total_profit': -0.07436117,
'current_epoch': 11, 'current_epoch': 11,
@ -1880,7 +1895,7 @@ def hyperopt_results():
'loss': 100000, 'loss': 100000,
'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501
'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501
'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'median_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501
'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501
'total_profit': 0, 'total_profit': 0,
'current_epoch': 12, 'current_epoch': 12,

View File

@ -28,6 +28,8 @@ def mock_trade_1(fee):
amount_requested=123.0, amount_requested=123.0,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=0.123, open_rate=0.123,
exchange='bittrex', exchange='bittrex',
open_order_id='dry_run_buy_12345', open_order_id='dry_run_buy_12345',
@ -81,6 +83,7 @@ def mock_trade_2(fee):
open_rate=0.123, open_rate=0.123,
close_rate=0.128, close_rate=0.128,
close_profit=0.005, close_profit=0.005,
close_profit_abs=0.000584127,
exchange='bittrex', exchange='bittrex',
is_open=False, is_open=False,
open_order_id='dry_run_sell_12345', open_order_id='dry_run_sell_12345',
@ -88,7 +91,7 @@ def mock_trade_2(fee):
timeframe=5, timeframe=5,
sell_reason='sell_signal', sell_reason='sell_signal',
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
close_date=datetime.now(tz=timezone.utc), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
) )
o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
trade.orders.append(o) trade.orders.append(o)
@ -140,6 +143,7 @@ def mock_trade_3(fee):
open_rate=0.05, open_rate=0.05,
close_rate=0.06, close_rate=0.06,
close_profit=0.01, close_profit=0.01,
close_profit_abs=0.000155,
exchange='bittrex', exchange='bittrex',
is_open=False, is_open=False,
strategy='DefaultStrategy', strategy='DefaultStrategy',
@ -180,6 +184,8 @@ def mock_trade_4(fee):
amount_requested=124.0, amount_requested=124.0,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14),
is_open=True,
open_rate=0.123, open_rate=0.123,
exchange='bittrex', exchange='bittrex',
open_order_id='prod_buy_12345', open_order_id='prod_buy_12345',
@ -230,6 +236,8 @@ def mock_trade_5(fee):
amount_requested=124.0, amount_requested=124.0,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12),
is_open=True,
open_rate=0.123, open_rate=0.123,
exchange='bittrex', exchange='bittrex',
strategy='SampleStrategy', strategy='SampleStrategy',
@ -279,8 +287,10 @@ def mock_trade_6(fee):
stake_amount=0.001, stake_amount=0.001,
amount=2.0, amount=2.0,
amount_requested=2.0, amount_requested=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5),
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
is_open=True,
open_rate=0.15, open_rate=0.15,
exchange='bittrex', exchange='bittrex',
strategy='SampleStrategy', strategy='SampleStrategy',

View File

@ -274,15 +274,17 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir): def test_calculate_max_drawdown(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)
drawdown, h, low = calculate_max_drawdown(bt_data) drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(bt_data)
assert isinstance(drawdown, float) assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.21142322 assert pytest.approx(drawdown) == 0.21142322
assert isinstance(h, Timestamp) assert isinstance(hdate, Timestamp)
assert isinstance(low, Timestamp) assert isinstance(lowdate, Timestamp)
assert h == Timestamp('2018-01-24 14:25:00', tz='UTC') assert isinstance(hval, float)
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC') assert isinstance(lval, float)
assert hdate == Timestamp('2018-01-24 14:25:00', tz='UTC')
assert lowdate == Timestamp('2018-01-30 04:45:00', tz='UTC')
with pytest.raises(ValueError, match='Trade dataframe empty.'): with pytest.raises(ValueError, match='Trade dataframe empty.'):
drawdown, h, low = calculate_max_drawdown(DataFrame()) drawdown, hdate, lowdate, hval, lval = calculate_max_drawdown(DataFrame())
def test_calculate_csum(testdatadir): def test_calculate_csum(testdatadir):
@ -294,6 +296,10 @@ def test_calculate_csum(testdatadir):
assert isinstance(csum_max, float) assert isinstance(csum_max, float)
assert csum_min < 0.01 assert csum_min < 0.01
assert csum_max > 0.02 assert csum_max > 0.02
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
assert csum_min1 == csum_min + 5
assert csum_max1 == csum_max + 5
with pytest.raises(ValueError, match='Trade dataframe empty.'): with pytest.raises(ValueError, match='Trade dataframe empty.'):
csum_min, csum_max = calculate_csum(DataFrame()) csum_min, csum_max = calculate_csum(DataFrame())
@ -310,13 +316,16 @@ def test_calculate_max_drawdown2():
# sort by profit and reset index # sort by profit and reset index
df = df.sort_values('profit').reset_index(drop=True) df = df.sort_values('profit').reset_index(drop=True)
df1 = df.copy() df1 = df.copy()
drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit') drawdown, hdate, ldate, hval, lval = calculate_max_drawdown(
df, date_col='open_date', value_col='profit')
# Ensure df has not been altered. # Ensure df has not been altered.
assert df.equals(df1) assert df.equals(df1)
assert isinstance(drawdown, float) assert isinstance(drawdown, float)
# High must be before low # High must be before low
assert h < low assert hdate < ldate
# High value must be higher than low value
assert hval > lval
assert drawdown == 0.091755 assert drawdown == 0.091755
df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date'])

View File

@ -44,6 +44,7 @@ EXCHANGES = {
def exchange_conf(): def exchange_conf():
config = get_default_conf((Path(__file__).parent / "testdata").resolve()) config = get_default_conf((Path(__file__).parent / "testdata").resolve())
config['exchange']['pair_whitelist'] = [] config['exchange']['pair_whitelist'] = []
config['dry_run'] = False
return config return config

View File

@ -1,6 +1,7 @@
import copy import copy
import logging import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import isclose
from random import randint from random import randint
from unittest.mock import MagicMock, Mock, PropertyMock, patch from unittest.mock import MagicMock, Mock, PropertyMock, patch
@ -18,21 +19,13 @@ from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes,
timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds) timeframe_to_seconds)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange, log_has, log_has_re from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re
# Make sure to always keep one exchange here which is NOT subclassed!! # Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx']
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
def get_mock_coro(return_value):
async def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs):
@ -378,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert result == 2 / 0.9 assert isclose(result, 2 * 1.1)
# min amount is set # min amount is set
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -390,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == 2 * 2 / 0.9 assert isclose(result, 2 * 2 * 1.1)
# min amount and cost are set (cost is minimal) # min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -402,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == max(2, 2 * 2) / 0.9 assert isclose(result, max(2, 2 * 2) * 1.1)
# min amount and cost are set (amount is minial) # min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -414,7 +407,14 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert result == max(8, 2 * 2) / 0.9 assert isclose(result, max(8, 2 * 2) * 1.1)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
assert isclose(result, max(8, 2 * 2) * 1.45)
# Really big stoploss
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
assert isclose(result, max(8, 2 * 2) * 1.5)
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
@ -432,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) 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) assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8)
def test_set_sandbox(default_conf, mocker): def test_set_sandbox(default_conf, mocker):
@ -498,7 +498,7 @@ def test__load_markets(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to initialize markets. Reason: SomeError', caplog) assert log_has('Unable to initialize markets.', caplog)
expected_return = {'ETH/BTC': 'available'} expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock() api_mock = MagicMock()
@ -2276,12 +2276,20 @@ def test_get_fee(default_conf, mocker, exchange_name):
'cost': 0.05 'cost': 0.05
}) })
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange._config.pop('fee', None)
assert exchange.get_fee('ETH/BTC') == 0.025 assert exchange.get_fee('ETH/BTC') == 0.025
assert api_mock.calculate_fee.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_fee', 'calculate_fee', symbol="ETH/BTC") 'get_fee', 'calculate_fee', symbol="ETH/BTC")
api_mock.calculate_fee.reset_mock()
exchange._config['fee'] = 0.001
assert exchange.get_fee('ETH/BTC') == 0.001
assert api_mock.calculate_fee.call_count == 0
def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='bittrex') exchange = get_patched_exchange(mocker, default_conf, id='bittrex')

View File

@ -1,6 +1,5 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
import logging import logging
from unittest.mock import MagicMock
import pytest import pytest
@ -489,7 +488,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal} default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal}
mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker) patch_exchange(mocker)
frame = _build_backtest_dataframe(data.data) frame = _build_backtest_dataframe(data.data)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -503,7 +503,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
min_date, max_date = get_timerange({pair: frame}) min_date, max_date = get_timerange({pair: frame})
results = backtesting.backtest( results = backtesting.backtest(
processed=data_processed, processed=data_processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=10, max_open_trades=10,
@ -514,6 +513,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
for c, trade in enumerate(data.trades): for c, trade in enumerate(data.trades):
res = results.iloc[c] res = results.iloc[c]
assert res.sell_reason == trade.sell_reason assert res.sell_reason == trade.sell_reason.value
assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick)

View File

@ -9,7 +9,6 @@ import pandas as pd
import pytest import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade import constants
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
@ -19,6 +18,7 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
@ -90,7 +90,6 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtesting.backtest( results = backtesting.backtest(
processed=processed, processed=processed,
stake_amount=config['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=1, max_open_trades=1,
@ -111,7 +110,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
return { return {
'processed': processed, 'processed': processed,
'stake_amount': conf['stake_amount'],
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
'max_open_trades': 10, 'max_open_trades': 10,
@ -233,8 +231,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog) assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog)
def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) -> None:
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -242,9 +239,21 @@ def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_con
'backtesting', 'backtesting',
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--stake-amount', '1',
'--starting-balance', '2'
] ]
with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): conf = setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
assert isinstance(conf, dict)
args = [
'backtesting',
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--stake-amount', '1',
'--starting-balance', '0.5'
]
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
setup_optimize_configuration(get_args(args), RunMode.BACKTEST) setup_optimize_configuration(get_args(args), RunMode.BACKTEST)
@ -448,9 +457,48 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf) Backtesting(default_conf)
def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker)
default_conf['stake_amount'] = 'unlimited'
backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC'
row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
1, # Sell
0.001, # Open
0.0011, # Close
0, # Sell
0.00099, # Low
0.0012, # High
]
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
assert isinstance(trade, LocalTrade)
assert trade.stake_amount == 495
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2)
assert trade is None
# Stake-amount too high!
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
assert trade is None
# Stake-amount too high!
mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount",
side_effect=DependencyException)
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0)
assert trade is None
def test_backtest_one(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)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC' pair = 'UNITTEST/BTC'
@ -461,7 +509,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
results = backtesting.backtest( results = backtesting.backtest(
processed=processed, processed=processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=10, max_open_trades=10,
@ -486,7 +533,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'trade_duration': [235, 40], 'trade_duration': [235, 40],
'profit_ratio': [0.0, 0.0], 'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0], 'profit_abs': [0.0, 0.0],
'sell_reason': [SellType.ROI, SellType.ROI], 'sell_reason': [SellType.ROI.value, SellType.ROI.value],
'initial_stop_loss_abs': [0.0940005, 0.09272236], 'initial_stop_loss_abs': [0.0940005, 0.09272236],
'initial_stop_loss_ratio': [-0.1, -0.1], 'initial_stop_loss_ratio': [-0.1, -0.1],
'stop_loss_abs': [0.0940005, 0.09272236], 'stop_loss_abs': [0.0940005, 0.09272236],
@ -512,6 +559,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_1min_timeframe(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)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -523,7 +571,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
results = backtesting.backtest( results = backtesting.backtest(
processed=processed, processed=processed,
stake_amount=default_conf['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=1, max_open_trades=1,
@ -558,6 +605,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
default_conf['enable_protections'] = True default_conf['enable_protections'] = True
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
tests = [ tests = [
['sine', 9], ['sine', 9],
['raise', 10], ['raise', 10],
@ -589,6 +637,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
default_conf['protections'] = protections default_conf['protections'] = protections
default_conf['enable_protections'] = True default_conf['enable_protections'] = True
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
# While buy-signals are unrealistic, running backtesting # While buy-signals are unrealistic, running backtesting
# over and over again should not cause different results # over and over again should not cause different results
@ -626,6 +675,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
pair='UNITTEST/BTC', datadir=testdatadir) pair='UNITTEST/BTC', datadir=testdatadir)
@ -658,6 +708,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
return dataframe return dataframe
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
@ -678,7 +729,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
backtest_conf = { backtest_conf = {
'processed': processed, 'processed': processed,
'stake_amount': default_conf['stake_amount'],
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
'max_open_trades': 3, 'max_open_trades': 3,
@ -694,7 +744,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtest_conf = { backtest_conf = {
'processed': processed, 'processed': processed,
'stake_amount': default_conf['stake_amount'],
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
'max_open_trades': 1, 'max_open_trades': 1,
@ -822,6 +871,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'2018-01-30 05:35:00', ], utc=True), '2018-01-30 05:35:00', ], utc=True),
'trade_duration': [235, 40], 'trade_duration': [235, 40],
'is_open': [False, False], 'is_open': [False, False],
'stake_amount': [0.01, 0.01],
'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]
@ -838,6 +888,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'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],
'is_open': [False, False, False], 'is_open': [False, False, False],
'stake_amount': [0.01, 0.01, 0.01],
'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

@ -12,11 +12,11 @@ import pytest
from arrow import Arrow from arrow import Arrow
from filelock import Timeout from filelock import Timeout
from freqtrade import constants
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
@ -130,8 +130,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
assert log_has('Parameter --print-all detected ...', caplog) assert log_has('Parameter --print-all detected ...', caplog)
def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None: def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None:
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -139,9 +138,20 @@ def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_con
'hyperopt', 'hyperopt',
'--config', 'config.json', '--config', 'config.json',
'--hyperopt', 'DefaultHyperOpt', '--hyperopt', 'DefaultHyperOpt',
'--stake-amount', '1',
'--starting-balance', '2'
] ]
conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
assert isinstance(conf, dict)
with pytest.raises(DependencyException, match=r'.`stake_amount`.*'): args = [
'hyperopt',
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--stake-amount', '1',
'--starting-balance', '0.5'
]
with pytest.raises(OperationalException, match=r"Starting balance .* smaller .*"):
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
@ -327,9 +337,9 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non
def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None: def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir) epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
results_file = testdatadir / 'optimize' / 'ut_results.pickle' results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_epochs = hyperopt._read_results(results_file) hyperopt_epochs = HyperoptTools._read_results(results_file)
assert log_has(f"Reading epochs from '{results_file}'", caplog) assert log_has(f"Reading epochs from '{results_file}'", caplog)
assert hyperopt_epochs == epochs assert hyperopt_epochs == epochs
mock_load.assert_called_once() mock_load.assert_called_once()
@ -337,7 +347,7 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N
def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
epochs = create_results(mocker, hyperopt, testdatadir) epochs = create_results(mocker, hyperopt, testdatadir)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) mocker.patch.object(Path, 'is_file', MagicMock(return_value=True))
statmock = MagicMock() statmock = MagicMock()
statmock.st_size = 5 statmock.st_size = 5
@ -345,16 +355,16 @@ def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None:
results_file = testdatadir / 'optimize' / 'ut_results.pickle' results_file = testdatadir / 'optimize' / 'ut_results.pickle'
hyperopt_epochs = hyperopt.load_previous_results(results_file) hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert hyperopt_epochs == epochs assert hyperopt_epochs == epochs
mock_load.assert_called_once() mock_load.assert_called_once()
del epochs[0]['is_best'] del epochs[0]['is_best']
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) mock_load = mocker.patch('freqtrade.optimize.hyperopt_tools.load', return_value=epochs)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
hyperopt.load_previous_results(results_file) HyperoptTools.load_previous_results(results_file)
def test_roi_table_generation(hyperopt) -> None: def test_roi_table_generation(hyperopt) -> None:
@ -444,7 +454,7 @@ def test_format_results(hyperopt):
'is_initial_point': True, 'is_initial_point': True,
} }
result = hyperopt._format_explanation_string(results, 1) result = HyperoptTools._format_explanation_string(results, 1)
assert result.find(' 66.67%') assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC') assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %') assert result.find('2.0000Σ %')
@ -458,7 +468,7 @@ def test_format_results(hyperopt):
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['total_profit'] = results_metrics['total_profit'] results['total_profit'] = results_metrics['total_profit']
result = hyperopt._format_explanation_string(results, 1) result = HyperoptTools._format_explanation_string(results, 1)
assert result.find('Total profit 1.00000000 EUR') assert result.find('Total profit 1.00000000 EUR')
@ -1067,7 +1077,7 @@ def test_print_epoch_details(capsys):
'is_best': True 'is_best': True
} }
Hyperopt.print_epoch_details(test_result, 5, False, no_header=True) HyperoptTools.print_epoch_details(test_result, 5, False, no_header=True)
captured = capsys.readouterr() captured = capsys.readouterr()
assert '# Trailing stop:' in captured.out assert '# Trailing stop:' in captured.out
# re.match(r"Pairs for .*", captured.out) # re.match(r"Pairs for .*", captured.out)

View File

@ -48,7 +48,7 @@ def test_text_table_bt_results():
) )
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results) starting_balance=4, results=results)
assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str
@ -73,11 +73,13 @@ def test_generate_backtest_stats(default_conf, testdatadir):
"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],
"is_open": [False, False, False, True], "is_open": [False, False, False, True],
"stake_amount": [0.01, 0.01, 0.01, 0.01],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS, "sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL] SellType.ROI, SellType.FORCE_SELL]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
'final_balance': 1000.02,
'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp,
} }
@ -100,6 +102,7 @@ def test_generate_backtest_stats(default_conf, testdatadir):
# Above sample had no loosing trade # Above sample had no loosing trade
assert strat_stats['max_drawdown'] == 0.0 assert strat_stats['max_drawdown'] == 0.0
# Retry with losing trade
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"],
@ -116,18 +119,31 @@ 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.0032903, 0.003217], "close_rate": [0.002546, 0.003014, 0.0032903, 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, "stake_amount": [0.01, 0.01, 0.01, 0.01],
SellType.ROI, SellType.FORCE_SELL] "sell_reason": [SellType.ROI, SellType.ROI,
SellType.STOP_LOSS, SellType.FORCE_SELL]
}), }),
'config': default_conf} 'config': default_conf,
'locks': [],
'final_balance': 1000.02,
'backtest_start_time': Arrow.utcnow().int_timestamp,
'backtest_end_time': Arrow.utcnow().int_timestamp,
}
} }
assert strat_stats['max_drawdown'] == 0.0 stats = generate_backtest_stats(btdata, results, min_date, max_date)
assert strat_stats['drawdown_start'] == datetime(1970, 1, 1, tzinfo=timezone.utc) assert isinstance(stats, dict)
assert strat_stats['drawdown_end'] == datetime(1970, 1, 1, tzinfo=timezone.utc) assert 'strategy' in stats
assert strat_stats['drawdown_end_ts'] == 0 assert 'DefStrat' in stats['strategy']
assert strat_stats['drawdown_start_ts'] == 0 assert 'strategy_comparison' in stats
strat_stats = stats['strategy']['DefStrat']
assert strat_stats['max_drawdown'] == 0.013803
assert strat_stats['drawdown_start'] == datetime(2017, 11, 14, 22, 10, tzinfo=timezone.utc)
assert strat_stats['drawdown_end'] == datetime(2017, 11, 14, 22, 43, tzinfo=timezone.utc)
assert strat_stats['drawdown_end_ts'] == 1510699380000
assert strat_stats['drawdown_start_ts'] == 1510697400000
assert strat_stats['pairlist'] == ['UNITTEST/BTC'] assert strat_stats['pairlist'] == ['UNITTEST/BTC']
# Test storing stats # Test storing stats
@ -189,7 +205,7 @@ def test_generate_pair_metrics():
) )
pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC', pair_results = generate_pair_metrics(data={'ETH/BTC': {}}, stake_currency='BTC',
max_open_trades=2, results=results) starting_balance=2, results=results)
assert isinstance(pair_results, list) assert isinstance(pair_results, list)
assert len(pair_results) == 2 assert len(pair_results) == 2
assert pair_results[-1]['key'] == 'TOTAL' assert pair_results[-1]['key'] == 'TOTAL'
@ -265,7 +281,7 @@ def test_generate_sell_reason_stats():
'wins': [2, 0, 0], 'wins': [2, 0, 0],
'draws': [0, 0, 0], 'draws': [0, 0, 0],
'losses': [0, 0, 1], 'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value]
} }
) )
@ -291,6 +307,7 @@ def test_generate_sell_reason_stats():
def test_text_table_strategy(default_conf): def test_text_table_strategy(default_conf):
default_conf['max_open_trades'] = 2 default_conf['max_open_trades'] = 2
default_conf['dry_run_wallet'] = 3
results = {} results = {}
results['TestStrategy1'] = {'results': pd.DataFrame( results['TestStrategy1'] = {'results': pd.DataFrame(
{ {
@ -323,9 +340,9 @@ def test_text_table_strategy(default_conf):
'|---------------+--------+----------------+----------------+------------------+' '|---------------+--------+----------------+----------------+------------------+'
'----------------+----------------+--------+---------+----------|\n' '----------------+----------------+--------+---------+----------|\n'
'| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |' '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |'
' 30.00 | 0:17:00 | 3 | 0 | 0 |\n' ' 36.67 | 0:17:00 | 3 | 0 | 0 |\n'
'| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
' 45.00 | 0:20:00 | 3 | 0 | 0 |' ' 43.33 | 0:20:00 | 3 | 0 | 0 |'
) )
strategy_results = generate_strategy_metrics(all_results=results) strategy_results = generate_strategy_metrics(all_results=results)

View File

@ -73,9 +73,13 @@ def test_PairLocks(use_db):
assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50))
if use_db: if use_db:
assert len(PairLock.query.all()) > 0 locks = PairLocks.get_all_locks()
locks_db = PairLock.query.all()
assert len(locks) == len(locks_db)
assert len(locks_db) > 0
else: else:
# Nothing was pushed to the database # Nothing was pushed to the database
assert len(PairLocks.get_all_locks()) > 0
assert len(PairLock.query.all()) == 0 assert len(PairLock.query.all()) == 0
# Reset use-db variable # Reset use-db variable
PairLocks.reset_locks() PairLocks.reset_locks()

View File

@ -1,7 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
from datetime import datetime from datetime import datetime, timedelta, timezone
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock
import pytest import pytest
@ -10,6 +10,7 @@ from numpy import isnan
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.pairlock_middleware import PairLocks
from freqtrade.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State from freqtrade.state import State
@ -412,10 +413,10 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2) assert prec_satoshi(stats['profit_closed_percent_mean'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 5.802e-05) assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
assert prec_satoshi(stats['profit_all_percent'], 2.89) assert prec_satoshi(stats['profit_all_percent_mean'], 2.89)
assert prec_satoshi(stats['profit_all_fiat'], 0.8703) assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
assert stats['trade_count'] == 2 assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now' assert stats['first_trade_date'] == 'just now'
@ -481,10 +482,10 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 0) assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0) assert prec_satoshi(stats['profit_closed_percent_mean'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0) assert prec_satoshi(stats['profit_closed_fiat'], 0)
assert prec_satoshi(stats['profit_all_coin'], 0) assert prec_satoshi(stats['profit_all_coin'], 0)
assert prec_satoshi(stats['profit_all_percent'], 0) assert prec_satoshi(stats['profit_all_percent_mean'], 0)
assert prec_satoshi(stats['profit_all_fiat'], 0) assert prec_satoshi(stats['profit_all_fiat'], 0)
assert stats['trade_count'] == 1 assert stats['trade_count'] == 1
assert stats['first_trade_date'] == 'just now' assert stats['first_trade_date'] == 'just now'
@ -911,6 +912,24 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
rpc._rpc_forcebuy(pair, None) rpc._rpc_forcebuy(pair, None)
@pytest.mark.usefixtures("init_persistence")
def test_rpc_delete_lock(mocker, default_conf):
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc = RPC(freqtradebot)
pair = 'ETH/BTC'
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4))
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5))
PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10))
locks = rpc._rpc_locks()
assert locks['lock_count'] == 3
locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id'])
assert locks1['lock_count'] == 2
locks2 = rpc._rpc_delete_lock(pair=pair)
assert locks2['lock_count'] == 0
def test_rpc_whitelist(mocker, default_conf) -> None: def test_rpc_whitelist(mocker, default_conf) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())

View File

@ -23,8 +23,8 @@ from freqtrade.rpc.api_server import ApiServer
from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
from freqtrade.state import RunMode, State from freqtrade.state import RunMode, State
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has,
patch_get_signal) log_has_re, patch_get_signal)
BASE_URI = "/api/v1" BASE_URI = "/api/v1"
@ -230,7 +230,7 @@ def test_api__init__(default_conf, mocker):
assert apiserver._config == default_conf assert apiserver._config == default_conf
def test_api_UvicornServer(default_conf, mocker): def test_api_UvicornServer(mocker):
thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread') thread_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.threading.Thread')
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert thread_mock.call_count == 0 assert thread_mock.call_count == 0
@ -248,6 +248,38 @@ def test_api_UvicornServer(default_conf, mocker):
assert s.should_exit is True assert s.should_exit is True
def test_api_UvicornServer_run(mocker):
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
get_mock_coro(None))
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert serve_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert serve_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run()
assert serve_mock.call_count == 1
def test_api_UvicornServer_run_no_uvloop(mocker, import_fails):
serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve',
get_mock_coro(None))
s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1'))
assert serve_mock.call_count == 0
s.install_signal_handlers()
# Original implementation starts a thread - make sure that's not the case
assert serve_mock.call_count == 0
# Fake started to avoid sleeping forever
s.started = True
s.run()
assert serve_mock.call_count == 1
def test_api_run(default_conf, mocker, caplog): def test_api_run(default_conf, mocker, caplog):
default_conf.update({"api_server": {"enabled": True, default_conf.update({"api_server": {"enabled": True,
"listen_ip_address": "127.0.0.1", "listen_ip_address": "127.0.0.1",
@ -418,6 +450,16 @@ def test_api_locks(botclient):
assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) assert 'randreason' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason']) assert 'deadbeef' in (rc.json()['locks'][0]['reason'], rc.json()['locks'][1]['reason'])
# Test deletions
rc = client_delete(client, f"{BASE_URI}/locks/1")
assert_response(rc)
assert rc.json()['lock_count'] == 1
rc = client_post(client, f"{BASE_URI}/locks/delete",
data='{"pair": "XRP/BTC"}')
assert_response(rc)
assert rc.json()['lock_count'] == 0
def test_api_show_config(botclient, mocker): def test_api_show_config(botclient, mocker):
ftbot, client = botclient ftbot, client = botclient
@ -614,14 +656,12 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
'latest_trade_timestamp': ANY, 'latest_trade_timestamp': ANY,
'profit_all_coin': 6.217e-05, 'profit_all_coin': 6.217e-05,
'profit_all_fiat': 0.76748865, 'profit_all_fiat': 0.76748865,
'profit_all_percent': 6.2,
'profit_all_percent_mean': 6.2, 'profit_all_percent_mean': 6.2,
'profit_all_ratio_mean': 0.06201058, 'profit_all_ratio_mean': 0.06201058,
'profit_all_percent_sum': 6.2, 'profit_all_percent_sum': 6.2,
'profit_all_ratio_sum': 0.06201058, 'profit_all_ratio_sum': 0.06201058,
'profit_closed_coin': 6.217e-05, 'profit_closed_coin': 6.217e-05,
'profit_closed_fiat': 0.76748865, 'profit_closed_fiat': 0.76748865,
'profit_closed_percent': 6.2,
'profit_closed_ratio_mean': 0.06201058, 'profit_closed_ratio_mean': 0.06201058,
'profit_closed_percent_mean': 6.2, 'profit_closed_percent_mean': 6.2,
'profit_closed_ratio_sum': 0.06201058, 'profit_closed_ratio_sum': 0.06201058,
@ -770,14 +810,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'trade_id': 1, 'trade_id': 1,
'close_rate_requested': None, 'close_rate_requested': None,
'current_rate': 1.099e-05,
'fee_close': 0.0025, 'fee_close': 0.0025,
'fee_close_cost': None, 'fee_close_cost': None,
'fee_close_currency': None, 'fee_close_currency': None,
'fee_open': 0.0025, 'fee_open': 0.0025,
'fee_open_cost': None, 'fee_open_cost': None,
'fee_open_currency': None, 'fee_open_currency': None,
'open_date': ANY,
'is_open': True, 'is_open': True,
'max_rate': 1.099e-05, 'max_rate': 1.099e-05,
'min_rate': 1.098e-05, 'min_rate': 1.098e-05,

View File

@ -92,7 +92,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], "
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], "
"['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']"
"]") "]")
@ -520,7 +521,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
assert 'Balance:' in result assert 'Balance:' in result
assert 'Est. BTC:' in result assert 'Est. BTC:' in result
assert 'BTC: 12.00000000' in result assert 'BTC: 12.00000000' in result
assert '*XRP:* not showing <1$ amount' in result assert '*XRP:* not showing <0.0001 BTC amount' in result
def test_balance_handle_empty_response(default_conf, update, mocker) -> None: def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
@ -981,6 +982,16 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None
assert 'deadbeef' in msg_mock.call_args_list[0][0][0] assert 'deadbeef' in msg_mock.call_args_list[0][0][0]
assert 'randreason' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0]
context = MagicMock()
context.args = ['XRP/BTC']
msg_mock.reset_mock()
telegram._delete_locks(update=update, context=context)
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
assert 'randreason' in msg_mock.call_args_list[0][0][0]
assert 'XRP/BTC' not in msg_mock.call_args_list[0][0][0]
assert 'deadbeef' not in msg_mock.call_args_list[0][0][0]
def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_static(default_conf, update, mocker) -> None:
@ -1117,8 +1128,10 @@ def test_telegram_trades(mocker, update, default_conf, fee):
msg_mock.call_count == 1 msg_mock.call_count == 1
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0] assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "Profit (" in msg_mock.call_args_list[0][0][0] assert "Profit (" in msg_mock.call_args_list[0][0][0]
assert "Open Date" in msg_mock.call_args_list[0][0][0] assert "Close Date" in msg_mock.call_args_list[0][0][0]
assert "<pre>" in msg_mock.call_args_list[0][0][0] assert "<pre>" in msg_mock.call_args_list[0][0][0]
assert bool(re.search(r"just now[ ]*XRP\/BTC \(#3\) 1.00% \(",
msg_mock.call_args_list[0][0][0]))
def test_telegram_delete_trade(mocker, update, default_conf, fee): def test_telegram_delete_trade(mocker, update, default_conf, fee):
@ -1185,6 +1198,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
msg = { msg = {
'type': RPCMessageType.BUY_NOTIFICATION, 'type': RPCMessageType.BUY_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'limit': 1.099e-05, 'limit': 1.099e-05,
@ -1201,7 +1215,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
telegram.send_msg(msg) telegram.send_msg(msg)
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \ '*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \
@ -1229,12 +1243,14 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'reason': CANCEL_REASON['TIMEOUT'] 'reason': CANCEL_REASON['TIMEOUT']
}) })
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* '
'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') 'Cancelling open buy Order for ETH/BTC (#1). '
'Reason: cancelled due to timeout.')
def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -1245,6 +1261,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
'gain': 'loss', 'gain': 'loss',
@ -1262,7 +1279,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
@ -1274,6 +1291,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
'gain': 'loss', 'gain': 'loss',
@ -1290,7 +1308,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
@ -1310,23 +1328,26 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
'reason': 'Cancelled on exchange' 'reason': 'Cancelled on exchange'
}) })
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. ' == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
'Reason: Cancelled on exchange') ' Reason: Cancelled on exchange')
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
'reason': 'timeout' 'reason': 'timeout'
}) })
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH. Reason: timeout') == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
' Reason: timeout')
# Reset singleton function to avoid random breaks # Reset singleton function to avoid random breaks
telegram._rpc._fiat_converter.convert_amount = old_convamount telegram._rpc._fiat_converter.convert_amount = old_convamount
@ -1373,6 +1394,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION, 'type': RPCMessageType.BUY_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'limit': 1.099e-05, 'limit': 1.099e-05,
@ -1385,7 +1407,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'amount': 1333.3333333333335, 'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1) 'open_date': arrow.utcnow().shift(hours=-1)
}) })
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00001099`\n' '*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n'
@ -1398,6 +1420,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
'gain': 'loss', 'gain': 'loss',
@ -1414,7 +1437,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH\n' assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'

View File

@ -225,3 +225,15 @@ def test__send_msg(default_conf, mocker, caplog):
mocker.patch("freqtrade.rpc.webhook.post", post) mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg) webhook._send_msg(msg)
assert log_has('Could not call webhook url. Exception: ', caplog) assert log_has('Could not call webhook url. Exception: ', caplog)
def test__send_msg_with_json_format(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["format"] = "json"
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
msg = {'text': 'Hello'}
post = MagicMock()
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert post.call_args[1] == {'json': msg}

View File

@ -1,8 +1,10 @@
from math import isclose
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest import pytest
from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes
def generate_test_data(timeframe: str, size: int): def generate_test_data(timeframe: str, size: int):
@ -95,3 +97,38 @@ def test_merge_informative_pair_lower():
with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"): with pytest.raises(ValueError, match=r"Tried to merge a faster timeframe .*"):
merge_informative_pair(data, informative, '1h', '15m', ffill=True) merge_informative_pair(data, informative, '1h', '15m', ffill=True)
def test_stoploss_from_open():
open_price_ranges = [
[0.01, 1.00, 30],
[1, 100, 30],
[100, 10000, 30],
]
current_profit_range = [-0.99, 2, 30]
desired_stop_range = [-0.50, 0.50, 30]
for open_range in open_price_ranges:
for open_price in np.linspace(*open_range):
for desired_stop in np.linspace(*desired_stop_range):
# -1 is not a valid current_profit, should return 1
assert stoploss_from_open(desired_stop, -1) == 1
for current_profit in np.linspace(*current_profit_range):
current_price = open_price * (1 + current_profit)
expected_stop_price = open_price * (1 + desired_stop)
stoploss = stoploss_from_open(desired_stop, current_profit)
assert stoploss >= 0
assert stoploss <= 1
stop_price = current_price * (1 - stoploss)
# there is no correct answer if the expected stop price is above
# the current price
if expected_stop_price > current_price:
assert stoploss == 0
else:
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)

View File

@ -430,7 +430,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--timerange', ':100', '--timerange', ':100',
'--export', '/bar/foo' '--export', '/bar/foo',
'--stake-amount', 'unlimited'
] ]
args = Arguments(arglist).get_parsed_arg() args = Arguments(arglist).get_parsed_arg()
@ -463,6 +464,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'export' in config assert 'export' in config
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
assert 'stake_amount' in config
assert config['stake_amount'] == 'unlimited'
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
@ -787,6 +790,38 @@ def test_validate_max_open_trades(default_conf):
validate_config_consistency(default_conf) validate_config_consistency(default_conf)
def test_validate_price_side(default_conf):
default_conf['order_types'] = {
"buy": "limit",
"sell": "limit",
"stoploss": "limit",
"stoploss_on_exchange": False,
}
# Default should pass
validate_config_consistency(default_conf)
conf = deepcopy(default_conf)
conf['order_types']['buy'] = 'market'
with pytest.raises(OperationalException,
match='Market buy orders require bid_strategy.price_side = "ask".'):
validate_config_consistency(conf)
conf = deepcopy(default_conf)
conf['order_types']['sell'] = 'market'
with pytest.raises(OperationalException,
match='Market sell orders require ask_strategy.price_side = "bid".'):
validate_config_consistency(conf)
# Validate inversed case
conf = deepcopy(default_conf)
conf['order_types']['sell'] = 'market'
conf['order_types']['buy'] = 'market'
conf['ask_strategy']['price_side'] = 'bid'
conf['bid_strategy']['price_side'] = 'ask'
validate_config_consistency(conf)
def test_validate_tsl(default_conf): def test_validate_tsl(default_conf):
default_conf['stoploss'] = 0.0 default_conf['stoploss'] = 0.0
with pytest.raises(OperationalException, match='The config stoploss needs to be different ' with pytest.raises(OperationalException, match='The config stoploss needs to be different '

View File

@ -94,6 +94,7 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
'stoploss': 'limit', 'stoploss': 'limit',
'stoploss_on_exchange': True, 'stoploss_on_exchange': True,
} }
conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf) freqtrade = FreqtradeBot(conf)
assert freqtrade.strategy.order_types['stoploss_on_exchange'] assert freqtrade.strategy.order_types['stoploss_on_exchange']
@ -128,6 +129,7 @@ def test_order_dict_live(default_conf, mocker, caplog) -> None:
'stoploss': 'limit', 'stoploss': 'limit',
'stoploss_on_exchange': True, 'stoploss_on_exchange': True,
} }
conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf) freqtrade = FreqtradeBot(conf)
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
@ -837,17 +839,17 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask ('ask', 4, 5, None, 1, 4), # last not available - uses ask
('ask', 4, 5, None, 0, 4), # last not available - uses ask ('ask', 4, 5, None, 0, 4), # last not available - uses ask
('bid', 10, 20, 10, 0.0, 20), # Full bid side ('bid', 21, 20, 10, 0.0, 20), # Full bid side
('bid', 10, 20, 10, 1.0, 10), # Full last side ('bid', 21, 20, 10, 1.0, 10), # Full last side
('bid', 10, 20, 10, 0.5, 15), # Between bid and last ('bid', 21, 20, 10, 0.5, 15), # Between bid and last
('bid', 10, 20, 10, 0.7, 13), # Between bid and last ('bid', 21, 20, 10, 0.7, 13), # Between bid and last
('bid', 10, 20, 10, 0.3, 17), # Between bid and last ('bid', 21, 20, 10, 0.3, 17), # Between bid and last
('bid', 4, 5, 10, 1.0, 5), # last bigger than bid ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid
('bid', 4, 5, 10, 0.5, 5), # last bigger than bid ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid
('bid', 10, 20, None, 0.5, 20), # last not available - uses bid ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid
('bid', 4, 5, None, 0.5, 5), # last not available - uses bid ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid
('bid', 4, 5, None, 1, 5), # last not available - uses bid ('bid', 6, 5, None, 1, 5), # last not available - uses bid
('bid', 4, 5, None, 0, 5), # last not available - uses bid ('bid', 6, 5, None, 0, 5), # last not available - uses bid
]) ])
def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None: last, last_ab, expected) -> None:
@ -856,7 +858,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
default_conf['bid_strategy']['price_side'] = side default_conf['bid_strategy']['price_side'] = side
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={'ask': ask, 'last': last, 'bid': bid})) return_value={'ask': ask, 'last': last, 'bid': bid})
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
assert not log_has("Using cached buy rate for ETH/BTC.", caplog) assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
@ -2243,6 +2245,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
open_trade.close_profit_abs = 0.001
open_trade.is_open = False open_trade.is_open = False
Trade.session.add(open_trade) Trade.session.add(open_trade)
@ -2290,6 +2293,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
open_trade.close_profit_abs = 0.001
open_trade.is_open = False open_trade.is_open = False
Trade.session.add(open_trade) Trade.session.add(open_trade)
@ -2794,7 +2798,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
sellmock = MagicMock() sellmock = MagicMock(return_value={'id': '12345555'})
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -4108,22 +4112,33 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o
assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog) assert log_has('Sell Price at location 1 from orderbook could not be determined.', caplog)
@pytest.mark.parametrize('side,ask,bid,expected', [ @pytest.mark.parametrize('side,ask,bid,last,last_ab,expected', [
('bid', 10.0, 11.0, 11.0), ('bid', 12.0, 11.0, 11.5, 0.0, 11.0), # full bid side
('bid', 10.0, 11.2, 11.2), ('bid', 12.0, 11.0, 11.5, 1.0, 11.5), # full last side
('bid', 10.0, 11.0, 11.0), ('bid', 12.0, 11.0, 11.5, 0.5, 11.25), # between bid and lat
('bid', 9.8, 11.0, 11.0), ('bid', 12.0, 11.2, 10.5, 0.0, 11.2), # Last smaller than bid
('bid', 0.0001, 0.002, 0.002), ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid
('ask', 10.0, 11.0, 10.0), ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid
('ask', 10.11, 11.2, 10.11), ('bid', 0.003, 0.002, 0.005, 0.0, 0.002),
('ask', 0.001, 0.002, 0.001), ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side
('ask', 0.006, 1.0, 0.006), ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side
('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat
('ask', 12.2, 11.2, 10.5, 0.0, 12.2), # Last smaller than ask
('ask', 12.0, 11.0, 10.5, 1.0, 12.0), # Last smaller than ask - uses ask
('ask', 12.0, 11.2, 10.5, 0.5, 12.0), # Last smaller than ask - uses ask
('ask', 10.0, 11.0, 11.0, 0.0, 10.0),
('ask', 10.11, 11.2, 11.0, 0.0, 10.11),
('ask', 0.001, 0.002, 11.0, 0.0, 0.001),
('ask', 0.006, 1.0, 11.0, 0.0, 0.006),
]) ])
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None: def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask,
last, last_ab, expected) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
default_conf['ask_strategy']['price_side'] = side default_conf['ask_strategy']['price_side'] = side
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid}) default_conf['ask_strategy']['bid_last_balance'] = last_ab
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': ask, 'bid': bid, 'last': last})
pair = "ETH/BTC" pair = "ETH/BTC"
# Test regular mode # Test regular mode
@ -4182,7 +4197,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog):
default_conf['ask_strategy']['price_side'] = 'ask' default_conf['ask_strategy']['price_side'] = 'ask'
pair = "ETH/BTC" pair = "ETH/BTC"
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': None, 'bid': 0.12}) return_value={'ask': None, 'bid': 0.12, 'last': None})
ft = get_patched_freqtradebot(mocker, default_conf) ft = get_patched_freqtradebot(mocker, default_conf)
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
ft.get_sell_rate(pair, True) ft.get_sell_rate(pair, True)
@ -4191,7 +4206,7 @@ def test_get_sell_rate_exception(default_conf, mocker, caplog):
assert ft.get_sell_rate(pair, True) == 0.12 assert ft.get_sell_rate(pair, True) == 0.12
# Reverse sides # Reverse sides
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
return_value={'ask': 0.13, 'bid': None}) return_value={'ask': 0.13, 'bid': None, 'last': None})
with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."): with pytest.raises(PricingError, match=r"Sell-Rate for ETH/BTC was empty."):
ft.get_sell_rate(pair, True) ft.get_sell_rate(pair, True)

View File

@ -1,5 +1,8 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import logging import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import FunctionType
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow import arrow
@ -8,7 +11,7 @@ from sqlalchemy import create_engine
from freqtrade import constants from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
from tests.conftest import create_mock_trades, log_has, log_has_re from tests.conftest import create_mock_trades, log_has, log_has_re
@ -19,14 +22,15 @@ def test_init_create_session(default_conf):
assert 'scoped_session' in type(Trade.session).__name__ assert 'scoped_session' in type(Trade.session).__name__
def test_init_custom_db_url(default_conf, mocker): def test_init_custom_db_url(default_conf, tmpdir):
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) filename = f"{tmpdir}/freqtrade2_test.sqlite"
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) assert not Path(filename).is_file()
default_conf.update({'db_url': f'sqlite:///{filename}'})
init_db(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1 assert Path(filename).is_file()
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_invalid_db_url(default_conf): def test_init_invalid_db_url(default_conf):
@ -47,15 +51,16 @@ def test_init_prod_db(default_conf, mocker):
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_init_dryrun_db(default_conf, mocker): def test_init_dryrun_db(default_conf, tmpdir):
default_conf.update({'dry_run': True}) filename = f"{tmpdir}/freqtrade2_prod.sqlite"
default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) assert not Path(filename).is_file()
default_conf.update({
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) 'dry_run': True,
'db_url': f'sqlite:///{filename}'
})
init_db(default_conf['db_url'], default_conf['dry_run']) init_db(default_conf['db_url'], default_conf['dry_run'])
assert create_engine_mock.call_count == 1 assert Path(filename).is_file()
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite'
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -1039,14 +1044,45 @@ def test_fee_updated(fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_total_open_trades_stakes(fee): @pytest.mark.parametrize('use_db', [True, False])
def test_total_open_trades_stakes(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
res = Trade.total_open_trades_stakes() res = Trade.total_open_trades_stakes()
assert res == 0 assert res == 0
create_mock_trades(fee) create_mock_trades(fee, use_db)
res = Trade.total_open_trades_stakes() res = Trade.total_open_trades_stakes()
assert res == 0.004 assert res == 0.004
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('use_db', [True, False])
def test_get_trades_proxy(fee, use_db):
Trade.use_db = use_db
Trade.reset_trades()
create_mock_trades(fee, use_db)
trades = Trade.get_trades_proxy()
assert len(trades) == 6
assert isinstance(trades[0], Trade)
trades = Trade.get_trades_proxy(is_open=True)
assert len(trades) == 4
assert trades[0].is_open
trades = Trade.get_trades_proxy(is_open=False)
assert len(trades) == 2
assert not trades[0].is_open
opendate = datetime.now(tz=timezone.utc) - timedelta(minutes=15)
assert len(Trade.get_trades_proxy(open_date=opendate)) == 3
Trade.use_db = True
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_get_overall_performance(fee): def test_get_overall_performance(fee):
@ -1172,3 +1208,25 @@ def test_select_order(fee):
assert order.ft_order_side == 'stoploss' assert order.ft_order_side == 'stoploss'
order = trades[4].select_order('sell', False) order = trades[4].select_order('sell', False)
assert order is None assert order is None
def test_Trade_object_idem():
assert issubclass(Trade, LocalTrade)
trade = vars(Trade)
localtrade = vars(LocalTrade)
# Parent (LocalTrade) should have the same attributes
for item in trade:
# Exclude private attributes and open_date (as it's not assigned a default)
if (not item.startswith('_')
and item not in ('delete', 'session', 'query', 'open_date')):
assert item in localtrade
# Fails if only a column is added without corresponding parent field
for item in localtrade:
if (not item.startswith('__')
and item not in ('trades', 'trades_open', 'total_profit')
and type(getattr(LocalTrade, item)) not in (property, FunctionType)):
assert item in trade

View File

@ -3,6 +3,7 @@ import arrow
import pytest import pytest
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.exceptions import OperationalException
def test_parse_timerange_incorrect(): def test_parse_timerange_incorrect():
@ -27,9 +28,13 @@ def test_parse_timerange_incorrect():
timerange = TimeRange.parse_timerange('-1231006505000') timerange = TimeRange.parse_timerange('-1231006505000')
assert TimeRange(None, 'date', 0, 1231006505) == timerange assert TimeRange(None, 'date', 0, 1231006505) == timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'): with pytest.raises(OperationalException, match=r'Incorrect syntax.*'):
TimeRange.parse_timerange('-') TimeRange.parse_timerange('-')
with pytest.raises(OperationalException,
match=r'Start date is after stop date for timerange.*'):
TimeRange.parse_timerange('20100523-20100522')
def test_subtract_start(): def test_subtract_start():
x = TimeRange('date', 'date', 1274486400, 1438214400) x = TimeRange('date', 'date', 1274486400, 1438214400)