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()`
* `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 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
* lock_side - long, short or '*'.
The `until` portion should be calculated using the provided `calculate_lock_end()` method.

View File

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

View File

@ -1,6 +1,7 @@
import logging
from datetime import datetime, timedelta
from typing import Optional
from freqtrade.persistence import Trade
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}.")
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
"""
@ -45,11 +46,15 @@ class CooldownPeriod(IProtection):
self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info)
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
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>
"""
# 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
This must evaluate to true for the whole period of the "cooldown period".

View File

@ -1,8 +1,9 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
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.misc import plural
@ -12,8 +13,13 @@ from freqtrade.persistence import LocalTrade
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):
@ -81,14 +87,14 @@ class IProtection(LoggingMixin, ABC):
"""
@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
This must evaluate to true for the whole period of the "cooldown period".
"""
@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
This must evaluate to true for the whole period of the "cooldown period".

View File

@ -1,7 +1,7 @@
import logging
from datetime import datetime, timedelta
from typing import Any, Dict
from typing import Any, Dict, Optional
from freqtrade.persistence import Trade
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}, '
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
"""
@ -51,7 +51,7 @@ class LowProfitPairs(IProtection):
# trades = Trade.get_trades(filters).all()
if len(trades) < self._trade_limit:
# 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)
if profit < self._required_profit:
@ -60,20 +60,24 @@ class LowProfitPairs(IProtection):
f"within {self._lookback_period} minutes.", logger.info)
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
This must evaluate to true for the whole period of the "cooldown period".
:return: Tuple of [bool, until, reason].
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
This must evaluate to true for the whole period of the "cooldown period".

View File

@ -1,7 +1,7 @@
import logging
from datetime import datetime, timedelta
from typing import Any, Dict
from typing import Any, Dict, Optional
import pandas as pd
@ -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}.')
def _max_drawdown(self, date_now: datetime) -> ProtectionReturn:
def _max_drawdown(self, date_now: datetime) -> Optional[ProtectionReturn]:
"""
Evaluate recent trades for drawdown ...
"""
@ -51,14 +51,14 @@ class MaxDrawdown(IProtection):
if len(trades) < self._trade_limit:
# Not enough trades in the relevant period
return False, None, None, None
return None
# Drawdown is always positive
try:
# TODO: This should use absolute profit calculation, considering account balance.
drawdown, _, _, _, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit')
except ValueError:
return False, None, None, None
return None
if drawdown > self._max_allowed_drawdown:
self.log_once(
@ -66,11 +66,16 @@ class MaxDrawdown(IProtection):
f" within {self.lookback_period_str}.", logger.info)
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
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)
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
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>
"""
return False, None, None, None
return None

View File

@ -38,7 +38,7 @@ class StoplossGuard(IProtection):
f'locking for {self._stop_duration} min.')
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
"""
@ -55,14 +55,19 @@ class StoplossGuard(IProtection):
trades = [trade for trade in trades if trade.trade_direction == side]
if len(trades) < self._trade_limit:
return False, None, None, None
return None
self.log_once(f"Trading stopped due to {self._trade_limit} "
f"stoplosses within {self._lookback_period} minutes.", logger.info)
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
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 self._disable_global_stop:
return False, None, None, None
return None
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
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:
assert handler.name in constants.AVAILABLE_PROTECTIONS
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:
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', [

View File

@ -21,6 +21,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.protections.iprotection import ProtectionReturn
from freqtrade.worker import 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,
@ -441,9 +442,9 @@ def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
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)
freqtrade.handle_protections('ETC/BTC')
freqtrade.handle_protections('ETC/BTC', '*')
send_msg_mock = freqtrade.rpc.send_msg
assert send_msg_mock.call_count == 2
assert send_msg_mock.call_args_list[0][0][0]['type'] == RPCMessageType.PROTECTION_TRIGGER