2020-10-13 06:06:29 +00:00
|
|
|
import logging
|
2020-11-22 10:49:41 +00:00
|
|
|
from abc import ABC, abstractmethod
|
2022-04-24 08:29:19 +00:00
|
|
|
from dataclasses import dataclass
|
2020-11-17 18:43:12 +00:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2024-07-04 08:29:13 +00:00
|
|
|
from typing import Any, Dict, List, Optional, Union
|
2020-10-13 06:06:29 +00:00
|
|
|
|
2022-09-18 11:20:36 +00:00
|
|
|
from freqtrade.constants import Config, LongShort
|
2020-12-07 09:45:35 +00:00
|
|
|
from freqtrade.exchange import timeframe_to_minutes
|
2020-12-07 10:08:54 +00:00
|
|
|
from freqtrade.misc import plural
|
2020-10-15 05:38:00 +00:00
|
|
|
from freqtrade.mixins import LoggingMixin
|
2021-02-20 19:22:00 +00:00
|
|
|
from freqtrade.persistence import LocalTrade
|
2020-10-15 05:38:00 +00:00
|
|
|
|
2020-10-13 06:06:29 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2022-04-24 08:29:19 +00:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class ProtectionReturn:
|
|
|
|
lock: bool
|
|
|
|
until: datetime
|
|
|
|
reason: Optional[str]
|
2024-05-12 14:38:10 +00:00
|
|
|
lock_side: str = "*"
|
2020-10-15 06:07:09 +00:00
|
|
|
|
2020-10-13 06:06:29 +00:00
|
|
|
|
2020-10-15 05:38:00 +00:00
|
|
|
class IProtection(LoggingMixin, ABC):
|
2020-11-19 19:34:29 +00:00
|
|
|
# Can globally stop the bot
|
|
|
|
has_global_stop: bool = False
|
|
|
|
# Can stop trading for one pair
|
|
|
|
has_local_stop: bool = False
|
|
|
|
|
2022-09-18 11:20:36 +00:00
|
|
|
def __init__(self, config: Config, protection_config: Dict[str, Any]) -> None:
|
2020-10-13 06:06:29 +00:00
|
|
|
self._config = config
|
2020-10-14 05:40:44 +00:00
|
|
|
self._protection_config = protection_config
|
2021-08-04 18:52:56 +00:00
|
|
|
self._stop_duration_candles: Optional[int] = None
|
2024-07-16 04:48:30 +00:00
|
|
|
self._stop_duration: int = 0
|
2021-08-04 18:52:56 +00:00
|
|
|
self._lookback_period_candles: Optional[int] = None
|
2024-07-16 04:28:02 +00:00
|
|
|
self._unlock_at: Optional[datetime] = None
|
2021-08-04 18:52:56 +00:00
|
|
|
|
2024-05-12 14:38:10 +00:00
|
|
|
tf_in_min = timeframe_to_minutes(config["timeframe"])
|
2024-07-16 04:28:02 +00:00
|
|
|
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 = self.calculate_unlock_at()
|
2020-12-07 09:45:35 +00:00
|
|
|
else:
|
2024-07-16 04:28:02 +00:00
|
|
|
self._stop_duration = int(protection_config.get("stop_duration", 60))
|
2024-07-14 19:46:22 +00:00
|
|
|
|
2024-05-12 14:38:10 +00:00
|
|
|
if "lookback_period_candles" in protection_config:
|
|
|
|
self._lookback_period_candles = int(protection_config.get("lookback_period_candles", 1))
|
2020-12-07 10:08:54 +00:00
|
|
|
self._lookback_period = tf_in_min * self._lookback_period_candles
|
2020-12-07 09:45:35 +00:00
|
|
|
else:
|
2024-07-16 04:28:02 +00:00
|
|
|
self._lookback_period_candles = None
|
2024-05-12 14:38:10 +00:00
|
|
|
self._lookback_period = int(protection_config.get("lookback_period", 60))
|
2020-11-27 09:32:23 +00:00
|
|
|
|
2020-10-15 05:38:00 +00:00
|
|
|
LoggingMixin.__init__(self, logger)
|
2020-10-13 06:06:29 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
return self.__class__.__name__
|
|
|
|
|
2020-12-07 10:08:54 +00:00
|
|
|
@property
|
|
|
|
def stop_duration_str(self) -> str:
|
|
|
|
"""
|
|
|
|
Output configured stop duration in either candles or minutes
|
|
|
|
"""
|
|
|
|
if self._stop_duration_candles:
|
2024-05-12 14:38:10 +00:00
|
|
|
return (
|
|
|
|
f"{self._stop_duration_candles} "
|
|
|
|
f"{plural(self._stop_duration_candles, 'candle', 'candles')}"
|
|
|
|
)
|
2020-12-07 10:08:54 +00:00
|
|
|
else:
|
2024-05-12 15:51:21 +00:00
|
|
|
return f"{self._stop_duration} {plural(self._stop_duration, 'minute', 'minutes')}"
|
2020-12-07 10:08:54 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def lookback_period_str(self) -> str:
|
|
|
|
"""
|
|
|
|
Output configured lookback period in either candles or minutes
|
|
|
|
"""
|
|
|
|
if self._lookback_period_candles:
|
2024-05-12 14:38:10 +00:00
|
|
|
return (
|
|
|
|
f"{self._lookback_period_candles} "
|
|
|
|
f"{plural(self._lookback_period_candles, 'candle', 'candles')}"
|
|
|
|
)
|
2020-12-07 10:08:54 +00:00
|
|
|
else:
|
2024-05-12 15:51:21 +00:00
|
|
|
return f"{self._lookback_period} {plural(self._lookback_period, 'minute', 'minutes')}"
|
2020-12-07 10:08:54 +00:00
|
|
|
|
2024-07-04 08:29:13 +00:00
|
|
|
@property
|
|
|
|
def unlock_at_str(self) -> Union[str, None]:
|
|
|
|
"""
|
|
|
|
Output configured unlock time
|
|
|
|
"""
|
2024-07-16 04:28:02 +00:00
|
|
|
if self._unlock_at:
|
|
|
|
return self._unlock_at.strftime("%H:%M")
|
2024-07-04 08:29:13 +00:00
|
|
|
return None
|
|
|
|
|
2024-07-16 04:48:30 +00:00
|
|
|
@property
|
|
|
|
def unlock_reason_time_element(self) -> str:
|
|
|
|
"""
|
|
|
|
Output configured unlock time or stop duration
|
|
|
|
"""
|
|
|
|
if self.unlock_at_str is not None:
|
|
|
|
return f"until {self.unlock_at_str}"
|
|
|
|
else:
|
|
|
|
return f"for {self.stop_duration_str}"
|
|
|
|
|
2020-10-13 06:06:29 +00:00
|
|
|
@abstractmethod
|
|
|
|
def short_desc(self) -> str:
|
|
|
|
"""
|
|
|
|
Short method description - used for startup-messages
|
|
|
|
-> Please overwrite in subclasses
|
|
|
|
"""
|
2020-10-14 05:40:44 +00:00
|
|
|
|
|
|
|
@abstractmethod
|
2022-04-24 08:58:21 +00:00
|
|
|
def global_stop(self, date_now: datetime, side: LongShort) -> Optional[ProtectionReturn]:
|
2020-10-14 05:40:44 +00:00
|
|
|
"""
|
|
|
|
Stops trading (position entering) for all pairs
|
|
|
|
This must evaluate to true for the whole period of the "cooldown period".
|
|
|
|
"""
|
2020-10-24 14:52:26 +00:00
|
|
|
|
|
|
|
@abstractmethod
|
2022-04-24 08:58:21 +00:00
|
|
|
def stop_per_pair(
|
2024-05-12 14:38:10 +00:00
|
|
|
self, pair: str, date_now: datetime, side: LongShort
|
|
|
|
) -> Optional[ProtectionReturn]:
|
2020-10-24 14:52:26 +00:00
|
|
|
"""
|
|
|
|
Stops trading (position entering) for this pair
|
|
|
|
This must evaluate to true for the whole period of the "cooldown period".
|
|
|
|
:return: Tuple of [bool, until, reason].
|
|
|
|
If true, this pair will be locked with <reason> until <until>
|
|
|
|
"""
|
2020-11-17 18:43:12 +00:00
|
|
|
|
2024-07-16 05:05:42 +00:00
|
|
|
def calculate_lock_end(self, trades: List[LocalTrade]) -> datetime:
|
2020-11-17 18:43:12 +00:00
|
|
|
"""
|
|
|
|
Get lock end time
|
2024-07-16 05:05:42 +00:00
|
|
|
Implicitly uses `self._stop_duration` or `self._unlock_at` depending on the configuration.
|
2020-11-17 18:43:12 +00:00
|
|
|
"""
|
2021-02-20 19:22:00 +00:00
|
|
|
max_date: datetime = max([trade.close_date for trade in trades if trade.close_date])
|
2024-04-18 20:51:25 +00:00
|
|
|
# coming from Database, tzinfo is not set.
|
2020-11-17 18:43:12 +00:00
|
|
|
if max_date.tzinfo is None:
|
|
|
|
max_date = max_date.replace(tzinfo=timezone.utc)
|
|
|
|
|
2024-07-16 05:05:42 +00:00
|
|
|
if self._unlock_at is not None:
|
|
|
|
# unlock_at case with fixed hour of the day
|
|
|
|
until = self._unlock_at
|
|
|
|
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)
|
2020-11-17 18:43:12 +00:00
|
|
|
return until
|