Merge branch 'develop' into pr/Axel-CH/5347

This commit is contained in:
Matthias 2021-08-14 09:13:30 +02:00
commit c456cfc312
65 changed files with 1024 additions and 902 deletions

View File

@ -37,7 +37,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even
Exchanges confirmed working by the community: Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/) - [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kukoin](https://www.kucoin.com/) - [X] [Kucoin](https://www.kucoin.com/)
## Documentation ## Documentation

View File

@ -74,7 +74,5 @@ fi
docker images docker images
if [ $? -ne 0 ]; then # Cleanup old images from arm64 node.
echo "failed building image" docker image prune -a --force --filter "until=24h"
return 1
fi

View File

@ -35,7 +35,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
* Calls `check_buy_timeout()` strategy callback for open buy orders. * Calls `check_buy_timeout()` strategy callback for open buy orders.
* Calls `check_sell_timeout()` strategy callback for open sell orders. * Calls `check_sell_timeout()` strategy callback for open sell orders.
* Verifies existing positions and eventually places sell orders. * Verifies existing positions and eventually places sell orders.
* Considers stoploss, ROI and sell-signal. * Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. * Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
* Check if trade-slots are still available (if `max_open_trades` is reached). * Check if trade-slots are still available (if `max_open_trades` is reached).
@ -53,9 +53,10 @@ This loop will be repeated again and again until the bot is stopped.
* Load historic data for configured pairlist. * Load historic data for configured pairlist.
* 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.
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
* Generate backtest report output * Generate backtest report output
!!! Note !!! Note

View File

@ -240,11 +240,18 @@ The `IProtection` parent class provides a helper method for this in `calculate_l
!!! Note !!! Note
This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade. This section is a Work in Progress and is not a complete guide on how to test a new exchange with Freqtrade.
!!! Note
Make sure to use an up-to-date version of CCXT before running any of the below tests.
You can get the latest version of ccxt by running `pip install -U ccxt` with activated virtual environment.
Native docker is not supported for these tests, however the available dev-container will support all required actions and eventually necessary changes.
Most exchanges supported by CCXT should work out of the box. Most exchanges supported by CCXT should work out of the box.
To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`.
Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar).
Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded).
### Stoploss On Exchange ### Stoploss On Exchange
Check if the new exchange supports Stoploss on Exchange orders through their API. Check if the new exchange supports Stoploss on Exchange orders through their API.

View File

@ -253,7 +253,7 @@ We continue to define hyperoptable parameters:
class MyAwesomeStrategy(IStrategy): class MyAwesomeStrategy(IStrategy):
buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy") buy_adx = DecimalParameter(20, 40, decimals=1, default=30.1, space="buy")
buy_rsi = IntParameter(20, 40, default=30, space="buy") buy_rsi = IntParameter(20, 40, default=30, space="buy")
buy_adx_enabled = CategoricalParameter([True, False], default=True, space="buy") buy_adx_enabled = BooleanParameter(default=True, space="buy")
buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy") buy_rsi_enabled = CategoricalParameter([True, False], default=False, space="buy")
buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy") buy_trigger = CategoricalParameter(["bb_lower", "macd_cross_signal"], default="bb_lower", space="buy")
``` ```
@ -316,6 +316,7 @@ There are four parameter types each suited for different purposes.
* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases.
* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. * `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.
* `CategoricalParameter` - defines a parameter with a predetermined number of choices. * `CategoricalParameter` - defines a parameter with a predetermined number of choices.
* `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters.
!!! Tip "Disabling parameter optimization" !!! Tip "Disabling parameter optimization"
Each parameter takes two boolean parameters: Each parameter takes two boolean parameters:
@ -326,7 +327,7 @@ There are four parameter types each suited for different purposes.
!!! Warning !!! Warning
Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case.
### Optimizing an indicator parameter ## Optimizing an indicator parameter
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy. Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
@ -336,8 +337,8 @@ from functools import reduce
import talib.abstract as ta import talib.abstract as ta
from freqtrade.strategy import IStrategy from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter IStrategy, IntParameter)
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy): class MyAwesomeStrategy(IStrategy):
@ -413,6 +414,98 @@ While this strategy is most likely too simple to provide consistent profit, it s
While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values). While this may slow down the hyperopt startup speed, the overall performance will increase as the Hyperopt execution itself may pick the same value for multiple epochs (changing other values).
You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space. You should however try to use space ranges as small as possible. Every new column will require more memory, and every possibility hyperopt can try will increase the search space.
## Optimizing protections
Freqtrade can also optimize protections. How you optimize protections is up to you, and the following should be considered as example only.
The strategy will simply need to define the "protections" entry as property returning a list of protection configurations.
``` python
from pandas import DataFrame
from functools import reduce
import talib.abstract as ta
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter)
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyAwesomeStrategy(IStrategy):
stoploss = -0.05
timeframe = '15m'
# Define the parameter spaces
cooldown_lookback = IntParameter(2, 48, default=5, space="protection", optimize=True)
stop_duration = IntParameter(12, 200, default=5, space="protection", optimize=True)
use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True)
@property
def protections(self):
prot = []
prot.append({
"method": "CooldownPeriod",
"stop_duration_candles": self.cooldown_lookback.value
})
if self.use_stop_protection.value:
prot.append({
"method": "StoplossGuard",
"lookback_period_candles": 24 * 3,
"trade_limit": 4,
"stop_duration_candles": self.stop_duration.value,
"only_per_pair": False
})
return protection
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ...
```
You can then run hyperopt as follows:
`freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy --spaces protection`
!!! Note
The protection space is not part of the default space, and is only available with the Parameters Hyperopt interface, not with the legacy hyperopt interface (which required separate hyperopt files).
Freqtrade will also automatically change the "--enable-protections" flag if the protection space is selected.
!!! Warning
If protections are defined as property, entries from the configuration will be ignored.
It is therefore recommended to not define protections in the configuration.
### Migrating from previous property setups
A migration from a previous setup is pretty simple, and can be accomplished by converting the protections entry to a property.
In simple terms, the following configuration will be converted to the below.
``` python
class MyAwesomeStrategy(IStrategy):
protections = [
{
"method": "CooldownPeriod",
"stop_duration_candles": 4
}
]
```
Result
``` python
class MyAwesomeStrategy(IStrategy):
@property
def protections(self):
return [
{
"method": "CooldownPeriod",
"stop_duration_candles": 4
}
]
```
You will then obviously also change potential interesting entries to parameters to allow hyper-optimization.
## Loss-functions ## Loss-functions
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.

View File

@ -58,7 +58,7 @@ This option must be configured along with `exchange.skip_pair_validation` in the
When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume.
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. When used in the leading position of the chain of Pairlist Handlers, the `pair_whitelist` configuration setting is ignored. Instead, `VolumePairList` selects the top assets from all available markets with matching stake-currency on the exchange.
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
@ -74,11 +74,14 @@ Filtering instances (not the first position in the list) will not apply any cach
"method": "VolumePairList", "method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 1800 "refresh_period": 1800
} }
], ],
``` ```
You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange.
`VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles. `VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles.
For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days: For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days:
@ -89,6 +92,7 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl
"method": "VolumePairList", "method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 86400, "refresh_period": 86400,
"lookback_days": 7 "lookback_days": 7
} }
@ -109,6 +113,7 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl
"method": "VolumePairList", "method": "VolumePairList",
"number_assets": 20, "number_assets": 20,
"sort_key": "quoteVolume", "sort_key": "quoteVolume",
"min_value": 0,
"refresh_period": 3600, "refresh_period": 3600,
"lookback_timeframe": "1h", "lookback_timeframe": "1h",
"lookback_period": 72 "lookback_period": 72

View File

