Update protection-interface to support per-side locks

This commit is contained in:
Matthias 2022-04-23 19:58:20 +02:00
parent 6ff3b178b0
commit 9e199165b4
9 changed files with 58 additions and 44 deletions

View File

@ -48,6 +48,8 @@ If `trade_limit` or more trades resulted in stoploss, trading will stop for `sto
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
Similarly, this protection will by default look at all trades (long and short). For futures bots, setting `only_per_side` will make the bot only one side, and will then only lock this one side.
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
``` python
@ -59,7 +61,8 @@ def protections(self):
"lookback_period_candles": 24,
"trade_limit": 4,
"stop_duration_candles": 4,
"only_per_pair": False
"only_per_pair": False,
"only_per_side": True
}
]
```

View File

@ -1604,21 +1604,21 @@ class FreqtradeBot(LoggingMixin):
if not trade.is_open:
if send_msg and not stoploss_order and not trade.open_order_id:
self._notify_exit(trade, '', True)
self.handle_protections(trade.pair)
self.handle_protections(trade.pair, trade.trade_direction)
elif send_msg and not trade.open_order_id:
# Enter fill
self._notify_enter(trade, order, fill=True)
return False
def handle_protections(self, pair: str) -> None:
prot_trig = self.protections.stop_per_pair(pair)
def handle_protections(self, pair: str, side: str) -> None:
prot_trig = self.protections.stop_per_pair(pair, side=side)
if prot_trig:
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
msg.update(prot_trig.to_json())
self.rpc.send_msg(msg)
prot_trig_glb = self.protections.global_stop()
prot_trig_glb = self.protections.global_stop(side=side)
if prot_trig_glb:
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
msg.update(prot_trig_glb.to_json())

View File

@ -849,10 +849,10 @@ class Backtesting:
return 'short'
return None
def run_protections(self, enable_protections, pair: str, current_time: datetime):
def run_protections(self, enable_protections, pair: str, current_time: datetime, side: str):
if enable_protections:
self.protections.stop_per_pair(pair, current_time)
self.protections.global_stop(current_time)
self.protections.stop_per_pair(pair, current_time, side)
self.protections.global_stop(current_time, side)
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
"""
@ -1002,7 +1002,8 @@ class Backtesting:
LocalTrade.close_bt_trade(trade)
trades.append(trade)
self.wallets.update()
self.run_protections(enable_protections, pair, current_time)
self.run_protections(
enable_protections, pair, current_time, trade.trade_direction)
# Move time one configured time_interval ahead.
self.progress.increment()

View File

@ -44,13 +44,14 @@ class ProtectionManager():
"""
return [{p.name: p.short_desc()} for p in self._protection_handlers]
def global_stop(self, now: Optional[datetime] = None) -> Optional[PairLock]:
def global_stop(self, now: Optional[datetime] = None, side: str = 'long') -> Optional[PairLock]:
if not now:
now = datetime.now(timezone.utc)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_global_stop:
lock, until, reason = protection_handler.global_stop(now)
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:
@ -58,13 +59,15 @@ class ProtectionManager():
result = PairLocks.lock_pair('*', until, reason, now=now)
return result
def stop_per_pair(self, pair, now: Optional[datetime] = None) -> Optional[PairLock]:
def stop_per_pair(
self, pair, now: Optional[datetime] = None, side: str = 'long') -> Optional[PairLock]:
if not now:
now = datetime.now(timezone.utc)
result = None
for protection_handler in self._protection_handlers:
if protection_handler.has_local_stop:
lock, until, reason = protection_handler.stop_per_pair(pair, now)
lock, until, reason, lock_side = 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)

View File

@ -26,7 +26,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) -> ProtectionReturn:
"""
Get last trade for this pair
"""
@ -45,11 +45,11 @@ 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()
return True, until, self._reason(), None
return False, None, None
return False, None, None, None
def global_stop(self, date_now: datetime) -> ProtectionReturn:
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@ -57,9 +57,9 @@ class CooldownPeriod(IProtection):
If true, all pairs will be locked with <reason> until <until>
"""
# Not implemented for cooldown period.
return False, None, None
return False, None, None, None
def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn:
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn:
"""
Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period".

View File

@ -12,7 +12,8 @@ from freqtrade.persistence import LocalTrade
logger = logging.getLogger(__name__)
ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str]]
# lock, until, reason, lock_side
ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str], Optional[str]]
class IProtection(LoggingMixin, ABC):
@ -80,14 +81,14 @@ class IProtection(LoggingMixin, ABC):
"""
@abstractmethod
def global_stop(self, date_now: datetime) -> ProtectionReturn:
def global_stop(self, date_now: datetime, side: str) -> 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) -> ProtectionReturn:
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn:
"""
Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period".

View File

@ -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
return False, None, None, None
profit = sum(trade.close_profit for trade in trades if trade.close_profit)
if profit < self._required_profit:
@ -60,20 +60,20 @@ 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)
return True, until, self._reason(profit), None
return False, None, None
return False, None, None, None
def global_stop(self, date_now: datetime) -> ProtectionReturn:
def global_stop(self, date_now: datetime, side: str) -> 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
return False, None, None, None
def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn:
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> ProtectionReturn:
"""
Stops trading (position entering) for this pair
This must evaluate to true for the whole period of the "cooldown period".

View File

@ -51,14 +51,14 @@ class MaxDrawdown(IProtection):
if len(trades) < self._trade_limit:
# Not enough trades in the relevant period
return False, None, None
return False, None, None, 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
return False, None, None, None
if drawdown > self._max_allowed_drawdown:
self.log_once(
@ -66,11 +66,11 @@ 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)
return True, until, self._reason(drawdown), None
return False, None, None
return False, None, None, None
def global_stop(self, date_now: datetime) -> ProtectionReturn:
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@ -79,11 +79,11 @@ class MaxDrawdown(IProtection):
"""
return self._max_drawdown(date_now)
def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn:
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> 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
return False, None, None, None

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.enums import ExitType
from freqtrade.persistence import Trade
@ -21,6 +21,7 @@ class StoplossGuard(IProtection):
self._trade_limit = protection_config.get('trade_limit', 10)
self._disable_global_stop = protection_config.get('only_per_pair', False)
self._only_per_side = protection_config.get('only_per_side', False)
def short_desc(self) -> str:
"""
@ -36,7 +37,8 @@ class StoplossGuard(IProtection):
return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, '
f'locking for {self._stop_duration} min.')
def _stoploss_guard(self, date_now: datetime, pair: str = None) -> ProtectionReturn:
def _stoploss_guard(
self, date_now: datetime, pair: Optional[str], side: str) -> ProtectionReturn:
"""
Evaluate recent trades
"""
@ -48,15 +50,19 @@ class StoplossGuard(IProtection):
ExitType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < 0)]
if self._only_per_side and side:
# Long or short trades only
trades = [trade for trade in trades if trade.trade_direction == side]
if len(trades) < self._trade_limit:
return False, None, None
return False, None, None, 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()
return True, until, self._reason(), (side if self._only_per_side else None)
def global_stop(self, date_now: datetime) -> ProtectionReturn:
def global_stop(self, date_now: datetime, side: str) -> ProtectionReturn:
"""
Stops trading (position entering) for all pairs
This must evaluate to true for the whole period of the "cooldown period".
@ -64,14 +70,14 @@ class StoplossGuard(IProtection):
If true, all pairs will be locked with <reason> until <until>
"""
if self._disable_global_stop:
return False, None, None
return self._stoploss_guard(date_now, None)
return False, None, None, None
return self._stoploss_guard(date_now, None, side)
def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn:
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> 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 self._stoploss_guard(date_now, pair)
return self._stoploss_guard(date_now, pair, side)