Merge branch 'develop' into feat/short
This commit is contained in:
commit
92ed7c0bf8
@ -26,8 +26,8 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists))
|
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
101
docs/hyperopt.md
101
docs/hyperopt.md
@ -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.
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
@ -36,7 +36,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#blacklists))
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
|
@ -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",
|
||||||
|
@ -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',
|
||||||
),
|
),
|
||||||
|
@ -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,
|
||||||
|
@ -187,7 +187,7 @@ def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> Li
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics'].get(
|
if x['results_metrics'].get(
|
||||||
'trade_count', x['results_metrics'].get('total_trades')
|
'trade_count', x['results_metrics'].get('total_trades')
|
||||||
) < filteroptions['filter_max_trades']
|
) < filteroptions['filter_max_trades']
|
||||||
]
|
]
|
||||||
return epochs
|
return epochs
|
||||||
|
|
||||||
@ -239,7 +239,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics'].get(
|
if x['results_metrics'].get(
|
||||||
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
|
'avg_profit', x['results_metrics'].get('profit_mean', 0) * 100
|
||||||
) < filteroptions['filter_max_avg_profit']
|
) < filteroptions['filter_max_avg_profit']
|
||||||
]
|
]
|
||||||
if filteroptions['filter_min_total_profit'] is not None:
|
if filteroptions['filter_min_total_profit'] is not None:
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
@ -247,7 +247,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics'].get(
|
if x['results_metrics'].get(
|
||||||
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
||||||
) > filteroptions['filter_min_total_profit']
|
) > filteroptions['filter_min_total_profit']
|
||||||
]
|
]
|
||||||
if filteroptions['filter_max_total_profit'] is not None:
|
if filteroptions['filter_max_total_profit'] is not None:
|
||||||
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
epochs = _hyperopt_filter_epochs_trade(epochs, 0)
|
||||||
@ -255,7 +255,7 @@ def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics'].get(
|
if x['results_metrics'].get(
|
||||||
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
'profit', x['results_metrics'].get('profit_total_abs', 0)
|
||||||
) < filteroptions['filter_max_total_profit']
|
) < filteroptions['filter_max_total_profit']
|
||||||
]
|
]
|
||||||
return epochs
|
return epochs
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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.")
|
||||||
|
@ -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'},
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -146,6 +146,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 +196,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):
|
||||||
"""
|
"""
|
||||||
|
@ -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))
|
||||||
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -82,8 +82,8 @@ 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'])
|
||||||
@ -149,7 +149,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 +158,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 +205,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":
|
||||||
|
@ -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}, "
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.",
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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] = []
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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.")
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -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():
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
@ -717,6 +717,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 +745,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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user