Update protection-interface to support per-side locks
This commit is contained in:
parent
6ff3b178b0
commit
9e199165b4
@ -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.
|
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.
|
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
|
``` python
|
||||||
@ -59,7 +61,8 @@ def protections(self):
|
|||||||
"lookback_period_candles": 24,
|
"lookback_period_candles": 24,
|
||||||
"trade_limit": 4,
|
"trade_limit": 4,
|
||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
"only_per_pair": False
|
"only_per_pair": False,
|
||||||
|
"only_per_side": True
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
@ -1604,21 +1604,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||||
self._notify_exit(trade, '', True)
|
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:
|
elif send_msg and not trade.open_order_id:
|
||||||
# Enter fill
|
# Enter fill
|
||||||
self._notify_enter(trade, order, fill=True)
|
self._notify_enter(trade, order, fill=True)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_protections(self, pair: str) -> None:
|
def handle_protections(self, pair: str, side: str) -> None:
|
||||||
prot_trig = self.protections.stop_per_pair(pair)
|
prot_trig = self.protections.stop_per_pair(pair, side=side)
|
||||||
if prot_trig:
|
if prot_trig:
|
||||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
|
||||||
msg.update(prot_trig.to_json())
|
msg.update(prot_trig.to_json())
|
||||||
self.rpc.send_msg(msg)
|
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:
|
if prot_trig_glb:
|
||||||
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
|
||||||
msg.update(prot_trig_glb.to_json())
|
msg.update(prot_trig_glb.to_json())
|
||||||
|
@ -849,10 +849,10 @@ class Backtesting:
|
|||||||
return 'short'
|
return 'short'
|
||||||
return None
|
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:
|
if enable_protections:
|
||||||
self.protections.stop_per_pair(pair, current_time)
|
self.protections.stop_per_pair(pair, current_time, side)
|
||||||
self.protections.global_stop(current_time)
|
self.protections.global_stop(current_time, side)
|
||||||
|
|
||||||
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
|
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -1002,7 +1002,8 @@ class Backtesting:
|
|||||||
LocalTrade.close_bt_trade(trade)
|
LocalTrade.close_bt_trade(trade)
|
||||||
trades.append(trade)
|
trades.append(trade)
|
||||||
self.wallets.update()
|
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.
|
# Move time one configured time_interval ahead.
|
||||||
self.progress.increment()
|
self.progress.increment()
|
||||||
|
@ -44,13 +44,14 @@ class ProtectionManager():
|
|||||||
"""
|
"""
|
||||||
return [{p.name: p.short_desc()} for p in self._protection_handlers]
|
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:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
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 = 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
|
# Early stopping - first positive result blocks further trades
|
||||||
if lock and until:
|
if lock and until:
|
||||||
@ -58,13 +59,15 @@ class ProtectionManager():
|
|||||||
result = PairLocks.lock_pair('*', until, reason, now=now)
|
result = PairLocks.lock_pair('*', until, reason, now=now)
|
||||||
return result
|
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:
|
if not now:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
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 = 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 lock and until:
|
||||||
if not PairLocks.is_pair_locked(pair, until):
|
if not PairLocks.is_pair_locked(pair, until):
|
||||||
result = PairLocks.lock_pair(pair, until, reason, now=now)
|
result = PairLocks.lock_pair(pair, until, reason, now=now)
|
||||||
|
@ -26,7 +26,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) -> ProtectionReturn:
|
||||||
"""
|
"""
|
||||||
Get last trade for this pair
|
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)
|
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()
|
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
|
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 +57,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
|
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
|
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".
|
||||||
|
@ -12,7 +12,8 @@ from freqtrade.persistence import LocalTrade
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class IProtection(LoggingMixin, ABC):
|
||||||
@ -80,14 +81,14 @@ class IProtection(LoggingMixin, ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
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) -> ProtectionReturn:
|
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> 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".
|
||||||
|
@ -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
|
return False, None, None, 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,20 @@ 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)
|
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
|
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
|
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
|
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".
|
||||||
|
@ -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
|
return False, None, None, 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
|
return False, None, None, None
|
||||||
|
|
||||||
if drawdown > self._max_allowed_drawdown:
|
if drawdown > self._max_allowed_drawdown:
|
||||||
self.log_once(
|
self.log_once(
|
||||||
@ -66,11 +66,11 @@ 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)
|
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
|
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 +79,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) -> ProtectionReturn:
|
def stop_per_pair(self, pair: str, date_now: datetime, side: str) -> 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
|
return False, None, None, None
|
||||||
|
@ -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.enums import ExitType
|
from freqtrade.enums import ExitType
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -21,6 +21,7 @@ class StoplossGuard(IProtection):
|
|||||||
|
|
||||||
self._trade_limit = protection_config.get('trade_limit', 10)
|
self._trade_limit = protection_config.get('trade_limit', 10)
|
||||||
self._disable_global_stop = protection_config.get('only_per_pair', False)
|
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:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
@ -36,7 +37,8 @@ class StoplossGuard(IProtection):
|
|||||||
return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, '
|
return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, '
|
||||||
f'locking for {self._stop_duration} 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
|
Evaluate recent trades
|
||||||
"""
|
"""
|
||||||
@ -48,15 +50,19 @@ class StoplossGuard(IProtection):
|
|||||||
ExitType.STOPLOSS_ON_EXCHANGE.value)
|
ExitType.STOPLOSS_ON_EXCHANGE.value)
|
||||||
and trade.close_profit and trade.close_profit < 0)]
|
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:
|
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} "
|
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()
|
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
|
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".
|
||||||
@ -64,14 +70,14 @@ 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
|
return False, None, None, None
|
||||||
return self._stoploss_guard(date_now, 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
|
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 self._stoploss_guard(date_now, pair)
|
return self._stoploss_guard(date_now, pair, side)
|
||||||
|
Loading…
Reference in New Issue
Block a user