@ -15,6 +15,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
!!! Note "Backtesting" !!! Note "Backtesting"
Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag. Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag.
!!! Warning "Setting protections from the configuration"
Setting protections from the configuration via `"protections": [],` key should be considered deprecated and will be removed in a future version.
It is also no longer guaranteed that your protections apply to the strategy in cases where the strategy defines [protections as property](hyperopt.md#optimizing-protections).
### Available Protections ### Available Protections
* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window.
@ -47,15 +51,17 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
``` python ``` python
protections = [ @property
{ def protections(self):
"method": "StoplossGuard", return [
"lookback_period_candles": 24, {
"trade_limit": 4, "method": "StoplossGuard",
"stop_duration_candles": 4, "lookback_period_candles": 24,
"only_per_pair": False "trade_limit": 4,
} "stop_duration_candles": 4,
] "only_per_pair": False
}
]
``` ```
!!! Note !!! Note
@ -69,15 +75,17 @@ protections = [
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used. The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
``` python ``` python
protections = [ @property
{ def protections(self):
"method": "MaxDrawdown", return [
"lookback_period_candles": 48, {
"trade_limit": 20, "method": "MaxDrawdown",
"stop_duration_candles": 12, "lookback_period_candles": 48,
"max_allowed_drawdown": 0.2 "trade_limit": 20,
}, "stop_duration_candles": 12,
] "max_allowed_drawdown": 0.2
},
]
``` ```
#### Low Profit Pairs #### Low Profit Pairs
@ -88,15 +96,17 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
``` python ``` python
protections = [ @property
{ def protections(self):
"method": "LowProfitPairs", return [
"lookback_period_candles": 6, {
"trade_limit": 2, "method": "LowProfitPairs",
"stop_duration": 60, "lookback_period_candles": 6,
"required_profit": 0.02 "trade_limit": 2,
} "stop_duration": 60,
] "required_profit": 0.02
}
]
``` ```
#### Cooldown Period #### Cooldown Period
@ -106,12 +116,14 @@ protections = [
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
``` python ``` python
protections = [ @property
{ def protections(self):
"method": "CooldownPeriod", return [
"stop_duration_candles": 2 {
} "method": "CooldownPeriod",
] "stop_duration_candles": 2
}
]
``` ```
!!! Note !!! Note
@ -136,39 +148,42 @@ from freqtrade.strategy import IStrategy
class AwesomeStrategy(IStrategy) class AwesomeStrategy(IStrategy)
timeframe = '1h' timeframe = '1h'
protections = [
{ @property
"method": "CooldownPeriod", def protections(self):
"stop_duration_candles": 5 return [
}, {
{ "method": "CooldownPeriod",
"method": "MaxDrawdown", "stop_duration_candles": 5
"lookback_period_candles": 48, },
"trade_limit": 20, {
"stop_duration_candles": 4, "method": "MaxDrawdown",
"max_allowed_drawdown": 0.2 "lookback_period_candles": 48,
}, "trade_limit": 20,
{ "stop_duration_candles": 4,
"method": "StoplossGuard", "max_allowed_drawdown": 0.2
"lookback_period_candles": 24, },
"trade_limit": 4, {
"stop_duration_candles": 2, "method": "StoplossGuard",
"only_per_pair": False "lookback_period_candles": 24,
}, "trade_limit": 4,
{ "stop_duration_candles": 2,
"method": "LowProfitPairs", "only_per_pair": False
"lookback_period_candles": 6, },
"trade_limit": 2, {
"stop_duration_candles": 60, "method": "LowProfitPairs",
"required_profit": 0.02 "lookback_period_candles": 6,
}, "trade_limit": 2,
{ "stop_duration_candles": 60,
"method": "LowProfitPairs", "required_profit": 0.02
"lookback_period_candles": 24, },
"trade_limit": 4, {
"stop_duration_candles": 2, "method": "LowProfitPairs",
"required_profit": 0.01 "lookback_period_candles": 24,
} "trade_limit": 4,
] "stop_duration_candles": 2,
"required_profit": 0.01
}
]
# ... # ...
``` ```

View File

@ -47,7 +47,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
Exchanges confirmed working by the community: Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/) - [X] [Bitvavo](https://bitvavo.com/)
- [X] [Kukoin](https://www.kucoin.com/) - [X] [Kucoin](https://www.kucoin.com/)
## Requirements ## Requirements

View File

@ -193,7 +193,7 @@ def deploy_new_config(config_path: Path, selections: Dict[str, Any]) -> None:
selections['exchange'] = render_template( selections['exchange'] = render_template(
templatefile=f"subtemplates/exchange_{exchange_template}.j2", templatefile=f"subtemplates/exchange_{exchange_template}.j2",
arguments=selections arguments=selections
) )
except TemplateNotFound: except TemplateNotFound:
selections['exchange'] = render_template( selections['exchange'] = render_template(
templatefile="subtemplates/exchange_generic.j2", templatefile="subtemplates/exchange_generic.j2",

View File

@ -218,7 +218,7 @@ AVAILABLE_CLI_OPTIONS = {
"spaces": Arg( "spaces": Arg(
'--spaces', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list.', help='Specify which parameters to hyperopt. Space-separated list.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'], choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
nargs='+', nargs='+',
default='default', default='default',
), ),

View File

@ -38,15 +38,15 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
indicators = render_template_with_fallback( indicators = render_template_with_fallback(
templatefile=f"subtemplates/indicators_{subtemplate}.j2", templatefile=f"subtemplates/indicators_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/indicators_{fallback}.j2", templatefallbackfile=f"subtemplates/indicators_{fallback}.j2",
) )
buy_trend = render_template_with_fallback( buy_trend = render_template_with_fallback(
templatefile=f"subtemplates/buy_trend_{subtemplate}.j2", templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2", templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2",
) )
sell_trend = render_template_with_fallback( sell_trend = render_template_with_fallback(
templatefile=f"subtemplates/sell_trend_{subtemplate}.j2", templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2", templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2",
) )
plot_config = render_template_with_fallback( plot_config = render_template_with_fallback(
templatefile=f"subtemplates/plot_config_{subtemplate}.j2", templatefile=f"subtemplates/plot_config_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2", templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2",
@ -97,19 +97,19 @@ def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: st
buy_guards = render_template_with_fallback( buy_guards = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
) )
sell_guards = render_template_with_fallback( sell_guards = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
) )
buy_space = render_template_with_fallback( buy_space = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
) )
sell_space = render_template_with_fallback( sell_space = render_template_with_fallback(
templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2", templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2", templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
) )
strategy_text = render_template(templatefile='base_hyperopt.py.j2', strategy_text = render_template(templatefile='base_hyperopt.py.j2',
arguments={"hyperopt": hyperopt_name, arguments={"hyperopt": hyperopt_name,

View File

@ -1,6 +1,6 @@
import logging import logging
from operator import itemgetter from operator import itemgetter
from typing import Any, Dict, List from typing import Any, Dict
from colorama import init as colorama_init from colorama import init as colorama_init
@ -28,30 +28,12 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
no_details = config.get('hyperopt_list_no_details', False) no_details = config.get('hyperopt_list_no_details', False)
no_header = False no_header = False
filteroptions = {
'only_best': config.get('hyperopt_list_best', False),
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
}
results_file = get_latest_hyperopt_file( results_file = get_latest_hyperopt_file(
config['user_data_dir'] / 'hyperopt_results', config['user_data_dir'] / 'hyperopt_results',
config.get('hyperoptexportfilename')) config.get('hyperoptexportfilename'))
# Previous evaluations # Previous evaluations
epochs = HyperoptTools.load_previous_results(results_file) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions)
if print_colorized: if print_colorized:
colorama_init(autoreset=True) colorama_init(autoreset=True)
@ -59,7 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if not export_csv: if not export_csv:
try: try:
print(HyperoptTools.get_result_table(config, epochs, total_epochs, print(HyperoptTools.get_result_table(config, epochs, total_epochs,
not filteroptions['only_best'], not config.get('hyperopt_list_best', False),
print_colorized, 0)) print_colorized, 0))
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
@ -71,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if epochs and export_csv: if epochs and export_csv:
HyperoptTools.export_csv_file( HyperoptTools.export_csv_file(
config, epochs, total_epochs, not filteroptions['only_best'], export_csv config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
) )
@ -91,26 +73,9 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
n = config.get('hyperopt_show_index', -1) n = config.get('hyperopt_show_index', -1)
filteroptions = {
'only_best': config.get('hyperopt_list_best', False),
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None)
}
# Previous evaluations # Previous evaluations
epochs = HyperoptTools.load_previous_results(results_file) epochs, total_epochs = HyperoptTools.load_filtered_results(results_file, config)
total_epochs = len(epochs)
epochs = hyperopt_filter_epochs(epochs, filteroptions)
filtered_epochs = len(epochs) filtered_epochs = len(epochs)
if n > filtered_epochs: if n > filtered_epochs:
@ -137,138 +102,3 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, HyperoptTools.show_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:
"""
Filter our items from the list of hyperopt results
TODO: after 2021.5 remove all "legacy" mode queries.
"""
if filteroptions['only_best']:
epochs = [x for x in epochs if x['is_best']]
if filteroptions['only_profitable']:
epochs = [x for x in epochs if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total', 0)) > 0]
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
logger.info(f"{len(epochs)} " +
("best " if filteroptions['only_best'] else "") +
("profitable " if filteroptions['only_profitable'] else "") +
"epochs found.")
return epochs
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
"""
Filter epochs with trade-counts > trades
"""
return [
x for x in epochs
if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades', 0)
) > trade_count
]
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_trades'] > 0:
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
if filteroptions['filter_max_trades'] > 0:
epochs = [
x for x in epochs
if x['results_metrics'].get(
'trade_count', x['results_metrics'].get('total_trades')
) < filteroptions['filter_max_trades']
]
return epochs
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def get_duration_value(x):
# Duration in minutes ...
if 'duration' in x['results_metrics']:
return x['results_metrics']['duration']
else:
# New mode
if 'holding_avg_s' in x['results_metrics']:
avg = x['results_metrics']['holding_avg_s']
return avg // 60
raise OperationalException(
"Holding-average not available. Please omit the filter on average time, "
"or rerun hyperopt with this version")
if filteroptions['filter_min_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) > filteroptions['filter_min_avg_time']
]
if filteroptions['filter_max_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) < filteroptions['filter_max_avg_time']
]
return epochs
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) > filteroptions['filter_min_avg_profit']
]
if filteroptions['filter_max_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
) < filteroptions['filter_max_avg_profit']
]
if filteroptions['filter_min_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0)
) > filteroptions['filter_min_total_profit']
]
if filteroptions['filter_max_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get(
'profit', x['results_metrics'].get('profit_total_abs', 0)
) < filteroptions['filter_max_total_profit']
]
return epochs
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
if filteroptions['filter_max_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
return epochs

View File

@ -51,10 +51,10 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
if not is_exchange_known_ccxt(exchange): if not is_exchange_known_ccxt(exchange):
raise OperationalException( raise OperationalException(
f'Exchange "{exchange}" is not known to the ccxt library ' f'Exchange "{exchange}" is not known to the ccxt library '
f'and therefore not available for the bot.\n' f'and therefore not available for the bot.\n'
f'The following exchanges are available for Freqtrade: ' f'The following exchanges are available for Freqtrade: '
f'{", ".join(available_exchanges())}' f'{", ".join(available_exchanges())}'
) )
valid, reason = validate_exchange(exchange) valid, reason = validate_exchange(exchange)

View File

@ -115,7 +115,7 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get('stoploss') == 0.0: if conf.get('stoploss') == 0.0:
raise OperationalException( raise OperationalException(
'The config stoploss needs to be different from 0 to avoid problems with sell orders.' 'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
) )
# Skip if trailing stoploss is not activated # Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False): if not conf.get('trailing_stop', False):
return return
@ -180,7 +180,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
raise OperationalException( raise OperationalException(
"Protections must specify either `stop_duration` or `stop_duration_candles`.\n" "Protections must specify either `stop_duration` or `stop_duration_candles`.\n"
f"Please fix the protection {prot.get('method')}" f"Please fix the protection {prot.get('method')}"
) )
if ('lookback_period' in prot and 'lookback_period_candles' in prot): if ('lookback_period' in prot and 'lookback_period_candles' in prot):
raise OperationalException( raise OperationalException(

View File

@ -108,5 +108,8 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
raise OperationalException( raise OperationalException(
"Both 'timeframe' and 'ticker_interval' detected." "Both 'timeframe' and 'ticker_interval' detected."
"Please remove 'ticker_interval' from your configuration to continue operating." "Please remove 'ticker_interval' from your configuration to continue operating."
) )
config['timeframe'] = config['ticker_interval'] config['timeframe'] = config['ticker_interval']
if 'protections' in config:
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")

View File

@ -280,7 +280,7 @@ CONF_SCHEMA = {
'type': 'string', 'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS, 'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off' 'default': 'off'
}, },
} }
}, },
'reload': {'type': 'boolean'}, 'reload': {'type': 'boolean'},

View File

@ -151,7 +151,7 @@ class Edge:
# Fake run-mode to Edge # Fake run-mode to Edge
prior_rm = self.config['runmode'] prior_rm = self.config['runmode']
self.config['runmode'] = RunMode.EDGE self.config['runmode'] = RunMode.EDGE
preprocessed = self.strategy.ohlcvdata_to_dataframe(data) preprocessed = self.strategy.advise_all_indicators(data)
self.config['runmode'] = prior_rm self.config['runmode'] = prior_rm
# Print timeframe # Print timeframe
@ -231,12 +231,12 @@ class Edge:
'Minimum expectancy and minimum winrate are met only for %s,' 'Minimum expectancy and minimum winrate are met only for %s,'
' so other pairs are filtered out.', ' so other pairs are filtered out.',
self._final_pairs self._final_pairs
) )
else: else:
logger.info( logger.info(
'Edge removed all pairs as no pair with minimum expectancy ' 'Edge removed all pairs as no pair with minimum expectancy '
'and minimum winrate was found !' 'and minimum winrate was found !'
) )
return self._final_pairs return self._final_pairs
@ -247,7 +247,7 @@ class Edge:
final = [] final = []
for pair, info in self._cached_pairs.items(): for pair, info in self._cached_pairs.items():
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
final.append({ final.append({
'Pair': pair, 'Pair': pair,
'Winrate': info.winrate, 'Winrate': info.winrate,

View File

@ -618,6 +618,8 @@ class Exchange:
if self.exchange_has('fetchL2OrderBook'): if self.exchange_has('fetchL2OrderBook'):
ob = self.fetch_l2_order_book(pair, 20) ob = self.fetch_l2_order_book(pair, 20)
ob_type = 'asks' if side == 'buy' else 'bids' ob_type = 'asks' if side == 'buy' else 'bids'
slippage = 0.05
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
remaining_amount = amount remaining_amount = amount
filled_amount = 0 filled_amount = 0
@ -626,7 +628,9 @@ class Exchange:
book_entry_coin_volume = book_entry[1] book_entry_coin_volume = book_entry[1]
if remaining_amount > 0: if remaining_amount > 0:
if remaining_amount < book_entry_coin_volume: if remaining_amount < book_entry_coin_volume:
# Orderbook at this slot bigger than remaining amount
filled_amount += remaining_amount * book_entry_price filled_amount += remaining_amount * book_entry_price
break
else: else:
filled_amount += book_entry_coin_volume * book_entry_price filled_amount += book_entry_coin_volume * book_entry_price
remaining_amount -= book_entry_coin_volume remaining_amount -= book_entry_coin_volume
@ -635,7 +639,14 @@ class Exchange:
else: else:
# If remaining_amount wasn't consumed completely (break was not called) # If remaining_amount wasn't consumed completely (break was not called)
filled_amount += remaining_amount * book_entry_price filled_amount += remaining_amount * book_entry_price
forecast_avg_filled_price = filled_amount / amount forecast_avg_filled_price = max(filled_amount, 0) / amount
# Limit max. slippage to specified value
if side == 'buy':
forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
else:
forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
return self.price_to_precision(pair, forecast_avg_filled_price) return self.price_to_precision(pair, forecast_avg_filled_price)
return rate return rate

View File

@ -44,7 +44,7 @@ def main(sysargv: List[str] = None) -> None:
"as `freqtrade trade [options...]`.\n" "as `freqtrade trade [options...]`.\n"
"To see the full list of options available, please use " "To see the full list of options available, please use "
"`freqtrade --help` or `freqtrade <command> --help`." "`freqtrade --help` or `freqtrade <command> --help`."
) )
except SystemExit as e: except SystemExit as e:
return_code = e return_code = e

View File

@ -130,6 +130,9 @@ class Backtesting:
self.abort = False self.abort = False
def __del__(self): def __del__(self):
self.cleanup()
def cleanup(self):
LoggingMixin.show_output = True LoggingMixin.show_output = True
PairLocks.use_db = True PairLocks.use_db = True
Trade.use_db = True Trade.use_db = True
@ -146,6 +149,8 @@ class Backtesting:
# since a "perfect" stoploss-sell is assumed anyway # since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
self.strategy.order_types['stoploss_on_exchange'] = False self.strategy.order_types['stoploss_on_exchange'] = False
def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False): if self.config.get('enable_protections', False):
conf = self.config conf = self.config
if hasattr(strategy, 'protections'): if hasattr(strategy, 'protections'):
@ -194,6 +199,7 @@ class Backtesting:
Trade.reset_trades() Trade.reset_trades()
self.rejected_trades = 0 self.rejected_trades = 0
self.dataprovider.clear_cache() self.dataprovider.clear_cache()
self._load_protections(self.strategy)
def check_abort(self): def check_abort(self):
""" """
@ -212,7 +218,7 @@ class Backtesting:
""" """
# Every change to this headers list must evaluate further usages of the resulting tuple # Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top # and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag']
data: Dict = {} data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed)) self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -220,13 +226,10 @@ class Backtesting:
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
self.check_abort() self.check_abort()
self.progress.increment() self.progress.increment()
has_buy_tag = 'buy_tag' in pair_data
headers = headers + ['buy_tag'] if has_buy_tag else headers
if not pair_data.empty: if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
if has_buy_tag: pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
df_analyzed = self.strategy.advise_sell( df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
@ -237,14 +240,13 @@ class Backtesting:
# from the previous candle # from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
if has_buy_tag: df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.drop(df_analyzed.head(1).index, inplace=True)
# Update dataprovider cache # Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
# 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] = df_analyzed[headers].values.tolist() data[pair] = df_analyzed[headers].values.tolist()
@ -317,14 +319,14 @@ class Backtesting:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_candle_time, sell_row[BUY_IDX],
sell_row[SELL_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.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_candle_time
trade.sell_reason = sell.sell_reason trade.sell_reason = sell.sell_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) 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)
@ -336,7 +338,7 @@ class Backtesting:
rate=closerate, rate=closerate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=sell.sell_reason, sell_reason=sell.sell_reason,
current_time=sell_row[DATE_IDX].to_pydatetime()): current_time=sell_candle_time):
return None return None
trade.close(closerate, show_msg=False) trade.close(closerate, show_msg=False)
@ -460,6 +462,8 @@ class Backtesting:
for i, pair in enumerate(data): for i, pair in enumerate(data):
row_index = indexes[pair] row_index = indexes[pair]
try: try:
# Row is treated as "current incomplete candle".
# Buy / sell signals are shifted by 1 to compensate for this.
row = data[pair][row_index] row = data[pair][row_index]
except IndexError: except IndexError:
# missing Data for one pair at the end. # missing Data for one pair at the end.
@ -471,8 +475,8 @@ class Backtesting:
continue continue
row_index += 1 row_index += 1
self.dataprovider._set_dataframe_max_index(row_index)
indexes[pair] = row_index indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index)
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected # max_open_trades must be respected
@ -496,7 +500,7 @@ class Backtesting:
open_trades[pair].append(trade) open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
for trade in open_trades[pair]: for trade in list(open_trades[pair]):
# 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 occurred # Sell occurred
@ -527,7 +531,8 @@ class Backtesting:
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
} }
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange): def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
timerange: TimeRange):
self.progress.init_step(BacktestState.ANALYZE, 0) self.progress.init_step(BacktestState.ANALYZE, 0)
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
@ -546,7 +551,7 @@ class Backtesting:
max_open_trades = 0 max_open_trades = 0
# need to reprocess data every time to populate signals # need to reprocess data every time to populate signals
preprocessed = self.strategy.ohlcvdata_to_dataframe(data) preprocessed = self.strategy.advise_all_indicators(data)
# Trim startup period from analyzed dataframe # Trim startup period from analyzed dataframe
preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup) preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup)

