mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 01:21:11 +00:00
Merge pull request #10400 from simwai/feature/stoploss-start-at
Added unlock_at field for protection config
This commit is contained in:
commit
6f33115187
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -114,3 +114,5 @@ target/
|
|||
!config_examples/config_full.example.json
|
||||
!config_examples/config_kraken.example.json
|
||||
!config_examples/config_freqai.example.json
|
||||
|
||||
docker-compose-*.yml
|
||||
|
|
|
@ -605,6 +605,10 @@
|
|||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"unlock_at": {
|
||||
"description": "Time when trading will be unlocked regularly. Format: HH:MM",
|
||||
"type": "string"
|
||||
},
|
||||
"trade_limit": {
|
||||
"description": "Minimum number of trades required during lookback period.",
|
||||
"type": "number",
|
||||
|
|
|
@ -36,6 +36,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||
| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections. <br> **Datatype:** Positive integer (in candles).
|
||||
| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered. <br>Cannot be used together with `lookback_period_candles`. <br>This setting may be ignored by some Protections. <br> **Datatype:** Float (in minutes)
|
||||
| `trade_limit` | Number of trades required at minimum (not used by all Protections). <br> **Datatype:** Positive integer
|
||||
| `unlock_at` | Time when trading will be unlocked regularly (not used by all Protections). <br> **Datatype:** string <br>**Input Format:** "HH:MM" (24-hours)
|
||||
|
||||
!!! Note "Durations"
|
||||
Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles).
|
||||
|
@ -44,7 +45,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||
#### Stoploss Guard
|
||||
|
||||
`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`).
|
||||
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
|
||||
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`).
|
||||
|
||||
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
|
||||
|
||||
|
@ -97,7 +98,7 @@ def protections(self):
|
|||
#### Low Profit Pairs
|
||||
|
||||
`LowProfitPairs` uses all trades for a pair within `lookback_period` in minutes (or in candles when using `lookback_period_candles`) to determine the overall profit ratio.
|
||||
If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
|
||||
If that ratio is below `required_profit`, that pair will be locked for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`).
|
||||
|
||||
For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long losses.
|
||||
|
||||
|
@ -120,7 +121,7 @@ def protections(self):
|
|||
|
||||
#### Cooldown Period
|
||||
|
||||
`CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`) after selling, avoiding a re-entry for this pair for `stop_duration` minutes.
|
||||
`CooldownPeriod` locks a pair for `stop_duration` in minutes (or in candles when using `stop_duration_candles`, or until the set time when using `unlock_at`) after exiting, avoiding a re-entry for this pair for `stop_duration` minutes.
|
||||
|
||||
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
||||
|
||||
|
|
|
@ -476,6 +476,12 @@ CONF_SCHEMA = {
|
|||
"type": "number",
|
||||
"minimum": 0,
|
||||
},
|
||||
"unlock_at": {
|
||||
"description": (
|
||||
"Time when trading will be unlocked regularly. Format: HH:MM"
|
||||
),
|
||||
"type": "string",
|
||||
},
|
||||
"trade_limit": {
|
||||
"description": "Minimum number of trades required during lookback period.",
|
||||
"type": "number",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from collections import Counter
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
from jsonschema import Draft4Validator, validators
|
||||
|
@ -201,16 +202,32 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
|
|||
"""
|
||||
|
||||
for prot in conf.get("protections", []):
|
||||
parsed_unlock_at = None
|
||||
if (config_unlock_at := prot.get("unlock_at")) is not None:
|
||||
try:
|
||||
parsed_unlock_at = datetime.strptime(config_unlock_at, "%H:%M")
|
||||
except ValueError:
|
||||
raise ConfigurationError(f"Invalid date format for unlock_at: {config_unlock_at}.")
|
||||
|
||||
if "stop_duration" in prot and "stop_duration_candles" in prot:
|
||||
raise ConfigurationError(
|
||||
"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:
|
||||
raise ConfigurationError(
|
||||
"Protections must specify either `lookback_period` or `lookback_period_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}"
|
||||
f"Please fix the protection {prot.get('method')}."
|
||||
)
|
||||
|
||||
if parsed_unlock_at is not None and (
|
||||
"stop_duration" in prot or "stop_duration_candles" in prot
|
||||
):
|
||||
raise ConfigurationError(
|
||||
"Protections must specify either `unlock_at`, `stop_duration` or "
|
||||
"`stop_duration_candles`.\n"
|
||||
f"Please fix the protection {prot.get('method')}."
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -18,19 +18,19 @@ class CooldownPeriod(IProtection):
|
|||
"""
|
||||
LockReason to use
|
||||
"""
|
||||
return f"Cooldown period for {self.stop_duration_str}."
|
||||
return f"Cooldown period for {self.unlock_reason_time_element}."
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short method description - used for startup-messages
|
||||
Short method description - used for startup messages
|
||||
"""
|
||||
return f"{self.name} - Cooldown period of {self.stop_duration_str}."
|
||||
return f"{self.name} - Cooldown period {self.unlock_reason_time_element}."
|
||||
|
||||
def _cooldown_period(self, pair: str, date_now: datetime) -> Optional[ProtectionReturn]:
|
||||
"""
|
||||
Get last trade for this pair
|
||||
"""
|
||||
look_back_until = date_now - timedelta(minutes=self._stop_duration)
|
||||
look_back_until = date_now - timedelta(minutes=self._lookback_period)
|
||||
# filters = [
|
||||
# Trade.is_open.is_(False),
|
||||
# Trade.close_date > look_back_until,
|
||||
|
@ -42,8 +42,8 @@ class CooldownPeriod(IProtection):
|
|||
# Get latest trade
|
||||
# Ignore type error as we know we only get closed trades.
|
||||
trade = sorted(trades, key=lambda t: t.close_date)[-1] # type: ignore
|
||||
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
|
||||
until = self.calculate_lock_end([trade], self._stop_duration)
|
||||
self.log_once(f"Cooldown for {pair} {self.unlock_reason_time_element}.", logger.info)
|
||||
until = self.calculate_lock_end([trade])
|
||||
|
||||
return ProtectionReturn(
|
||||
lock=True,
|
||||
|
|
|
@ -32,15 +32,19 @@ class IProtection(LoggingMixin, ABC):
|
|||
self._config = config
|
||||
self._protection_config = protection_config
|
||||
self._stop_duration_candles: Optional[int] = None
|
||||
self._stop_duration: int = 0
|
||||
self._lookback_period_candles: Optional[int] = None
|
||||
self._unlock_at: Optional[str] = None
|
||||
|
||||
tf_in_min = timeframe_to_minutes(config["timeframe"])
|
||||
if "stop_duration_candles" in protection_config:
|
||||
self._stop_duration_candles = int(protection_config.get("stop_duration_candles", 1))
|
||||
self._stop_duration = tf_in_min * self._stop_duration_candles
|
||||
elif "unlock_at" in protection_config:
|
||||
self._unlock_at = protection_config.get("unlock_at")
|
||||
else:
|
||||
self._stop_duration_candles = None
|
||||
self._stop_duration = int(protection_config.get("stop_duration", 60))
|
||||
|
||||
if "lookback_period_candles" in protection_config:
|
||||
self._lookback_period_candles = int(protection_config.get("lookback_period_candles", 1))
|
||||
self._lookback_period = tf_in_min * self._lookback_period_candles
|
||||
|
@ -80,6 +84,16 @@ class IProtection(LoggingMixin, ABC):
|
|||
else:
|
||||
return f"{self._lookback_period} {plural(self._lookback_period, 'minute', 'minutes')}"
|
||||
|
||||
@property
|
||||
def unlock_reason_time_element(self) -> str:
|
||||
"""
|
||||
Output configured unlock time or stop duration
|
||||
"""
|
||||
if self._unlock_at is not None:
|
||||
return f"until {self._unlock_at}"
|
||||
else:
|
||||
return f"for {self.stop_duration_str}"
|
||||
|
||||
@abstractmethod
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
|
@ -105,16 +119,23 @@ class IProtection(LoggingMixin, ABC):
|
|||
If true, this pair will be locked with <reason> until <until>
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime:
|
||||
def calculate_lock_end(self, trades: List[LocalTrade]) -> datetime:
|
||||
"""
|
||||
Get lock end time
|
||||
Implicitly uses `self._stop_duration` or `self._unlock_at` depending on the configuration.
|
||||
"""
|
||||
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
|
||||
# coming from Database, tzinfo is not set.
|
||||
if max_date.tzinfo is None:
|
||||
max_date = max_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
until = max_date + timedelta(minutes=stop_minutes)
|
||||
if self._unlock_at is not None:
|
||||
# unlock_at case with fixed hour of the day
|
||||
hour, minutes = self._unlock_at.split(":")
|
||||
unlock_at = max_date.replace(hour=int(hour), minute=int(minutes))
|
||||
if unlock_at < max_date:
|
||||
unlock_at += timedelta(days=1)
|
||||
return unlock_at
|
||||
|
||||
until = max_date + timedelta(minutes=self._stop_duration)
|
||||
return until
|
||||
|
|
|
@ -36,7 +36,7 @@ class LowProfitPairs(IProtection):
|
|||
"""
|
||||
return (
|
||||
f"{profit} < {self._required_profit} in {self.lookback_period_str}, "
|
||||
f"locking for {self.stop_duration_str}."
|
||||
f"locking {self.unlock_reason_time_element}."
|
||||
)
|
||||
|
||||
def _low_profit(
|
||||
|
@ -70,7 +70,7 @@ class LowProfitPairs(IProtection):
|
|||
f"within {self._lookback_period} minutes.",
|
||||
logger.info,
|
||||
)
|
||||
until = self.calculate_lock_end(trades, self._stop_duration)
|
||||
until = self.calculate_lock_end(trades)
|
||||
|
||||
return ProtectionReturn(
|
||||
lock=True,
|
||||
|
|
|
@ -39,7 +39,7 @@ class MaxDrawdown(IProtection):
|
|||
"""
|
||||
return (
|
||||
f"{drawdown} passed {self._max_allowed_drawdown} in {self.lookback_period_str}, "
|
||||
f"locking for {self.stop_duration_str}."
|
||||
f"locking {self.unlock_reason_time_element}."
|
||||
)
|
||||
|
||||
def _max_drawdown(self, date_now: datetime) -> Optional[ProtectionReturn]:
|
||||
|
@ -70,7 +70,8 @@ class MaxDrawdown(IProtection):
|
|||
f" within {self.lookback_period_str}.",
|
||||
logger.info,
|
||||
)
|
||||
until = self.calculate_lock_end(trades, self._stop_duration)
|
||||
|
||||
until = self.calculate_lock_end(trades)
|
||||
|
||||
return ProtectionReturn(
|
||||
lock=True,
|
||||
|
|
|
@ -38,7 +38,7 @@ class StoplossGuard(IProtection):
|
|||
"""
|
||||
return (
|
||||
f"{self._trade_limit} stoplosses in {self._lookback_period} min, "
|
||||
f"locking for {self._stop_duration} min."
|
||||
f"locking {self.unlock_reason_time_element}."
|
||||
)
|
||||
|
||||
def _stoploss_guard(
|
||||
|
@ -78,7 +78,7 @@ class StoplossGuard(IProtection):
|
|||
f"stoplosses within {self._lookback_period} minutes.",
|
||||
logger.info,
|
||||
)
|
||||
until = self.calculate_lock_end(trades, self._stop_duration)
|
||||
until = self.calculate_lock_end(trades)
|
||||
return ProtectionReturn(
|
||||
lock=True,
|
||||
until=until,
|
||||
|
|
|
@ -102,56 +102,94 @@ def test_protectionmanager(mocker, default_conf):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeframe,expected,protconf",
|
||||
"timeframe,expected_lookback,expected_stop,protconf",
|
||||
[
|
||||
(
|
||||
"1m",
|
||||
[20, 10],
|
||||
20,
|
||||
10,
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}],
|
||||
),
|
||||
(
|
||||
"5m",
|
||||
[100, 15],
|
||||
100,
|
||||
15,
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 15}],
|
||||
),
|
||||
(
|
||||
"1h",
|
||||
[1200, 40],
|
||||
1200,
|
||||
40,
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 40}],
|
||||
),
|
||||
(
|
||||
"1d",
|
||||
[1440, 5],
|
||||
1440,
|
||||
5,
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration": 5}],
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
[20, 5],
|
||||
20,
|
||||
5,
|
||||
[{"method": "StoplossGuard", "lookback_period": 20, "stop_duration_candles": 5}],
|
||||
),
|
||||
(
|
||||
"5m",
|
||||
[15, 25],
|
||||
15,
|
||||
25,
|
||||
[{"method": "StoplossGuard", "lookback_period": 15, "stop_duration_candles": 5}],
|
||||
),
|
||||
(
|
||||
"1h",
|
||||
[50, 600],
|
||||
50,
|
||||
600,
|
||||
[{"method": "StoplossGuard", "lookback_period": 50, "stop_duration_candles": 10}],
|
||||
),
|
||||
(
|
||||
"1h",
|
||||
[60, 540],
|
||||
60,
|
||||
540,
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration_candles": 9}],
|
||||
),
|
||||
(
|
||||
"1m",
|
||||
20,
|
||||
"01:00",
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "01:00"}],
|
||||
),
|
||||
(
|
||||
"5m",
|
||||
100,
|
||||
"02:00",
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "02:00"}],
|
||||
),
|
||||
(
|
||||
"1h",
|
||||
1200,
|
||||
"03:00",
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "03:00"}],
|
||||
),
|
||||
(
|
||||
"1d",
|
||||
1440,
|
||||
"04:00",
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 1, "unlock_at": "04:00"}],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_protections_init(default_conf, timeframe, expected, protconf):
|
||||
def test_protections_init(default_conf, timeframe, expected_lookback, expected_stop, protconf):
|
||||
"""
|
||||
Test the initialization of protections with different configurations, including unlock_at.
|
||||
"""
|
||||
default_conf["timeframe"] = timeframe
|
||||
man = ProtectionManager(default_conf, protconf)
|
||||
assert len(man._protection_handlers) == len(protconf)
|
||||
assert man._protection_handlers[0]._lookback_period == expected[0]
|
||||
assert man._protection_handlers[0]._stop_duration == expected[1]
|
||||
assert man._protection_handlers[0]._lookback_period == expected_lookback
|
||||
if isinstance(expected_stop, int):
|
||||
assert man._protection_handlers[0]._stop_duration == expected_stop
|
||||
else:
|
||||
assert man._protection_handlers[0]._unlock_at == expected_stop
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
|
@ -385,6 +423,89 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
|
|||
assert not PairLocks.is_global_lock()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
|
||||
default_conf["protections"] = [
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"unlock_at": "05:00",
|
||||
}
|
||||
]
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
message = r"Trading stopped due to .*"
|
||||
assert not freqtrade.protections.global_stop()
|
||||
assert not freqtrade.protections.stop_per_pair("XRP/BTC")
|
||||
|
||||
assert not log_has_re(message, caplog)
|
||||
caplog.clear()
|
||||
|
||||
start_dt = datetime(2024, 5, 2, 0, 30, 0, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start_dt, tick=False)
|
||||
|
||||
generate_mock_trade(
|
||||
"XRP/BTC",
|
||||
fee.return_value,
|
||||
False,
|
||||
exit_reason=ExitType.STOP_LOSS.value,
|
||||
min_ago_open=20,
|
||||
min_ago_close=10,
|
||||
)
|
||||
|
||||
assert not freqtrade.protections.global_stop()
|
||||
assert freqtrade.protections.stop_per_pair("XRP/BTC")
|
||||
assert PairLocks.is_pair_locked("XRP/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
# Move time to "4:30"
|
||||
time_machine.move_to(start_dt + timedelta(hours=4), tick=False)
|
||||
assert PairLocks.is_pair_locked("XRP/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
# Move time to "past 5:00"
|
||||
time_machine.move_to(start_dt + timedelta(hours=5), tick=False)
|
||||
assert not PairLocks.is_pair_locked("XRP/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
# Force rollover to the next day.
|
||||
start_dt = datetime(2024, 5, 2, 22, 00, 0, tzinfo=timezone.utc)
|
||||
time_machine.move_to(start_dt, tick=False)
|
||||
generate_mock_trade(
|
||||
"ETH/BTC",
|
||||
fee.return_value,
|
||||
False,
|
||||
exit_reason=ExitType.ROI.value,
|
||||
min_ago_open=20,
|
||||
min_ago_close=10,
|
||||
)
|
||||
|
||||
assert not freqtrade.protections.global_stop()
|
||||
assert not PairLocks.is_pair_locked("ETH/BTC")
|
||||
assert freqtrade.protections.stop_per_pair("ETH/BTC")
|
||||
assert PairLocks.is_pair_locked("ETH/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
# Move to 23:00
|
||||
time_machine.move_to(start_dt + timedelta(hours=1), tick=False)
|
||||
assert PairLocks.is_pair_locked("ETH/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
# Move to 04:59 (should still be locked)
|
||||
time_machine.move_to(start_dt + timedelta(hours=6, minutes=59), tick=False)
|
||||
assert PairLocks.is_pair_locked("ETH/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
# Move to 05:01 (should still be locked - it unlocks once the 05:00 candle stops at 05:05)
|
||||
time_machine.move_to(start_dt + timedelta(hours=7, minutes=1), tick=False)
|
||||
|
||||
assert PairLocks.is_pair_locked("ETH/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
# Move to 05:01 (unlocked).
|
||||
time_machine.move_to(start_dt + timedelta(hours=7, minutes=5), tick=False)
|
||||
|
||||
assert not PairLocks.is_pair_locked("ETH/BTC")
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("only_per_side", [False, True])
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
|
||||
|
@ -610,7 +731,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
|||
),
|
||||
(
|
||||
{"method": "CooldownPeriod", "stop_duration": 60},
|
||||
"[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 minutes.'}]",
|
||||
"[{'CooldownPeriod': 'CooldownPeriod - Cooldown period for 60 minutes.'}]",
|
||||
None,
|
||||
),
|
||||
(
|
||||
|
@ -639,7 +760,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
|||
),
|
||||
(
|
||||
{"method": "CooldownPeriod", "stop_duration_candles": 5},
|
||||
"[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 5 candles.'}]",
|
||||
"[{'CooldownPeriod': 'CooldownPeriod - Cooldown period for 5 candles.'}]",
|
||||
None,
|
||||
),
|
||||
(
|
||||
|
@ -654,6 +775,38 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
|||
"if drawdown is > 0.0 within 20 candles.'}]",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
"method": "CooldownPeriod",
|
||||
"unlock_at": "01:00",
|
||||
},
|
||||
"[{'CooldownPeriod': 'CooldownPeriod - Cooldown period until 01:00.'}]",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period_candles": 12,
|
||||
"trade_limit": 2,
|
||||
"required_profit": -0.05,
|
||||
"unlock_at": "01:00",
|
||||
},
|
||||
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
|
||||
"2 stoplosses with profit < -5.00% within 12 candles.'}]",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"method": "LowProfitPairs", "lookback_period_candles": 11, "unlock_at": "03:00"},
|
||||
"[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with "
|
||||
"profit < 0.0 within 11 candles.'}]",
|
||||
None,
|
||||
),
|
||||
(
|
||||
{"method": "MaxDrawdown", "lookback_period_candles": 20, "unlock_at": "04:00"},
|
||||
"[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading "
|
||||
"if drawdown is > 0.0 within 20 candles.'}]",
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_protection_manager_desc(
|
||||
|
|
|
@ -840,6 +840,25 @@ def test_validate_whitelist(default_conf):
|
|||
],
|
||||
r"Protections must specify either `stop_duration`.*",
|
||||
),
|
||||
(
|
||||
[
|
||||
{
|
||||
"method": "StoplossGuard",
|
||||
"lookback_period": 20,
|
||||
"stop_duration": 10,
|
||||
"unlock_at": "20:02",
|
||||
}
|
||||
],
|
||||
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
|
||||
),
|
||||
(
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
|
||||
None,
|
||||
),
|
||||
(
|
||||
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
|
||||
"Invalid date format for unlock_at: 55:102.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_validate_protections(default_conf, protconf, expected):
|
||||
|
|
Loading…
Reference in New Issue
Block a user