Merge branch 'develop' into pr/Axel-CH/5347
This commit is contained in:
commit
c456cfc312
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
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.
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
|
@ -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'},
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal file
128
freqtrade/optimize/hyperopt_epoch_filters.py
Normal 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
|
@ -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.
|
||||||
|
@ -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 ""
|
||||||
)
|
)
|
||||||
|
@ -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}, "
|
||||||
|
@ -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"""
|
||||||
|
@ -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:
|
||||||
|
@ -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!
|
||||||
|
@ -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.")
|
||||||
|
@ -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": [
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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 = [
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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():
|
||||||
|
@ -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", [
|
||||||
|
@ -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
|
||||||
@ -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')
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user