View File

@ -66,6 +66,7 @@ class Hyperopt:
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.buy_space: List[Dimension] = [] self.buy_space: List[Dimension] = []
self.sell_space: List[Dimension] = [] self.sell_space: List[Dimension] = []
self.protection_space: List[Dimension] = []
self.roi_space: List[Dimension] = [] self.roi_space: List[Dimension] = []
self.stoploss_space: List[Dimension] = [] self.stoploss_space: List[Dimension] = []
self.trailing_space: List[Dimension] = [] self.trailing_space: List[Dimension] = []
@ -191,6 +192,8 @@ class Hyperopt:
result['buy'] = {p.name: params.get(p.name) for p in self.buy_space} result['buy'] = {p.name: params.get(p.name) for p in self.buy_space}
if HyperoptTools.has_space(self.config, 'sell'): if HyperoptTools.has_space(self.config, 'sell'):
result['sell'] = {p.name: params.get(p.name) for p in self.sell_space} result['sell'] = {p.name: params.get(p.name) for p in self.sell_space}
if HyperoptTools.has_space(self.config, 'protection'):
result['protection'] = {p.name: params.get(p.name) for p in self.protection_space}
if HyperoptTools.has_space(self.config, 'roi'): if HyperoptTools.has_space(self.config, 'roi'):
result['roi'] = {str(k): v for k, v in result['roi'] = {str(k): v for k, v in
self.custom_hyperopt.generate_roi_table(params).items()} self.custom_hyperopt.generate_roi_table(params).items()}
@ -241,6 +244,12 @@ class Hyperopt:
""" """
Assign the dimensions in the hyperoptimization space. Assign the dimensions in the hyperoptimization space.
""" """
if self.auto_hyperopt and HyperoptTools.has_space(self.config, 'protection'):
# Protections can only be optimized when using the Parameter interface
logger.debug("Hyperopt has 'protection' space")
# Enable Protections if protection space is selected.
self.config['enable_protections'] = True
self.protection_space = self.custom_hyperopt.protection_space()
if HyperoptTools.has_space(self.config, 'buy'): if HyperoptTools.has_space(self.config, 'buy'):
logger.debug("Hyperopt has 'buy' space") logger.debug("Hyperopt has 'buy' space")
@ -261,8 +270,8 @@ class Hyperopt:
if HyperoptTools.has_space(self.config, 'trailing'): if HyperoptTools.has_space(self.config, 'trailing'):
logger.debug("Hyperopt has 'trailing' space") logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.trailing_space() self.trailing_space = self.custom_hyperopt.trailing_space()
self.dimensions = (self.buy_space + self.sell_space + self.roi_space + self.dimensions = (self.buy_space + self.sell_space + self.protection_space
self.stoploss_space + self.trailing_space) + self.roi_space + self.stoploss_space + self.trailing_space)
def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
""" """
@ -282,6 +291,12 @@ class Hyperopt:
self.backtesting.strategy.advise_sell = ( # type: ignore self.backtesting.strategy.advise_sell = ( # type: ignore
self.custom_hyperopt.sell_strategy_generator(params_dict)) self.custom_hyperopt.sell_strategy_generator(params_dict))
if HyperoptTools.has_space(self.config, 'protection'):
for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'):
if attr.optimize:
# noinspection PyProtectedMember
attr.value = params_dict[attr_name]
if HyperoptTools.has_space(self.config, 'roi'): if HyperoptTools.has_space(self.config, 'roi'):
self.backtesting.strategy.minimal_roi = ( # type: ignore self.backtesting.strategy.minimal_roi = ( # type: ignore
self.custom_hyperopt.generate_roi_table(params_dict)) self.custom_hyperopt.generate_roi_table(params_dict))
@ -379,7 +394,7 @@ class Hyperopt:
data, timerange = self.backtesting.load_bt_data() data, timerange = self.backtesting.load_bt_data()
logger.info("Dataload complete. Calculating indicators") logger.info("Dataload complete. Calculating indicators")
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) preprocessed = self.backtesting.strategy.advise_all_indicators(data)
# Trim startup period from analyzed dataframe to get correct dates for output. # Trim startup period from analyzed dataframe to get correct dates for output.
processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup)
@ -444,9 +459,9 @@ class Hyperopt:
' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']',
] ]
with progressbar.ProgressBar( with progressbar.ProgressBar(
max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False,
widgets=widgets widgets=widgets
) as pbar: ) as pbar:
EVALS = ceil(self.total_epochs / jobs) EVALS = ceil(self.total_epochs / jobs)
for i in range(EVALS): for i in range(EVALS):
# Correct the number of epochs to be processed for the last # Correct the number of epochs to be processed for the last

View File

@ -73,6 +73,9 @@ class HyperOptAuto(IHyperOpt):
def sell_indicator_space(self) -> List['Dimension']: def sell_indicator_space(self) -> List['Dimension']:
return self._get_indicator_space('sell', 'sell_indicator_space') return self._get_indicator_space('sell', 'sell_indicator_space')
def protection_space(self) -> List['Dimension']:
return self._get_indicator_space('protection', 'indicator_space')
def generate_roi_table(self, params: Dict) -> Dict[int, float]: def generate_roi_table(self, params: Dict) -> Dict[int, float]:
return self._get_func('generate_roi_table')(params) return self._get_func('generate_roi_table')(params)

View File

