Convert ProtectionReturn to dataclass

This commit is contained in:
Matthias 2022-04-24 10:29:19 +02:00
parent 9e199165b4
commit b7cada1edd
9 changed files with 74 additions and 50 deletions

View File

@ -200,11 +200,12 @@ For that reason, they must implement the following methods:
* `global_stop()` * `global_stop()`
* `stop_per_pair()`. * `stop_per_pair()`.
`global_stop()` and `stop_per_pair()` must return a ProtectionReturn tuple, which consists of: `global_stop()` and `stop_per_pair()` must return a ProtectionReturn object, which consists of:
* lock pair - boolean * lock pair - boolean
* lock until - datetime - until when should the pair be locked (will be rounded up to the next new candle) * lock until - datetime - until when should the pair be locked (will be rounded up to the next new candle)
* reason - string, used for logging and storage in the database * reason - string, used for logging and storage in the database
* lock_side - long, short or '*'.
The `until` portion should be calculated using the provided `calculate_lock_end()` method. The `until` portion should be calculated using the provided `calculate_lock_end()` method.

View File

@ -50,13 +50,10 @@ class ProtectionManager():
result = None result = None
for protection_handler in self._protection_handlers: for protection_handler in self._protection_handlers:
if protection_handler.has_global_stop: if protection_handler.has_global_stop:
lock, until, reason, lock_side = protection_handler.global_stop( lock = protection_handler.global_stop(date_now=now, side=side)
date_now=now, side=side) if lock and lock.until:
if not PairLocks.is_global_lock(lock.until):
# Early stopping - first positive result blocks further trades result = PairLocks.lock_pair('*', lock.until, lock.reason, now=now)
if lock and until:
if not PairLocks.is_global_lock(until):
result = PairLocks.lock_pair('*', until, reason, now=now)
return result return result
def stop_per_pair( def stop_per_pair(
@ -66,9 +63,9 @@ class ProtectionManager():
result = None result = None
for protection_handler in self._protection_handlers: for protection_handler in self._protection_handlers:
if protection_handler.has_local_stop: if protection_handler.has_local_stop:
lock, until, reason, lock_side = protection_handler.stop_per_pair( lock = protection_handler.stop_per_pair(
pair=pair, date_now=now, side=side) pair=pair, date_now=now, side=side)
if lock and until: if lock and lock.until:
if not PairLocks.is_pair_locked(pair, until): if not PairLocks.is_pair_locked(pair, lock.until):
result = PairLocks.lock_pair(pair, until, reason, now=now) result = PairLocks.lock_pair(pair, lock.until, lock.reason, now=now)
return result return result

View File

@ -1,6 +1,7 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.plugins.protections import IProtection, ProtectionReturn
@ -26,7 +27,7 @@ class CooldownPeriod(IProtection):
""" """
return (f"{self.name} - Cooldown period of {self.stop_duration_str}.") return (f"{self.name} - Cooldown period of {self.stop_duration_str}.")
def _cooldown_period(self, pair: str, date_now: datetime) -> ProtectionReturn: def _cooldown_period(self, pair: str, date_now: datetime) -> Optional[ProtectionReturn]:
""" """
Get last trade for this pair Get last trade for this pair
""" """
@ -45,11 +46,15 @@ class CooldownPeriod(IProtection):
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
until = self.calculate_lock_end([trade], self._stop_duration) until = self.calculate_lock_end([trade], self._stop_duration)
return True, until, self._reason(), None return ProtectionReturn(
lock=True,
until=until,
reason=self._reason(),
)
return False, None, None, None return None
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn: def global_stop(self, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for all pairs Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".
@ -57,9 +62,9 @@ class CooldownPeriod(IProtection):
If true, all pairs will be locked with <reason> until <until> If true, all pairs will be locked with <reason> until <until>
""" """
# Not implemented for cooldown period. # Not implemented for cooldown period.
return False, None, None, None return None
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn: def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for this pair Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".

View File

@ -1,8 +1,9 @@
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import plural from freqtrade.misc import plural
@ -12,8 +13,13 @@ from freqtrade.persistence import LocalTrade
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# lock, until, reason, lock_side
ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str], Optional[str]] @dataclass
class ProtectionReturn:
lock: bool
until: datetime
reason: Optional[str]
lock_side: Optional[str] = None
class IProtection(LoggingMixin, ABC): class IProtection(LoggingMixin, ABC):
@ -81,14 +87,14 @@ class IProtection(LoggingMixin, ABC):
""" """
@abstractmethod @abstractmethod
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn: def global_stop(self, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for all pairs Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".
""" """
@abstractmethod @abstractmethod
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn: def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for this pair Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".

View File

@ -1,7 +1,7 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict from typing import Any, Dict, Optional
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.plugins.protections import IProtection, ProtectionReturn
@ -35,7 +35,7 @@ class LowProfitPairs(IProtection):
return (f'{profit} < {self._required_profit} in {self.lookback_period_str}, ' return (f'{profit} < {self._required_profit} in {self.lookback_period_str}, '
f'locking for {self.stop_duration_str}.') f'locking for {self.stop_duration_str}.')
def _low_profit(self, date_now: datetime, pair: str) -> ProtectionReturn: def _low_profit(self, date_now: datetime, pair: str) -> Optional[ProtectionReturn]:
""" """
Evaluate recent trades for pair Evaluate recent trades for pair
""" """
@ -51,7 +51,7 @@ class LowProfitPairs(IProtection):
# trades = Trade.get_trades(filters).all() # trades = Trade.get_trades(filters).all()
if len(trades) < self._trade_limit: if len(trades) < self._trade_limit:
# Not enough trades in the relevant period # Not enough trades in the relevant period
return False, None, None, None return None
profit = sum(trade.close_profit for trade in trades if trade.close_profit) profit = sum(trade.close_profit for trade in trades if trade.close_profit)
if profit < self._required_profit: if profit < self._required_profit:
@ -60,20 +60,24 @@ class LowProfitPairs(IProtection):
f"within {self._lookback_period} minutes.", logger.info) f"within {self._lookback_period} minutes.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration) until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason(profit), None return ProtectionReturn(
lock=True,
until=until,
reason=self._reason(profit),
)
return False, None, None, None return None
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn: def global_stop(self, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for all pairs Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".
:return: Tuple of [bool, until, reason]. :return: Tuple of [bool, until, reason].
If true, all pairs will be locked with <reason> until <until> If true, all pairs will be locked with <reason> until <until>
""" """
return False, None, None, None return None
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn: def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for this pair Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".

View File

@ -1,7 +1,7 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict from typing import Any, Dict, Optional
import pandas as pd import pandas as pd
@ -39,7 +39,7 @@ class MaxDrawdown(IProtection):
return (f'{drawdown} passed {self._max_allowed_drawdown} in {self.lookback_period_str}, ' return (f'{drawdown} passed {self._max_allowed_drawdown} in {self.lookback_period_str}, '
f'locking for {self.stop_duration_str}.') f'locking for {self.stop_duration_str}.')
def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: def _max_drawdown(self, date_now: datetime) -> Optional[ProtectionReturn]:
""" """
Evaluate recent trades for drawdown ... Evaluate recent trades for drawdown ...
""" """
@ -51,14 +51,14 @@ class MaxDrawdown(IProtection):
if len(trades) < self._trade_limit: if len(trades) < self._trade_limit:
# Not enough trades in the relevant period # Not enough trades in the relevant period
return False, None, None, None return None
# Drawdown is always positive # Drawdown is always positive
try: try:
# TODO: This should use absolute profit calculation, considering account balance. # TODO: This should use absolute profit calculation, considering account balance.
drawdown, _, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') drawdown, _, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
except ValueError: except ValueError:
return False, None, None, None return None
if drawdown > self._max_allowed_drawdown: if drawdown > self._max_allowed_drawdown:
self.log_once( self.log_once(
@ -66,11 +66,16 @@ class MaxDrawdown(IProtection):
f" within {self.lookback_period_str}.", logger.info) f" within {self.lookback_period_str}.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration) until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason(drawdown), None # return True, until, self._reason(drawdown), None
return ProtectionReturn(
lock=True,
until=until,
reason=self._reason(drawdown),
)
return False, None, None, None return None
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn: def global_stop(self, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for all pairs Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".
@ -79,11 +84,11 @@ class MaxDrawdown(IProtection):
""" """
return self._max_drawdown(date_now) return self._max_drawdown(date_now)
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn: def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for this pair Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".
:return: Tuple of [bool, until, reason]. :return: Tuple of [bool, until, reason].
If true, this pair will be locked with <reason> until <until> If true, this pair will be locked with <reason> until <until>
""" """
return False, None, None, None return None

View File

@ -38,7 +38,7 @@ class StoplossGuard(IProtection):
f'locking for {self._stop_duration} min.') f'locking for {self._stop_duration} min.')
def _stoploss_guard( def _stoploss_guard(
self, date_now: datetime, pair: Optional[str], side: str) -> ProtectionReturn: self, date_now: datetime, pair: Optional[str], side: str) -> Optional[ProtectionReturn]:
""" """
Evaluate recent trades Evaluate recent trades
""" """
@ -55,14 +55,19 @@ class StoplossGuard(IProtection):
trades = [trade for trade in trades if trade.trade_direction == side] trades = [trade for trade in trades if trade.trade_direction == side]
if len(trades) < self._trade_limit: if len(trades) < self._trade_limit:
return False, None, None, None return None
self.log_once(f"Trading stopped due to {self._trade_limit} " self.log_once(f"Trading stopped due to {self._trade_limit} "
f"stoplosses within {self._lookback_period} minutes.", logger.info) f"stoplosses within {self._lookback_period} minutes.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration) until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason(), (side if self._only_per_side else None) return ProtectionReturn(
lock=True,
until=until,
reason=self._reason(),
lock_side=(side if self._only_per_side else None)
)
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn: def global_stop(self, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for all pairs Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".
@ -70,10 +75,10 @@ class StoplossGuard(IProtection):
If true, all pairs will be locked with <reason> until <until> If true, all pairs will be locked with <reason> until <until>
""" """
if self._disable_global_stop: if self._disable_global_stop:
return False, None, None, None return None
return self._stoploss_guard(date_now, None, side) return self._stoploss_guard(date_now, None, side)
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn: def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> Optional[ProtectionReturn]:
""" """
Stops trading (position entering) for this pair Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period". This must evaluate to true for the whole period of the "cooldown period".

View File

@ -45,9 +45,9 @@ def test_protectionmanager(mocker, default_conf):
for handler in freqtrade.protections._protection_handlers: for handler in freqtrade.protections._protection_handlers:
assert handler.name in constants.AVAILABLE_PROTECTIONS assert handler.name in constants.AVAILABLE_PROTECTIONS
if not handler.has_global_stop: if not handler.has_global_stop:
assert handler.global_stop(datetime.utcnow()) == (False, None, None) assert handler.global_stop(datetime.utcnow(), '*') is None
if not handler.has_local_stop: if not handler.has_local_stop:
assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) assert handler.stop_per_pair('XRP/BTC', datetime.utcnow(), '*') is None
@pytest.mark.parametrize('timeframe,expected,protconf', [ @pytest.mark.parametrize('timeframe,expected,protconf', [

View File

@ -21,6 +21,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock from freqtrade.persistence.models import PairLock
from freqtrade.plugins.protections.iprotection import ProtectionReturn
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker,
log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal,
@ -441,9 +442,9 @@ def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
freqtrade.protections._protection_handlers[1].global_stop = MagicMock( freqtrade.protections._protection_handlers[1].global_stop = MagicMock(
return_value=(True, arrow.utcnow().shift(hours=1).datetime, "asdf")) return_value=ProtectionReturn(True, arrow.utcnow().shift(hours=1).datetime, "asdf"))
create_mock_trades(fee, is_short) create_mock_trades(fee, is_short)
freqtrade.handle_protections('ETC/BTC') freqtrade.handle_protections('ETC/BTC', '*')
send_msg_mock = freqtrade.rpc.send_msg send_msg_mock = freqtrade.rpc.send_msg
assert send_msg_mock.call_count == 2 assert send_msg_mock.call_count == 2
assert send_msg_mock.call_args_list[0][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER assert send_msg_mock.call_args_list[0][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER