Convert ProtectionReturn to dataclass
This commit is contained in:
parent
9e199165b4
commit
b7cada1edd
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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".
|
||||||
|
@ -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".
|
||||||
|
@ -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".
|
||||||
|
@ -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
|
||||||
|
@ -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".
|
||||||
|
@ -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', [
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user