@ -0,0 +1,128 @@
import logging
from typing import List
from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def hyperopt_filter_epochs(epochs: List, filteroptions: dict, log: bool = True) -> List:
"""
Filter our items from the list of hyperopt results
"""
if filteroptions['only_best']:
epochs = [x for x in epochs if x['is_best']]
if filteroptions['only_profitable']:
epochs = [x for x in epochs
if x['results_metrics'].get('profit_total', 0) > 0]
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
if log:
logger.info(f"{len(epochs)} " +
("best " if filteroptions['only_best'] else "") +
("profitable " if filteroptions['only_profitable'] else "") +
"epochs found.")
return epochs
def _hyperopt_filter_epochs_trade(epochs: List, trade_count: int):
"""
Filter epochs with trade-counts > trades
"""
return [
x for x in epochs if x['results_metrics'].get('total_trades', 0) > trade_count
]
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_trades'] > 0:
epochs = _hyperopt_filter_epochs_trade(epochs, filteroptions['filter_min_trades'])
if filteroptions['filter_max_trades'] > 0:
epochs = [
x for x in epochs
if x['results_metrics'].get('total_trades') < filteroptions['filter_max_trades']
]
return epochs
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
def get_duration_value(x):
# Duration in minutes ...
if 'holding_avg_s' in x['results_metrics']:
avg = x['results_metrics']['holding_avg_s']
return avg // 60
raise OperationalException(
"Holding-average not available. Please omit the filter on average time, "
"or rerun hyperopt with this version")
if filteroptions['filter_min_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) > filteroptions['filter_min_avg_time']
]
if filteroptions['filter_max_avg_time'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if get_duration_value(x) < filteroptions['filter_max_avg_time']
]
return epochs
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_mean', 0) * 100
> filteroptions['filter_min_avg_profit']
]
if filteroptions['filter_max_avg_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_mean', 0) * 100
< filteroptions['filter_max_avg_profit']
]
if filteroptions['filter_min_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_total_abs', 0)
> filteroptions['filter_min_total_profit']
]
if filteroptions['filter_max_total_profit'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [
x for x in epochs
if x['results_metrics'].get('profit_total_abs', 0)
< filteroptions['filter_max_total_profit']
]
return epochs
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
if filteroptions['filter_min_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
if filteroptions['filter_max_objective'] is not None:
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
return epochs

View File

@ -57,6 +57,13 @@ class IHyperOpt(ABC):
""" """
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell')) raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
def protection_space(self) -> List[Dimension]:
"""
Create a protection space.
Only supported by the Parameter interface.
"""
raise OperationalException(_format_exception_message('indicator_space', 'protection'))
def indicator_space(self) -> List[Dimension]: def indicator_space(self) -> List[Dimension]:
""" """
Create an indicator space. Create an indicator space.

View File

@ -4,7 +4,7 @@ import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, Iterator, List, Optional, Tuple
import numpy as np import numpy as np
import rapidjson import rapidjson
@ -15,6 +15,7 @@ from pandas import isna, json_normalize
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2 from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -82,53 +83,77 @@ class HyperoptTools():
""" """
Tell if the space value is contained in the configuration Tell if the space value is contained in the configuration
""" """
# The 'trailing' space is not included in the 'default' set of spaces # 'trailing' and 'protection spaces are not included in the 'default' set of spaces
if space == 'trailing': if space in ('trailing', 'protection'):
return any(s in config['spaces'] for s in [space, 'all']) return any(s in config['spaces'] for s in [space, 'all'])
else: else:
return any(s in config['spaces'] for s in [space, 'all', 'default']) return any(s in config['spaces'] for s in [space, 'all', 'default'])
@staticmethod @staticmethod
def _read_results_pickle(results_file: Path) -> List: def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
""" """
Read hyperopt results from pickle file Stream hyperopt results from file
LEGACY method - new files are written as json and cannot be read with this method.
"""
from joblib import load
logger.info(f"Reading pickled epochs from '{results_file}'")
data = load(results_file)
return data
@staticmethod
def _read_results(results_file: Path) -> List:
"""
Read hyperopt results from file
""" """
import rapidjson import rapidjson
logger.info(f"Reading epochs from '{results_file}'") logger.info(f"Reading epochs from '{results_file}'")
with results_file.open('r') as f: with results_file.open('r') as f:
data = [rapidjson.loads(line) for line in f] data = []
return data for line in f:
data += [rapidjson.loads(line)]
if len(data) >= batch_size:
yield data
data = []
yield data
@staticmethod @staticmethod
def load_previous_results(results_file: Path) -> List: def _test_hyperopt_results_exist(results_file) -> bool:
"""
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: if results_file.is_file() and results_file.stat().st_size > 0:
if results_file.suffix == '.pickle': if results_file.suffix == '.pickle':
epochs = HyperoptTools._read_results_pickle(results_file) raise OperationalException(
else: "Legacy hyperopt results are no longer supported."
epochs = HyperoptTools._read_results(results_file) "Please rerun hyperopt or use an older version to load this file."
# Detection of some old format, without 'is_best' field saved )
if epochs[0].get('is_best') is None: return True
else:
# No file found.
return False
@staticmethod
def load_filtered_results(results_file: Path, config: Dict[str, Any]) -> Tuple[List, int]:
filteroptions = {
'only_best': config.get('hyperopt_list_best', False),
'only_profitable': config.get('hyperopt_list_profitable', False),
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
}
if not HyperoptTools._test_hyperopt_results_exist(results_file):
# No file found.
return [], 0
epochs = []
total_epochs = 0
for epochs_tmp in HyperoptTools._read_results(results_file):
if total_epochs == 0 and epochs_tmp[0].get('is_best') is None:
raise OperationalException( raise OperationalException(
"The file with HyperoptTools results is incompatible with this version " "The file with HyperoptTools results is incompatible with this version "
"of Freqtrade and cannot be loaded.") "of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(epochs)} previous evaluations from disk.") total_epochs += len(epochs_tmp)
return epochs epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
# Final filter run ...
epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
return epochs, total_epochs
@staticmethod @staticmethod
def show_epoch_details(results, total_epochs: int, print_json: bool, def show_epoch_details(results, total_epochs: int, print_json: bool,
@ -149,7 +174,7 @@ class HyperoptTools():
if print_json: if print_json:
result_dict: Dict = {} result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
@ -158,6 +183,8 @@ class HyperoptTools():
non_optimized) non_optimized)
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:", HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
non_optimized) non_optimized)
HyperoptTools._params_pretty_print(params, 'protection',
"Protection hyperspace params:", non_optimized)
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized) HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized) HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized) HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
@ -203,7 +230,7 @@ class HyperoptTools():
elif space == "roi": elif space == "roi":
result = result[:-1] + f'{appendix}\n' result = result[:-1] + f'{appendix}\n'
minimal_roi_result = rapidjson.dumps({ minimal_roi_result = rapidjson.dumps({
str(k): v for k, v in (space_params or no_params).items() str(k): v for k, v in (space_params or no_params).items()
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE) }, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
result += f"minimal_roi = {minimal_roi_result}" result += f"minimal_roi = {minimal_roi_result}"
elif space == "trailing": elif space == "trailing":
@ -431,21 +458,14 @@ class HyperoptTools():
trials['Best'] = '' trials['Best'] = ''
trials['Stake currency'] = config['stake_currency'] trials['Stake currency'] = config['stake_currency']
if 'results_metrics.total_trades' in trials: base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades', 'results_metrics.profit_mean', 'results_metrics.profit_median',
'results_metrics.profit_mean', 'results_metrics.profit_median', 'results_metrics.profit_total',
'results_metrics.profit_total', 'Stake currency',
'Stake currency', 'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
'results_metrics.profit_total_abs', 'results_metrics.holding_avg', 'loss', 'is_initial_point', 'is_best']
'loss', 'is_initial_point', 'is_best'] perc_multi = 100
perc_multi = 100
else:
perc_multi = 1
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()] param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics] trials = trials[base_metrics + param_metrics]
@ -473,11 +493,6 @@ class HyperoptTools():
trials['Avg profit'] = trials['Avg profit'].apply( trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else "" lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
) )
if perc_multi == 1:
trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: f'{x:,.1f} m' if isinstance(
x, float) else f"{x.total_seconds() // 60:,.1f} m" if not isna(x) else ""
)
trials['Objective'] = trials['Objective'].apply( trials['Objective'] = trials['Objective'].apply(
lambda x: f'{x:,.5f}' if x != 100000 else "" lambda x: f'{x:,.5f}' if x != 100000 else ""
) )

View File

@ -31,7 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
filename = Path.joinpath( filename = Path.joinpath(
recordfilename.parent, recordfilename.parent,
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
).with_suffix(recordfilename.suffix) ).with_suffix(recordfilename.suffix)
file_dump_json(filename, stats) file_dump_json(filename, stats)
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
@ -173,7 +173,7 @@ def generate_strategy_comparison(all_results: Dict) -> List[Dict]:
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']['dry_run_wallet'], strategy) results['results'], results['config']['dry_run_wallet'], strategy)
) )
try: try:
max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'], max_drawdown_per, _, _, _, _ = calculate_max_drawdown(results['results'],
value_col='profit_ratio') value_col='profit_ratio')
@ -604,7 +604,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency']) strat_results['stake_currency'])
stake_amount = round_coin_value( stake_amount = round_coin_value(
strat_results['stake_amount'], strat_results['stake_currency'] strat_results['stake_amount'], strat_results['stake_currency']
) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited' ) if strat_results['stake_amount'] != UNLIMITED_STAKE_AMOUNT else 'unlimited'
message = ("No trades made. " message = ("No trades made. "
f"Your starting balance was {start_balance}, " f"Your starting balance was {start_balance}, "

View File

@ -161,7 +161,7 @@ class Order(_DECL_BASE):
self.ft_is_open = True self.ft_is_open = True
if self.status in ('closed', 'canceled', 'cancelled'): if self.status in ('closed', 'canceled', 'cancelled'):
self.ft_is_open = False self.ft_is_open = False
if order.get('filled', 0) > 0: if (order.get('filled', 0.0) or 0.0) > 0:
self.order_filled_date = datetime.now(timezone.utc) self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc)
@ -354,12 +354,12 @@ class LocalTrade():
LocalTrade.trades_open = [] LocalTrade.trades_open = []
LocalTrade.total_profit = 0 LocalTrade.total_profit = 0
def adjust_min_max_rates(self, current_price: float) -> None: def adjust_min_max_rates(self, current_price: float, current_price_low: float) -> None:
""" """
Adjust the max_rate and min_rate. Adjust the max_rate and min_rate.
""" """
self.max_rate = max(current_price, self.max_rate or self.open_rate) self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price, self.min_rate or self.open_rate) self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
def _set_new_stoploss(self, new_loss: float, stoploss: float): def _set_new_stoploss(self, new_loss: float, stoploss: float):
"""Assign new stop value""" """Assign new stop value"""

View File

@ -334,8 +334,8 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots:
) )
elif indicator_b not in data: elif indicator_b not in data:
logger.info( logger.info(
'fill_to: "%s" ignored. Reason: This indicator is not ' 'fill_to: "%s" ignored. Reason: This indicator is not '
'in your strategy.', indicator_b 'in your strategy.', indicator_b
) )
return fig return fig

View File

@ -144,7 +144,7 @@ class IPairList(LoggingMixin, ABC):
markets = self._exchange.markets markets = self._exchange.markets
if not markets: if not markets:
raise OperationalException( raise OperationalException(
'Markets not loaded. Make sure that exchange is initialized correctly.') 'Markets not loaded. Make sure that exchange is initialized correctly.')
sanitized_whitelist: List[str] = [] sanitized_whitelist: List[str] = []
for pair in pairlist: for pair in pairlist:

View File

@ -120,9 +120,9 @@ class VolumePairList(IPairList):
# Use fresh pairlist # Use fresh pairlist
# Check if pair quote currency equals to the stake currency. # Check if pair quote currency equals to the stake currency.
filtered_tickers = [ filtered_tickers = [
v for k, v in tickers.items() v for k, v in tickers.items()
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and v[self._sort_key] is not None)] and v[self._sort_key] is not None)]
pairlist = [s['symbol'] for s in filtered_tickers] pairlist = [s['symbol'] for s in filtered_tickers]
pairlist = self.filter_pairlist(pairlist, tickers) pairlist = self.filter_pairlist(pairlist, tickers)
@ -197,7 +197,7 @@ class VolumePairList(IPairList):
if self._min_value > 0: if self._min_value > 0:
filtered_tickers = [ filtered_tickers = [
v for v in filtered_tickers if v[self._sort_key] > self._min_value] v for v in filtered_tickers if v[self._sort_key] > self._min_value]
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key]) sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key])

View File

@ -28,13 +28,13 @@ class PairListManager():
self._tickers_needed = False self._tickers_needed = False
for pairlist_handler_config in self._config.get('pairlists', None): for pairlist_handler_config in self._config.get('pairlists', None):
pairlist_handler = PairListResolver.load_pairlist( pairlist_handler = PairListResolver.load_pairlist(
pairlist_handler_config['method'], pairlist_handler_config['method'],
exchange=exchange, exchange=exchange,
pairlistmanager=self, pairlistmanager=self,
config=config, config=config,
pairlistconfig=pairlist_handler_config, pairlistconfig=pairlist_handler_config,
pairlist_pos=len(self._pairlist_handlers) pairlist_pos=len(self._pairlist_handlers)
) )
self._tickers_needed |= pairlist_handler.needstickers self._tickers_needed |= pairlist_handler.needstickers
self._pairlist_handlers.append(pairlist_handler) self._pairlist_handlers.append(pairlist_handler)

View File

@ -25,19 +25,22 @@ class IProtection(LoggingMixin, ABC):
def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None:
self._config = config self._config = config
self._protection_config = protection_config self._protection_config = protection_config
self._stop_duration_candles: Optional[int] = None
self._lookback_period_candles: Optional[int] = None
tf_in_min = timeframe_to_minutes(config['timeframe']) tf_in_min = timeframe_to_minutes(config['timeframe'])
if 'stop_duration_candles' in protection_config: if 'stop_duration_candles' in protection_config:
self._stop_duration_candles = protection_config.get('stop_duration_candles', 1) self._stop_duration_candles = int(protection_config.get('stop_duration_candles', 1))
self._stop_duration = (tf_in_min * self._stop_duration_candles) self._stop_duration = (tf_in_min * self._stop_duration_candles)
else: else:
self._stop_duration_candles = None self._stop_duration_candles = None
self._stop_duration = protection_config.get('stop_duration', 60) self._stop_duration = protection_config.get('stop_duration', 60)
if 'lookback_period_candles' in protection_config: if 'lookback_period_candles' in protection_config:
self._lookback_period_candles = protection_config.get('lookback_period_candles', 1) self._lookback_period_candles = int(protection_config.get('lookback_period_candles', 1))
self._lookback_period = tf_in_min * self._lookback_period_candles self._lookback_period = tf_in_min * self._lookback_period_candles
else: else:
self._lookback_period_candles = None self._lookback_period_candles = None
self._lookback_period = protection_config.get('lookback_period', 60) self._lookback_period = int(protection_config.get('lookback_period', 60))
LoggingMixin.__init__(self, logger) LoggingMixin.__init__(self, logger)

View File

@ -54,9 +54,9 @@ class StoplossGuard(IProtection):
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
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 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

@ -8,6 +8,3 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from freqtrade.resolvers.pairlist_resolver import PairListResolver from freqtrade.resolvers.pairlist_resolver import PairListResolver
from freqtrade.resolvers.protection_resolver import ProtectionResolver from freqtrade.resolvers.protection_resolver import ProtectionResolver
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver

View File

@ -50,7 +50,7 @@ class StrategyResolver(IResolver):
if 'timeframe' not in config: if 'timeframe' not in config:
logger.warning( logger.warning(
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'." "DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
) )
strategy.timeframe = strategy.ticker_interval strategy.timeframe = strategy.ticker_interval
if strategy._ft_params_from_file: if strategy._ft_params_from_file:
@ -119,7 +119,7 @@ class StrategyResolver(IResolver):
- default (if not None) - default (if not None)
""" """
if (attribute in config if (attribute in config
and not isinstance(getattr(type(strategy), 'my_property', None), property)): and not isinstance(getattr(type(strategy), attribute, None), property)):
# Ensure Properties are not overwritten # Ensure Properties are not overwritten
setattr(strategy, attribute, config[attribute]) setattr(strategy, attribute, config[attribute])
logger.info("Override strategy '%s' with value in config file: %s.", logger.info("Override strategy '%s' with value in config file: %s.",

View File

@ -199,8 +199,8 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
config=Depends(get_config)): config=Depends(get_config)):
config = deepcopy(config) config = deepcopy(config)
config.update({ config.update({
'strategy': strategy, 'strategy': strategy,
}) })
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)

View File

@ -62,7 +62,7 @@ class CryptoToFiatConverter:
# If the request is not a 429 error we want to raise the normal error # If the request is not a 429 error we want to raise the normal error
logger.error( logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: {}".format( "Could not load FIAT Cryptocurrency map for the following problem: {}".format(
request_exception request_exception
) )
) )
except (Exception) as exception: except (Exception) as exception:

View File

@ -15,6 +15,7 @@ class RPCManager:
""" """
Class to manage RPC objects (Telegram, API, ...) Class to manage RPC objects (Telegram, API, ...)
""" """
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" Initializes all enabled rpc modules """ """ Initializes all enabled rpc modules """
self.registered_modules: List[RPCHandler] = [] self.registered_modules: List[RPCHandler] = []

View File

@ -77,7 +77,6 @@ class Telegram(RPCHandler):
""" This class handles all telegram communication """ """ This class handles all telegram communication """
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None: def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
""" """
Init the Telegram call, and init the super class RPCHandler Init the Telegram call, and init the super class RPCHandler
:param rpc: instance of RPC Helper class :param rpc: instance of RPC Helper class
@ -270,7 +269,7 @@ class Telegram(RPCHandler):
noti = '' noti = ''
if msg_type == RPCMessageType.SELL: if msg_type == RPCMessageType.SELL:
sell_noti = self._config['telegram'] \ sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {}) .get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string # For backward compatibility sell still can be string
if isinstance(sell_noti, str): if isinstance(sell_noti, str):
noti = sell_noti noti = sell_noti
@ -278,7 +277,7 @@ class Telegram(RPCHandler):
noti = sell_noti.get(str(msg['sell_reason']), default_noti) noti = sell_noti.get(str(msg['sell_reason']), default_noti)
else: else:
noti = self._config['telegram'] \ noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), default_noti) .get('notification_settings', {}).get(str(msg_type), default_noti)
if noti == 'off': if noti == 'off':
logger.info(f"Notification '{msg_type}' not sent.") logger.info(f"Notification '{msg_type}' not sent.")
@ -541,7 +540,7 @@ class Telegram(RPCHandler):
f"`{first_trade_date}`\n" f"`{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}\n`" f"*Latest Trade opened:* `{latest_trade_date}\n`"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
) )
if stats['closed_trade_count'] > 0: if stats['closed_trade_count'] > 0:
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
@ -576,13 +575,14 @@ class Telegram(RPCHandler):
sell_reasons_msg = tabulate( sell_reasons_msg = tabulate(
sell_reasons_tabulate, sell_reasons_tabulate,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
) )
durations = stats['durations'] durations = stats['durations']
duration_msg = tabulate([ duration_msg = tabulate(
['Wins', str(timedelta(seconds=durations['wins'])) [
if durations['wins'] != 'N/A' else 'N/A'], ['Wins', str(timedelta(seconds=durations['wins']))
['Losses', str(timedelta(seconds=durations['losses'])) if durations['wins'] != 'N/A' else 'N/A'],
if durations['losses'] != 'N/A' else 'N/A'] ['Losses', str(timedelta(seconds=durations['losses']))
if durations['losses'] != 'N/A' else 'N/A']
], ],
headers=['', 'Avg. Duration'] headers=['', 'Avg. Duration']
) )
@ -1100,7 +1100,7 @@ class Telegram(RPCHandler):
if reload_able: if reload_able:
reply_markup = InlineKeyboardMarkup([ reply_markup = InlineKeyboardMarkup([
[InlineKeyboardButton("Refresh", callback_data=callback_path)], [InlineKeyboardButton("Refresh", callback_data=callback_path)],
]) ])
else: else:
reply_markup = InlineKeyboardMarkup([[]]) reply_markup = InlineKeyboardMarkup([[]])
msg += "\nUpdated: {}".format(datetime.now().ctime()) msg += "\nUpdated: {}".format(datetime.now().ctime())

View File

@ -1,7 +1,7 @@
# flake8: noqa: F401 # flake8: noqa: F401
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.hyper import (CategoricalParameter, DecimalParameter, IntParameter, from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
RealParameter) IntParameter, RealParameter)
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open

View File

@ -270,6 +270,28 @@ class CategoricalParameter(BaseParameter):
return [self.value] return [self.value]
class BooleanParameter(CategoricalParameter):
def __init__(self, *, default: Optional[Any] = None,
space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs):
"""
Initialize hyperopt-optimizable Boolean Parameter.
It's a shortcut to `CategoricalParameter([True, False])`.
:param default: A default value. If not specified, first item from specified space will be
used.
:param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if
parameter field
name is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Categorical.
"""
categories = [True, False]
super().__init__(categories=categories, default=default, space=space, optimize=optimize,
load=load, **kwargs)
class HyperStrategyMixin(object): class HyperStrategyMixin(object):
""" """
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
@ -283,6 +305,7 @@ class HyperStrategyMixin(object):
self.config = config self.config = config
self.ft_buy_params: List[BaseParameter] = [] self.ft_buy_params: List[BaseParameter] = []
self.ft_sell_params: List[BaseParameter] = [] self.ft_sell_params: List[BaseParameter] = []
self.ft_protection_params: List[BaseParameter] = []
self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT) self._load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
@ -292,11 +315,12 @@ class HyperStrategyMixin(object):
:param category: :param category:
:return: :return:
""" """
if category not in ('buy', 'sell', None): if category not in ('buy', 'sell', 'protection', None):
raise OperationalException('Category must be one of: "buy", "sell", None.') raise OperationalException(
'Category must be one of: "buy", "sell", "protection", None.')
if category is None: if category is None:
params = self.ft_buy_params + self.ft_sell_params params = self.ft_buy_params + self.ft_sell_params + self.ft_protection_params
else: else:
params = getattr(self, f"ft_{category}_params") params = getattr(self, f"ft_{category}_params")
@ -324,9 +348,10 @@ class HyperStrategyMixin(object):
params: Dict = { params: Dict = {
'buy': list(cls.detect_parameters('buy')), 'buy': list(cls.detect_parameters('buy')),
'sell': list(cls.detect_parameters('sell')), 'sell': list(cls.detect_parameters('sell')),
'protection': list(cls.detect_parameters('protection')),
} }
params.update({ params.update({
'count': len(params['buy'] + params['sell']) 'count': len(params['buy'] + params['sell'] + params['protection'])
}) })
return params return params
@ -340,9 +365,12 @@ class HyperStrategyMixin(object):
self._ft_params_from_file = params self._ft_params_from_file = params
buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {})) buy_params = deep_merge_dicts(params.get('buy', {}), getattr(self, 'buy_params', {}))
sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {})) sell_params = deep_merge_dicts(params.get('sell', {}), getattr(self, 'sell_params', {}))
protection_params = deep_merge_dicts(params.get('protection', {}),
getattr(self, 'protection_params', {}))
self._load_params(buy_params, 'buy', hyperopt) self._load_params(buy_params, 'buy', hyperopt)
self._load_params(sell_params, 'sell', hyperopt) self._load_params(sell_params, 'sell', hyperopt)
self._load_params(protection_params, 'protection', hyperopt)
def load_params_from_file(self) -> Dict: def load_params_from_file(self) -> Dict:
filename_str = getattr(self, '__file__', '') filename_str = getattr(self, '__file__', '')
@ -397,7 +425,8 @@ class HyperStrategyMixin(object):
""" """
params = { params = {
'buy': {}, 'buy': {},
'sell': {} 'sell': {},
'protection': {},
} }
for name, p in self.enumerate_parameters(): for name, p in self.enumerate_parameters():
if not p.optimize or not p.in_space: if not p.optimize or not p.in_space:

View File

@ -605,7 +605,7 @@ class IStrategy(ABC, HyperStrategyMixin):
current_rate = rate current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
trade.adjust_min_max_rates(high or current_rate) trade.adjust_min_max_rates(high or current_rate, low or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit, current_time=date, current_profit=current_profit,
@ -769,7 +769,7 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return current_profit > roi return current_profit > roi
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def advise_all_indicators(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 advise_buy or advise_sell! Does not run advise_buy or advise_sell!

View File

@ -38,7 +38,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
informative['date_merge'] = ( informative['date_merge'] = (
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
) )
else: else:
raise ValueError("Tried to merge a faster timeframe to a slower timeframe." raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
"This would create new rows, and can throw off your regular indicators.") "This would create new rows, and can throw off your regular indicators.")

View File

@ -25,7 +25,7 @@
"ask_strategy": { "ask_strategy": {
"price_side": "ask", "price_side": "ask",
"use_order_book": true, "use_order_book": true,
"order_book_top": 1, "order_book_top": 1
}, },
{{ exchange | indent(4) }}, {{ exchange | indent(4) }},
"pairlists": [ "pairlists": [

View File

@ -6,8 +6,8 @@ import numpy as np # noqa
import pandas as pd # noqa import pandas as pd # noqa
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy import IStrategy from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter IStrategy, IntParameter)
# -------------------------------- # --------------------------------
# Add your lib to import here # Add your lib to import here

View File

@ -6,8 +6,8 @@ import numpy as np # noqa
import pandas as pd # noqa import pandas as pd # noqa
from pandas import DataFrame from pandas import DataFrame
from freqtrade.strategy import IStrategy from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
from freqtrade.strategy import CategoricalParameter, DecimalParameter, IntParameter IStrategy, IntParameter)
# -------------------------------- # --------------------------------
# Add your lib to import here # Add your lib to import here

View File

@ -19,7 +19,7 @@ isort==5.9.3
nbconvert==6.1.0 nbconvert==6.1.0
# mypy types # mypy types
types-cachetools==0.1.9 types-cachetools==0.1.10
types-filelock==0.1.4 types-filelock==0.1.5
types-requests==2.25.1 types-requests==2.25.6
types-tabulate==0.1.1 types-tabulate==0.8.2

View File

@ -1,7 +1,7 @@
numpy==1.21.1 numpy==1.21.1
pandas==1.3.1 pandas==1.3.1
ccxt==1.54.24 ccxt==1.54.74
# 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.7 cryptography==3.4.7
aiohttp==3.7.4.post0 aiohttp==3.7.4.post0

View File

@ -938,247 +938,261 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}') pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}')
def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
saved_hyperopt_results_legacy, tmpdir):
csv_file = Path(tmpdir) / "test.csv" csv_file = Path(tmpdir) / "test.csv"
for res in (saved_hyperopt_results, saved_hyperopt_results_legacy): mocker.patch(
mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', return_value=True
MagicMock(return_value=res)
) )
args = [ def fake_iterator(*args, **kwargs):
"hyperopt-list", yield from [saved_hyperopt_results]
"--no-details",
"--no-color", mocker.patch(
] 'freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results',
pargs = get_args(args) side_effect=fake_iterator
pargs['config'] = None )
start_hyperopt_list(pargs)
captured = capsys.readouterr() args = [
assert all(x in captured.out "hyperopt-list",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", "--no-details",
" 6/12", " 7/12", " 8/12", " 9/12", " 10/12", "--no-color",
" 11/12", " 12/12"]) ]
args = [ pargs = get_args(args)
"hyperopt-list", pargs['config'] = None
"--best", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
] for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12",
pargs = get_args(args) " 6/12", " 7/12", " 8/12", " 9/12", " 10/12",
pargs['config'] = None " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--best",
for x in [" 1/12", " 5/12", " 10/12"]) "--no-details",
assert all(x not in captured.out "--no-color",
for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 1/12", " 5/12", " 10/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 2/12", " 3/12", " 4/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs['config'] = None " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--profitable",
for x in [" 2/12", " 10/12"]) "--no-details",
assert all(x not in captured.out "--no-color",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
] for x in [" 2/12", " 10/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
start_hyperopt_list(pargs) " 11/12", " 12/12"])
captured = capsys.readouterr() args = [
assert all(x in captured.out "hyperopt-list",
for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params", "--profitable",
"Sell hyperspace params", "ROI table", "Stoploss"]) "--no-color",
assert all(x not in captured.out ]
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", pargs = get_args(args)
" 11/12", " 12/12"]) pargs['config'] = None
args = [ start_hyperopt_list(pargs)
"hyperopt-list", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 2/12", " 10/12", "Best result:", "Buy hyperspace params",
"--min-trades", "20", "Sell hyperspace params", "ROI table", "Stoploss"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs['config'] = None " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"]) "--no-color",
assert all(x not in captured.out "--min-trades", "20",
for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"]) ]
args = [ pargs = get_args(args)
"hyperopt-list", pargs['config'] = None
"--profitable", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-trades", "20", for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--profitable",
assert all(x in captured.out "--no-details",
for x in [" 2/12", " 10/12"]) "--no-color",
assert all(x not in captured.out "--max-trades", "20",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 2/12", " 10/12"])
"--min-avg-profit", "0.11", assert all(x not in captured.out
] for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs = get_args(args) " 11/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--profitable",
assert all(x in captured.out "--no-details",
for x in [" 2/12"]) "--no-color",
assert all(x not in captured.out "--min-avg-profit", "0.11",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", ]
" 10/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-avg-profit", "0.10", for x in [" 2/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
pargs['config'] = None " 10/12", " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", "--no-color",
" 11/12"]) "--max-avg-profit", "0.10",
assert all(x not in captured.out ]
for x in [" 2/12", " 4/12", " 10/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--min-total-profit", "0.4", for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
] " 11/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 2/12", " 4/12", " 10/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 10/12"]) "--no-color",
assert all(x not in captured.out "--min-total-profit", "0.4",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", ]
" 9/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-total-profit", "0.4", for x in [" 10/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
pargs['config'] = None " 9/12", " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", "--no-color",
" 9/12", " 11/12"]) "--max-total-profit", "0.4",
assert all(x not in captured.out ]
for x in [" 4/12", " 10/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--min-objective", "0.1", for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
] " 9/12", " 11/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 4/12", " 10/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 10/12"]) "--no-color",
assert all(x not in captured.out "--min-objective", "0.1",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", ]
" 9/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--max-objective", "0.1", assert all(x in captured.out
] for x in [" 10/12"])
pargs = get_args(args) assert all(x not in captured.out
pargs['config'] = None for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
start_hyperopt_list(pargs) " 9/12", " 11/12", " 12/12"])
captured = capsys.readouterr() args = [
assert all(x in captured.out "hyperopt-list",
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", "--no-details",
" 9/12", " 11/12"]) "--max-objective", "0.1",
assert all(x not in captured.out ]
for x in [" 4/12", " 10/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--profitable", captured = capsys.readouterr()
"--no-details", assert all(x in captured.out
"--no-color", for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
"--min-avg-time", "2000", " 9/12", " 11/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 4/12", " 10/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--profitable",
assert all(x in captured.out "--no-details",
for x in [" 10/12"]) "--no-color",
assert all(x not in captured.out "--min-avg-time", "2000",
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", ]
" 8/12", " 9/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--max-avg-time", "1500", for x in [" 10/12"])
] assert all(x not in captured.out
pargs = get_args(args) for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12",
pargs['config'] = None " 8/12", " 9/12", " 11/12", " 12/12"])
start_hyperopt_list(pargs) args = [
captured = capsys.readouterr() "hyperopt-list",
assert all(x in captured.out "--no-details",
for x in [" 2/12", " 6/12"]) "--no-color",
assert all(x not in captured.out "--max-avg-time", "1500",
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12" ]
" 9/12", " 10/12", " 11/12", " 12/12"]) pargs = get_args(args)
args = [ pargs['config'] = None
"hyperopt-list", start_hyperopt_list(pargs)
"--no-details", captured = capsys.readouterr()
"--no-color", assert all(x in captured.out
"--export-csv", for x in [" 2/12", " 6/12"])
str(csv_file), assert all(x not in captured.out
] for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12"
pargs = get_args(args) " 9/12", " 10/12", " 11/12", " 12/12"])
pargs['config'] = None args = [
start_hyperopt_list(pargs) "hyperopt-list",
captured = capsys.readouterr() "--no-details",
log_has("CSV file created: test_file.csv", caplog) "--no-color",
assert csv_file.is_file() "--export-csv",
line = csv_file.read_text() str(csv_file),
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line ]
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line) pargs = get_args(args)
csv_file.unlink() pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
log_has("CSV file created: test_file.csv", caplog)
assert csv_file.is_file()
line = csv_file.read_text()
assert ('Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,"3,930.0 m",0.43662' in line
or "Best,1,2,-1.25%,-1.2222,-0.00125625,,-2.51,2 days 17:30:00,0.43662" in line)
csv_file.unlink()
def test_hyperopt_show(mocker, capsys, saved_hyperopt_results): def test_hyperopt_show(mocker, capsys, saved_hyperopt_results):
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools.load_previous_results', 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
MagicMock(return_value=saved_hyperopt_results) return_value=True
)
def fake_iterator(*args, **kwargs):
yield from [saved_hyperopt_results]
mocker.patch(
'freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results',
side_effect=fake_iterator
) )
mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result') mocker.patch('freqtrade.commands.hyperopt_commands.show_backtest_result')

View File

@ -6,8 +6,8 @@
*/ */
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.05,
"fiat_display_currency": "USD", // C++-style comment "fiat_display_currency": "USD", // C++-style comment
"amount_reserve_percent" : 0.05, // And more, tabs before this comment "amount_reserve_percent": 0.05, // And more, tabs before this comment
"dry_run": false, "dry_run": false,
"timeframe": "5m", "timeframe": "5m",
"trailing_stop": false, "trailing_stop": false,
@ -15,15 +15,15 @@
"trailing_stop_positive_offset": 0.0051, "trailing_stop_positive_offset": 0.0051,
"trailing_only_offset_is_reached": false, "trailing_only_offset_is_reached": false,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
"20": 0.02, "20": 0.02,
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "buy": 10,
"sell": 30, // Trailing comma should also be accepted now "sell": 30, // Trailing comma should also be accepted now
}, },
"bid_strategy": { "bid_strategy": {
"use_order_book": false, "use_order_book": false,
@ -34,7 +34,7 @@
"bids_to_ask_delta": 1 "bids_to_ask_delta": 1
} }
}, },
"ask_strategy":{ "ask_strategy": {
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9 "order_book_max": 9
@ -64,7 +64,9 @@
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"password": "", "password": "",
"ccxt_config": {"enableRateLimit": true}, "ccxt_config": {
"enableRateLimit": true
},
"ccxt_async_config": { "ccxt_async_config": {
"enableRateLimit": false, "enableRateLimit": false,
"rateLimit": 500, "rateLimit": 500,
@ -103,8 +105,8 @@
"remove_pumps": false "remove_pumps": false
}, },
"telegram": { "telegram": {
// We can now comment out some settings // We can now comment out some settings
// "enabled": true, // "enabled": true,
"enabled": false, "enabled": false,
"token": "your_telegram_token", "token": "your_telegram_token",
"chat_id": "your_telegram_chat_id" "chat_id": "your_telegram_chat_id"

View File

@ -1814,138 +1814,6 @@ def open_trade():
) )
@pytest.fixture
def saved_hyperopt_results_legacy():
return [
{
'loss': 0.4366182531161519,
'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
'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, '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
'total_profit': -0.00125625,
'current_epoch': 1,
'is_initial_point': True,
'is_best': True
}, {
'loss': 20.0,
'params_dict': {
'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', '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', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501
'params_details': {
'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # 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
'stoploss': {'stoploss': -0.338070047333259}},
'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
'total_profit': 6.185e-05,
'current_epoch': 2,
'is_initial_point': True,
'is_best': False
}, {
'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_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, '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
'total_profit': -0.13639474,
'current_epoch': 3,
'is_initial_point': True,
'is_best': False
}, {
'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_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, '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
'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False
}, {
'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_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, '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
'total_profit': -0.002480140000000001,
'current_epoch': 5,
'is_initial_point': True,
'is_best': True
}, {
'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_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, '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
'total_profit': -0.0041773,
'current_epoch': 6,
'is_initial_point': True,
'is_best': False
}, {
'loss': 4.713497421432944,
'params_dict': {'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-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_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501
'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
'stoploss': {'stoploss': -0.14613268022709905}}, # 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
'total_profit': -0.06339929,
'current_epoch': 7,
'is_initial_point': True,
'is_best': False
}, {
'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_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, '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
'total_profit': 0.0,
'current_epoch': 8,
'is_initial_point': True,
'is_best': False
}, {
'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_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, '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
'total_profit': -0.044050070000000004, # noqa: E501
'current_epoch': 9,
'is_initial_point': True,
'is_best': False
}, {
'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_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, '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
'total_profit': 0.00021629,
'current_epoch': 10,
'is_initial_point': True,
'is_best': True
}, {
'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_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, '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
'total_profit': -0.07436117,
'current_epoch': 11,
'is_initial_point': True,
'is_best': False
}, {
'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_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, '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
'total_profit': 0,
'current_epoch': 12,
'is_initial_point': True,
'is_best': False
}
]
@pytest.fixture @pytest.fixture
def saved_hyperopt_results(): def saved_hyperopt_results():
hyperopt_res = [ hyperopt_res = [

View File

@ -381,7 +381,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.ohlcvdata_to_dataframe( data = strategy.advise_all_indicators(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='1m', timeframe='1m',
@ -399,7 +399,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.ohlcvdata_to_dataframe( data = strategy.advise_all_indicators(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='1m', timeframe='1m',
@ -424,7 +424,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange('index', 'index', 200, 250) timerange = TimeRange('index', 'index', 200, 250)
data = strategy.ohlcvdata_to_dataframe( data = strategy.advise_all_indicators(
load_data( load_data(
datadir=testdatadir, datadir=testdatadir,
timeframe='5m', timeframe='5m',

View File

@ -984,16 +984,21 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
assert order['fee'] assert order['fee']
@pytest.mark.parametrize("side,amount,endprice", [ @pytest.mark.parametrize("side,rate,amount,endprice", [
("buy", 1, 25.566), # spread is 25.263-25.266
("buy", 100, 25.5672), # Requires interpolation ("buy", 25.564, 1, 25.566),
("buy", 1000, 25.575), # More than orderbook return ("buy", 25.564, 100, 25.5672), # Requires interpolation
("sell", 1, 25.563), ("buy", 25.590, 100, 25.5672), # Price above spread ... average is lower
("sell", 100, 25.5625), # Requires interpolation ("buy", 25.564, 1000, 25.575), # More than orderbook return
("sell", 1000, 25.5555), # More than orderbook return ("buy", 24.000, 100000, 25.200), # Run into max_slippage of 5%
("sell", 25.564, 1, 25.563),
("sell", 25.564, 100, 25.5625), # Requires interpolation
("sell", 25.510, 100, 25.5625), # price below spread - average is higher
("sell", 25.564, 1000, 25.5555), # More than orderbook return
("sell", 27, 10000, 25.65), # max-slippage 5%
]) ])
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, endprice, def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amount, endprice,
exchange_name, order_book_l2_usd): exchange_name, order_book_l2_usd):
default_conf['dry_run'] = True default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
@ -1003,7 +1008,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, amount, en
) )
order = exchange.create_dry_run_order( order = exchange.create_dry_run_order(
pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=25.5) pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate)
assert 'id' in order assert 'id' in order
assert f'dry_run_{side}_' in order["id"] assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side assert order["side"] == side

View File

@ -52,4 +52,6 @@ def _build_backtest_dataframe(data):
# Ensure floats are in place # Ensure floats are in place
for column in ['open', 'high', 'low', 'close', 'volume']: for column in ['open', 'high', 'low', 'close', 'volume']:
frame[column] = frame[column].astype('float64') frame[column] = frame[column].astype('float64')
if 'buy_tag' not in columns:
frame['buy_tag'] = None
return frame return frame

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import random import random
from datetime import timedelta
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
@ -85,7 +86,7 @@ def simple_backtest(config, contour, mocker, testdatadir) -> None:
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
data = load_data_test(contour, testdatadir) data = load_data_test(contour, testdatadir)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtesting.backtest( results = backtesting.backtest(
@ -107,7 +108,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(conf) backtesting = Backtesting(conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
return { return {
'processed': processed, 'processed': processed,
@ -289,7 +290,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.timeframe == '5m' assert backtesting.timeframe == '5m'
assert callable(backtesting.strategy.ohlcvdata_to_dataframe) assert callable(backtesting.strategy.advise_all_indicators)
assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_buy)
assert callable(backtesting.strategy.advise_sell) assert callable(backtesting.strategy.advise_sell)
assert isinstance(backtesting.strategy.dp, DataProvider) assert isinstance(backtesting.strategy.dp, DataProvider)
@ -335,14 +336,14 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
fill_up_missing=True) fill_up_missing=True)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
assert len(processed['UNITTEST/BTC']) == 102 assert len(processed['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same # Load strategy to compare the result between Backtesting function and strategy are the same
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
processed2 = strategy.ohlcvdata_to_dataframe(data) processed2 = strategy.advise_all_indicators(data)
assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC']) assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC'])
@ -535,6 +536,8 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
backtesting.cleanup()
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_sell_signal'] = False default_conf['use_sell_signal'] = False
@ -547,7 +550,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
timerange = TimeRange('date', None, 1517227800, 0) timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
result = backtesting.backtest( result = backtesting.backtest(
processed=processed, processed=processed,
@ -581,7 +584,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'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],
'stop_loss_ratio': [-0.1, -0.1], 'stop_loss_ratio': [-0.1, -0.1],
'min_rate': [0.1038, 0.10302485], 'min_rate': [0.10370188, 0.10300000000000001],
'max_rate': [0.10501, 0.1038888], 'max_rate': [0.10501, 0.1038888],
'is_open': [False, False], 'is_open': [False, False],
'buy_tag': [None, None], 'buy_tag': [None, None],
@ -612,7 +615,7 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
timerange = TimeRange.parse_timerange('1510688220-1510700340') timerange = TimeRange.parse_timerange('1510688220-1510700340')
data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'],
timerange=timerange) timerange=timerange)
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
results = backtesting.backtest( results = backtesting.backtest(
processed=processed, processed=processed,
@ -631,7 +634,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
dict_of_tickerrows = load_data_test('raise', testdatadir) dict_of_tickerrows = load_data_test('raise', testdatadir)
dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows) dataframes = backtesting.strategy.advise_all_indicators(dict_of_tickerrows)
dataframe = dataframes['UNITTEST/BTC'] dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns cols = dataframe.columns
# assert the dataframe got some of the indicator columns # assert the dataframe got some of the indicator columns
@ -739,8 +742,13 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
# 100 buys signals # 100 buys signals
results = result['results'] results = result['results']
assert len(results) == 100 assert len(results) == 100
# Cached data should be 200 (no change since required_startup is 0) # Cached data should be 200
assert len(backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]) == 200 analyzed_df = backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]
assert len(analyzed_df) == 200
# Expect last candle to be 1 below end date (as the last candle is assumed as "incomplete"
# during backtesting)
expected_last_candle_date = backtest_conf['end_date'] - timedelta(minutes=1)
assert analyzed_df.iloc[-1]['date'].to_pydatetime() == expected_last_candle_date
# One trade was force-closed at the end # One trade was force-closed at the end
assert len(results.loc[results['is_open']]) == 0 assert len(results.loc[results['is_open']]) == 0
@ -772,7 +780,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
data = trim_dictlist(data, -500) data = trim_dictlist(data, -500)
# Remove data for one pair from the beginning of the data # Remove data for one pair from the beginning of the data
data[pair] = data[pair][tres:].reset_index() if tres > 0:
data[pair] = data[pair][tres:].reset_index()
default_conf['timeframe'] = '5m' default_conf['timeframe'] = '5m'
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@ -780,7 +789,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_buy = _trend_alternate_hold # Override
backtesting.strategy.advise_sell = _trend_alternate_hold # Override backtesting.strategy.advise_sell = _trend_alternate_hold # Override
processed = backtesting.strategy.ohlcvdata_to_dataframe(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
backtest_conf = { backtest_conf = {
'processed': processed, 'processed': processed,
@ -798,8 +807,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0
# Cached data correctly removed amounts # Cached data correctly removed amounts
removed_candles = len(data[pair]) - 1 - backtesting.strategy.startup_candle_count offset = 1 if tres == 0 else 0
removed_candles = len(data[pair]) - offset - backtesting.strategy.startup_candle_count
assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles
assert len(backtesting.dataprovider.get_analyzed_dataframe(
'NXT/BTC', '5m')[0]) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
backtest_conf = { backtest_conf = {
'processed': processed, 'processed': processed,

View File

@ -351,7 +351,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
del hyperopt_conf['timeframe'] del hyperopt_conf['timeframe']
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -399,7 +399,7 @@ def test_hyperopt_format_results(hyperopt):
'rejected_signals': 2, 'rejected_signals': 2,
'backtest_start_time': 1619718665, 'backtest_start_time': 1619718665,
'backtest_end_time': 1619718665, 'backtest_end_time': 1619718665,
} }
results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result, results_metrics = generate_strategy_stats({'XRP/BTC': None}, '', bt_result,
Arrow(2017, 11, 14, 19, 32, 00), Arrow(2017, 11, 14, 19, 32, 00),
Arrow(2017, 12, 14, 19, 32, 00), market_change=0) Arrow(2017, 12, 14, 19, 32, 00), market_change=0)
@ -426,7 +426,7 @@ def test_hyperopt_format_results(hyperopt):
def test_populate_indicators(hyperopt, testdatadir) -> None: def test_populate_indicators(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -438,7 +438,7 @@ def test_populate_indicators(hyperopt, testdatadir) -> None:
def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -463,7 +463,7 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
def test_sell_strategy_generator(hyperopt, testdatadir) -> None: def test_sell_strategy_generator(hyperopt, testdatadir) -> None:
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@ -577,6 +577,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"20.0": 0.02, "20.0": 0.02,
"50.0": 0.01, "50.0": 0.01,
"110.0": 0}, "110.0": 0},
'protection': {},
'sell': {'sell-adx-enabled': False, 'sell': {'sell-adx-enabled': False,
'sell-adx-value': 0, 'sell-adx-value': 0,
'sell-fastd-enabled': True, 'sell-fastd-enabled': True,
@ -592,7 +593,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'trailing_stop_positive': 0.02, 'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.07}}, 'trailing_stop_positive_offset': 0.07}},
'params_dict': optimizer_param, 'params_dict': optimizer_param,
'params_not_optimized': {'buy': {}, 'sell': {}}, 'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
'results_metrics': ANY, 'results_metrics': ANY,
'total_profit': 3.1e-08 'total_profit': 3.1e-08
} }
@ -659,7 +660,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
}) })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -712,7 +713,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
hyperopt_conf.update({'print_json': True}) hyperopt_conf.update({'print_json': True})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -760,7 +761,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
}) })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
@ -804,7 +805,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
hyperopt_conf.update({'spaces': 'roi stoploss'}) hyperopt_conf.update({'spaces': 'roi stoploss'})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@ -843,7 +844,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
hyperopt_conf.update({'spaces': 'all', }) hyperopt_conf.update({'spaces': 'all', })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
@ -885,7 +886,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
hyperopt_conf.update({'spaces': 'buy'}) hyperopt_conf.update({'spaces': 'buy'})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: sell_strategy_generator() is actually not called because # TODO: sell_strategy_generator() is actually not called because
@ -939,7 +940,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
hyperopt_conf.update({'spaces': 'sell', }) hyperopt_conf.update({'spaces': 'sell', })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
# TODO: buy_strategy_generator() is actually not called because # TODO: buy_strategy_generator() is actually not called because
@ -984,7 +985,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No
hyperopt_conf.update({'spaces': space}) hyperopt_conf.update({'spaces': space})
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.advise_all_indicators = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
delattr(hyperopt.custom_hyperopt.__class__, method) delattr(hyperopt.custom_hyperopt.__class__, method)
@ -1002,6 +1003,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
hyperopt_conf.update({ hyperopt_conf.update({
'strategy': 'HyperoptableStrategy', 'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir), 'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['all']
}) })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
@ -1009,12 +1012,18 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35 assert hyperopt.backtesting.strategy.buy_rsi.value == 35
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
assert isinstance(buy_rsi_range, range) assert isinstance(buy_rsi_range, range)
# Range from 0 - 50 (inclusive) # Range from 0 - 50 (inclusive)
assert len(list(buy_rsi_range)) == 51 assert len(list(buy_rsi_range)) == 51
hyperopt.start() hyperopt.start()
# All values should've changed.
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
def test_SKDecimal(): def test_SKDecimal():

View File

@ -10,7 +10,7 @@ import rapidjson
from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.constants import FTHYPT_FILEVERSION
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
from tests.conftest import log_has, log_has_re from tests.conftest import log_has
# Functions for recurrent object patching # Functions for recurrent object patching
@ -20,9 +20,14 @@ def create_results() -> List[Dict]:
def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None: def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None:
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {})
assert hyperopt_epochs == ([], 0)
# Test writing to temp dir and reading again # Test writing to temp dir and reading again
epochs = create_results() epochs = create_results()
hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt')
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
@ -33,36 +38,28 @@ def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None:
hyperopt._save_result(epochs[0]) hyperopt._save_result(epochs[0])
assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog) assert log_has(f"2 epochs saved to '{hyperopt.results_file}'.", caplog)
hyperopt_epochs = HyperoptTools.load_previous_results(hyperopt.results_file) hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {})
assert len(hyperopt_epochs) == 2 assert len(hyperopt_epochs) == 2
assert hyperopt_epochs[1] == 2
assert len(hyperopt_epochs[0]) == 2
result_gen = HyperoptTools._read_results(hyperopt.results_file, 1)
def test_load_previous_results(testdatadir, caplog) -> None: epoch = next(result_gen)
assert len(epoch) == 1
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' assert epoch[0] == epochs[0]
epoch = next(result_gen)
hyperopt_epochs = HyperoptTools.load_previous_results(results_file) assert len(epoch) == 1
epoch = next(result_gen)
assert len(hyperopt_epochs) == 5 assert len(epoch) == 0
assert log_has_re(r"Reading pickled epochs from .*", caplog) with pytest.raises(StopIteration):
next(result_gen)
caplog.clear()
# Modern version
results_file = testdatadir / 'strategy_SampleStrategy.fthypt'
hyperopt_epochs = HyperoptTools.load_previous_results(results_file)
assert len(hyperopt_epochs) == 5
assert log_has_re(r"Reading epochs from .*", caplog)
def test_load_previous_results2(mocker, testdatadir, caplog) -> None: def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt_tools.HyperoptTools._read_results_pickle',
return_value=[{'asdf': '222'}])
results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle' results_file = testdatadir / 'hyperopt_results_SampleStrategy.pickle'
with pytest.raises(OperationalException, match=r"The file .* incompatible.*"): with pytest.raises(OperationalException,
HyperoptTools.load_previous_results(results_file) match=r"Legacy hyperopt results are no longer supported.*"):
HyperoptTools.load_filtered_results(results_file, {})
@pytest.mark.parametrize("spaces, expected_results", [ @pytest.mark.parametrize("spaces, expected_results", [

View File

@ -93,7 +93,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, min_ago_open=200, min_ago_close=30,
)) ))
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
@ -150,7 +150,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, profit_rate=0.9, min_ago_open=200, min_ago_close=30, profit_rate=0.9,
)) ))
assert not freqtrade.protections.stop_per_pair(pair) assert not freqtrade.protections.stop_per_pair(pair)
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()

View File

@ -139,9 +139,9 @@ def test_fiat_too_many_requests_response(mocker, caplog):
assert length_cryptomap == 0 assert length_cryptomap == 0
assert fiat_convert._backoff > datetime.datetime.now().timestamp() assert fiat_convert._backoff > datetime.datetime.now().timestamp()
assert log_has( assert log_has(
'Too many requests for Coingecko API, backing off and trying again later.', 'Too many requests for Coingecko API, backing off and trying again later.',
caplog caplog
) )
def test_fiat_invalid_response(mocker, caplog): def test_fiat_invalid_response(mocker, caplog):

View File

@ -942,7 +942,7 @@ def test_api_whitelist(botclient):
"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'], "whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
"length": 4, "length": 4,
"method": ["StaticPairList"] "method": ["StaticPairList"]
} }
def test_api_forcebuy(botclient, mocker, fee): def test_api_forcebuy(botclient, mocker, fee):
@ -1033,7 +1033,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'buy_tag': None, 'buy_tag': None,
'timeframe': 5, 'timeframe': 5,
'exchange': 'binance', 'exchange': 'binance',
} }
def test_api_forcesell(botclient, mocker, ticker, fee, markets): def test_api_forcesell(botclient, mocker, ticker, fee, markets):
@ -1215,7 +1215,7 @@ def test_api_strategies(botclient):
'DefaultStrategy', 'DefaultStrategy',
'HyperoptableStrategy', 'HyperoptableStrategy',
'TestStrategyLegacy' 'TestStrategyLegacy'
]} ]}
def test_api_strategy(botclient): def test_api_strategy(botclient):

View File

@ -4,7 +4,8 @@ import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, RealParameter from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
RealParameter)
class HyperoptableStrategy(IStrategy): class HyperoptableStrategy(IStrategy):
@ -64,6 +65,18 @@ class HyperoptableStrategy(IStrategy):
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
load=False) load=False)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
@property
def protections(self):
prot = []
if self.protection_enabled.value:
prot.append({
"method": "CooldownPeriod",
"stop_duration_candles": self.protection_cooldown_lookback.value
})
return prot
def informative_pairs(self): def informative_pairs(self):
""" """

View File

@ -16,8 +16,8 @@ from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.optimize.space import SKDecimal from freqtrade.optimize.space import SKDecimal
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter,
IntParameter, RealParameter) DecimalParameter, IntParameter, RealParameter)
from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import log_has, log_has_re from tests.conftest import log_has, log_has_re
@ -228,25 +228,25 @@ def test_assert_df(ohlcv_history, caplog):
_STRATEGY.disable_dataframe_checks = False _STRATEGY.disable_dataframe_checks = False
def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: def test_advise_all_indicators(default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True) fill_up_missing=True)
processed = strategy.ohlcvdata_to_dataframe(data) processed = strategy.advise_all_indicators(data)
assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed
def test_ohlcvdata_to_dataframe_copy(mocker, default_conf, testdatadir) -> None: def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators')
timerange = TimeRange.parse_timerange('1510694220-1510700340') timerange = TimeRange.parse_timerange('1510694220-1510700340')
data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
fill_up_missing=True) fill_up_missing=True)
strategy.ohlcvdata_to_dataframe(data) strategy.advise_all_indicators(data)
assert aimock.call_count == 1 assert aimock.call_count == 1
# Ensure that a copy of the dataframe is passed to advice_indicators # Ensure that a copy of the dataframe is passed to advice_indicators
assert aimock.call_args_list[0][0][0] is not data assert aimock.call_args_list[0][0][0] is not data
@ -398,7 +398,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
exchange='binance', exchange='binance',
open_rate=1, open_rate=1,
) )
trade.adjust_min_max_rates(trade.open_rate) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
strategy.trailing_stop = trailing strategy.trailing_stop = trailing
strategy.trailing_stop_positive = -0.05 strategy.trailing_stop_positive = -0.05
strategy.use_custom_stoploss = custom strategy.use_custom_stoploss = custom
@ -552,6 +552,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
def test_is_pair_locked(default_conf): def test_is_pair_locked(default_conf):
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
PairLocks.timeframe = default_conf['timeframe'] PairLocks.timeframe = default_conf['timeframe']
PairLocks.use_db = True
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# No lock should be present # No lock should be present
assert len(PairLocks.get_pair_locks(None)) == 0 assert len(PairLocks.get_pair_locks(None)) == 0
@ -717,6 +718,17 @@ def test_hyperopt_parameters():
assert len(list(catpar.range)) == 3 assert len(list(catpar.range)) == 3
assert list(catpar.range) == ['buy_rsi', 'buy_macd', 'buy_none'] assert list(catpar.range) == ['buy_rsi', 'buy_macd', 'buy_none']
boolpar = BooleanParameter(default=True, space='buy')
assert boolpar.value is True
assert isinstance(boolpar.get_space(''), Categorical)
assert isinstance(boolpar.range, list)
assert len(list(boolpar.range)) == 1
boolpar.in_space = True
assert len(list(boolpar.range)) == 2
assert list(boolpar.range) == [True, False]
def test_auto_hyperopt_interface(default_conf): def test_auto_hyperopt_interface(default_conf):
default_conf.update({'strategy': 'HyperoptableStrategy'}) default_conf.update({'strategy': 'HyperoptableStrategy'})
@ -734,7 +746,8 @@ def test_auto_hyperopt_interface(default_conf):
assert isinstance(all_params, dict) assert isinstance(all_params, dict)
assert len(all_params['buy']) == 2 assert len(all_params['buy']) == 2
assert len(all_params['sell']) == 2 assert len(all_params['sell']) == 2
assert all_params['count'] == 4 # Number of Hyperoptable parameters
assert all_params['count'] == 6
strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy') strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy')

View File

@ -125,7 +125,7 @@ def test_parse_args_backtesting_custom() -> None:
'--strategy-list', '--strategy-list',
'DefaultStrategy', 'DefaultStrategy',
'SampleStrategy' 'SampleStrategy'
] ]
call_args = Arguments(args).get_parsed_arg() call_args = Arguments(args).get_parsed_arg()
assert call_args['config'] == ['test_conf.json'] assert call_args['config'] == ['test_conf.json']
assert call_args['verbosity'] == 0 assert call_args['verbosity'] == 0

View File

@ -1130,17 +1130,17 @@ def test_pairlist_resolving_fallback(mocker):
@pytest.mark.parametrize("setting", [ @pytest.mark.parametrize("setting", [
("ask_strategy", "use_sell_signal", True, ("ask_strategy", "use_sell_signal", True,
None, "use_sell_signal", False), None, "use_sell_signal", False),
("ask_strategy", "sell_profit_only", True, ("ask_strategy", "sell_profit_only", True,
None, "sell_profit_only", False), None, "sell_profit_only", False),
("ask_strategy", "sell_profit_offset", 0.1, ("ask_strategy", "sell_profit_offset", 0.1,
None, "sell_profit_offset", 0.01), None, "sell_profit_offset", 0.01),
("ask_strategy", "ignore_roi_if_buy_signal", True, ("ask_strategy", "ignore_roi_if_buy_signal", True,
None, "ignore_roi_if_buy_signal", False), None, "ignore_roi_if_buy_signal", False),
("ask_strategy", "ignore_buying_expired_candle_after", 5, ("ask_strategy", "ignore_buying_expired_candle_after", 5,
None, "ignore_buying_expired_candle_after", 6), None, "ignore_buying_expired_candle_after", 6),
]) ])
def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog): def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -1180,10 +1180,10 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca
@pytest.mark.parametrize("setting", [ @pytest.mark.parametrize("setting", [
("experimental", "use_sell_signal", False), ("experimental", "use_sell_signal", False),
("experimental", "sell_profit_only", True), ("experimental", "sell_profit_only", True),
("experimental", "ignore_roi_if_buy_signal", True), ("experimental", "ignore_roi_if_buy_signal", True),
]) ])
def test_process_removed_settings(mocker, default_conf, setting): def test_process_removed_settings(mocker, default_conf, setting):
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@ -1330,7 +1330,7 @@ def test_process_removed_setting(mocker, default_conf, caplog):
'sectionB', 'somesetting') 'sectionB', 'somesetting')
def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): def test_process_deprecated_ticker_interval(default_conf, caplog):
message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval." message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval."
config = deepcopy(default_conf) config = deepcopy(default_conf)
process_temporary_deprecated_settings(config) process_temporary_deprecated_settings(config)
@ -1352,6 +1352,17 @@ def test_process_deprecated_ticker_interval(mocker, default_conf, caplog):
process_temporary_deprecated_settings(config) process_temporary_deprecated_settings(config)
def test_process_deprecated_protections(default_conf, caplog):
message = "DEPRECATED: Setting 'protections' in the configuration is deprecated."
config = deepcopy(default_conf)
process_temporary_deprecated_settings(config)
assert not log_has(message, caplog)
config['protections'] = []
process_temporary_deprecated_settings(config)
assert log_has(message, caplog)
def test_flat_vars_to_nested_dict(caplog): def test_flat_vars_to_nested_dict(caplog):
test_args = { test_args = {

View File

@ -799,25 +799,30 @@ def test_adjust_min_max_rates(fee):
open_rate=1, open_rate=1,
) )
trade.adjust_min_max_rates(trade.open_rate) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
assert trade.max_rate == 1 assert trade.max_rate == 1
assert trade.min_rate == 1 assert trade.min_rate == 1
# check min adjusted, max remained # check min adjusted, max remained
trade.adjust_min_max_rates(0.96) trade.adjust_min_max_rates(0.96, 0.96)
assert trade.max_rate == 1 assert trade.max_rate == 1
assert trade.min_rate == 0.96 assert trade.min_rate == 0.96
# check max adjusted, min remains # check max adjusted, min remains
trade.adjust_min_max_rates(1.05) trade.adjust_min_max_rates(1.05, 1.05)
assert trade.max_rate == 1.05 assert trade.max_rate == 1.05
assert trade.min_rate == 0.96 assert trade.min_rate == 0.96
# current rate "in the middle" - no adjustment # current rate "in the middle" - no adjustment
trade.adjust_min_max_rates(1.03) trade.adjust_min_max_rates(1.03, 1.03)
assert trade.max_rate == 1.05 assert trade.max_rate == 1.05
assert trade.min_rate == 0.96 assert trade.min_rate == 0.96
# current rate "in the middle" - no adjustment
trade.adjust_min_max_rates(1.10, 0.91)
assert trade.max_rate == 1.10
assert trade.min_rate == 0.91
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize('use_db', [True, False]) @pytest.mark.parametrize('use_db', [True, False])
@ -1219,6 +1224,11 @@ def test_update_order_from_ccxt(caplog):
assert o.ft_is_open assert o.ft_is_open
assert o.order_filled_date is None assert o.order_filled_date is None
# Order is unfilled, "filled" not set
# https://github.com/freqtrade/freqtrade/issues/5404
ccxt_order.update({'filled': None, 'remaining': 20.0, 'status': 'canceled'})
o.update_from_ccxt_object(ccxt_order)
# Order has been closed # Order has been closed
ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'})
o.update_from_ccxt_object(ccxt_order) o.update_from_ccxt_object(ccxt_order)