Files
Place-Order-Trading-Bot/loss_protection_manager.py
T
cbazzaandClaude Opus 4.5 4ac27cc7b4 feat: Add Loss Protection Manager with multi-layer safety system
- Daily loss limit (2% / $500 max)
- Consecutive loss breaker (3 losses → 2h cooldown)
- Max drawdown circuit breaker (10% threshold)
- News filter with 30min buffer for high-impact events
- Integrated as SCHRITT 0.5 in enhanced_trading_check_wrapper
- Combined lot multiplier with Equity Curve and Reversal Detector

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 17:18:29 +01:00

655 lines
24 KiB
Python

#!/usr/bin/env python3
"""
🛡️ LOSS PROTECTION MANAGER
Umfassender Verlustschutz für den Trading Bot
FEATURES:
1. Daily Loss Limit - Stoppt Trading nach X% Tagesverlust
2. Consecutive Loss Breaker - Pausiert nach X Verlusten in Folge
3. Max Drawdown Circuit Breaker - Hard Stop bei kritischem Drawdown
4. News Filter - Vermeidet Trading bei High-Impact News
VERWENDUNG:
from loss_protection_manager import LossProtectionManager
lpm = LossProtectionManager()
# Vor jedem Trade prüfen:
allowed, reason, multiplier = lpm.check_trading_allowed()
if not allowed:
print(f"Trading blocked: {reason}")
return None
# Nach jedem Trade updaten:
lpm.record_trade(profit=-50.00, symbol="XAUUSD")
"""
import json
import os
import logging
from datetime import datetime, timedelta
from typing import Dict, Tuple, List, Optional
import requests
logger = logging.getLogger(__name__)
class LossProtectionManager:
"""
Comprehensive Loss Protection System
Features:
- Daily loss limit with auto-reset at midnight
- Consecutive loss tracking with cooldown period
- Max drawdown circuit breaker
- Economic calendar news filter
"""
def __init__(self,
# Daily Loss Limit
daily_loss_limit_pct: float = 2.0,
daily_loss_limit_dollars: float = 500.0,
# Consecutive Loss Breaker
max_consecutive_losses: int = 3,
cooldown_minutes: int = 120,
# Max Drawdown Circuit Breaker
max_drawdown_pct: float = 10.0,
drawdown_recovery_pct: float = 5.0,
# News Filter
news_filter_enabled: bool = True,
news_buffer_minutes: int = 30,
block_high_impact: bool = True,
block_medium_impact: bool = False,
# General
data_file: str = "loss_protection_state.json",
account_balance: float = 100000.0):
"""
Args:
daily_loss_limit_pct: Max daily loss as % of account (default: 2%)
daily_loss_limit_dollars: Max daily loss in dollars (default: $500)
max_consecutive_losses: Losses in a row before pause (default: 3)
cooldown_minutes: Pause duration after consecutive losses (default: 2h)
max_drawdown_pct: Circuit breaker threshold (default: 10%)
drawdown_recovery_pct: Recovery needed to resume (default: 5%)
news_filter_enabled: Enable news filtering (default: True)
news_buffer_minutes: Minutes before/after news to avoid (default: 30)
block_high_impact: Block during high-impact news (default: True)
block_medium_impact: Block during medium-impact news (default: False)
data_file: File for persistent state
account_balance: Account balance for % calculations
"""
# Daily Loss Settings
self.daily_loss_limit_pct = daily_loss_limit_pct
self.daily_loss_limit_dollars = daily_loss_limit_dollars
# Consecutive Loss Settings
self.max_consecutive_losses = max_consecutive_losses
self.cooldown_minutes = cooldown_minutes
# Drawdown Settings
self.max_drawdown_pct = max_drawdown_pct
self.drawdown_recovery_pct = drawdown_recovery_pct
# News Settings
self.news_filter_enabled = news_filter_enabled
self.news_buffer_minutes = news_buffer_minutes
self.block_high_impact = block_high_impact
self.block_medium_impact = block_medium_impact
# General
self.data_file = data_file
self.account_balance = account_balance
# State
self.state = self._load_state()
self._check_daily_reset()
# News cache
self.news_cache = []
self.news_cache_time = None
self._log_initialization()
# ==========================================
# MAIN CHECK METHOD
# ==========================================
def check_trading_allowed(self, mt5_account_info=None) -> Tuple[bool, str, float]:
"""
Hauptprüfung ob Trading erlaubt ist
Args:
mt5_account_info: Optional MT5 account info for live balance
Returns:
(allowed, reason, lot_multiplier)
- allowed: True wenn Trading erlaubt
- reason: Erklärung
- lot_multiplier: 1.0 = normal, 0.5 = reduziert, 0.0 = blockiert
"""
# Update account balance if provided
if mt5_account_info:
self.account_balance = mt5_account_info.balance
# Check 1: Daily Loss Limit
daily_allowed, daily_reason = self._check_daily_loss_limit()
if not daily_allowed:
return False, f"🛑 DAILY LIMIT: {daily_reason}", 0.0
# Check 2: Consecutive Losses
consec_allowed, consec_reason, consec_mult = self._check_consecutive_losses()
if not consec_allowed:
return False, f"🛑 CONSECUTIVE LOSSES: {consec_reason}", 0.0
# Check 3: Max Drawdown Circuit Breaker
dd_allowed, dd_reason = self._check_drawdown_circuit_breaker()
if not dd_allowed:
return False, f"🛑 CIRCUIT BREAKER: {dd_reason}", 0.0
# Check 4: News Filter
news_allowed, news_reason, news_mult = self._check_news_filter()
if not news_allowed:
return False, f"🛑 NEWS FILTER: {news_reason}", 0.0
# All checks passed - calculate final multiplier
final_multiplier = min(consec_mult, news_mult)
# Build status message
status_parts = []
if self.state['daily_loss'] != 0:
status_parts.append(f"Daily: ${self.state['daily_loss']:.2f}")
if self.state['consecutive_losses'] > 0:
status_parts.append(f"Consec: {self.state['consecutive_losses']}")
if status_parts:
reason = f"✅ Trading allowed ({', '.join(status_parts)})"
else:
reason = "✅ All protection checks passed"
if final_multiplier < 1.0:
reason += f" | Lot: {final_multiplier:.0%}"
return True, reason, final_multiplier
# ==========================================
# INDIVIDUAL CHECKS
# ==========================================
def _check_daily_loss_limit(self) -> Tuple[bool, str]:
"""Prüft Daily Loss Limit"""
self._check_daily_reset()
daily_loss = abs(self.state['daily_loss'])
limit_dollars = self.daily_loss_limit_dollars
limit_pct = self.daily_loss_limit_pct
# Calculate % loss
if self.account_balance > 0:
loss_pct = (daily_loss / self.account_balance) * 100
else:
loss_pct = 0
# Check dollar limit
if daily_loss >= limit_dollars:
return False, f"${daily_loss:.2f} loss today (limit: ${limit_dollars:.2f})"
# Check percentage limit
if loss_pct >= limit_pct:
return False, f"{loss_pct:.1f}% loss today (limit: {limit_pct:.1f}%)"
return True, f"${daily_loss:.2f} / ${limit_dollars:.2f} ({loss_pct:.1f}%)"
def _check_consecutive_losses(self) -> Tuple[bool, str, float]:
"""Prüft Consecutive Loss Breaker"""
consec = self.state['consecutive_losses']
cooldown_until = self.state.get('cooldown_until')
# Check if in cooldown
if cooldown_until:
cooldown_time = datetime.fromisoformat(cooldown_until)
if datetime.now() < cooldown_time:
remaining = (cooldown_time - datetime.now()).total_seconds() / 60
return False, f"Cooldown active ({remaining:.0f} min remaining)", 0.0
else:
# Cooldown expired, reset
self.state['cooldown_until'] = None
self.state['consecutive_losses'] = 0
self._save_state()
# Check consecutive losses
if consec >= self.max_consecutive_losses:
# Activate cooldown
cooldown_until = datetime.now() + timedelta(minutes=self.cooldown_minutes)
self.state['cooldown_until'] = cooldown_until.isoformat()
self._save_state()
return False, f"{consec} consecutive losses - {self.cooldown_minutes}min cooldown activated", 0.0
# Calculate multiplier based on streak
if consec == 0:
multiplier = 1.0
elif consec == 1:
multiplier = 0.75 # Reduce after 1 loss
elif consec == 2:
multiplier = 0.5 # Reduce more after 2 losses
else:
multiplier = 0.25 # Minimal size
return True, f"{consec}/{self.max_consecutive_losses} consecutive losses", multiplier
def _check_drawdown_circuit_breaker(self) -> Tuple[bool, str]:
"""Prüft Max Drawdown Circuit Breaker"""
if self.state.get('circuit_breaker_active', False):
# Check if recovered enough
peak = self.state.get('peak_equity', self.account_balance)
current = self.account_balance
recovery_target = peak * (1 - (self.max_drawdown_pct - self.drawdown_recovery_pct) / 100)
if current >= recovery_target:
# Recovered, deactivate circuit breaker
self.state['circuit_breaker_active'] = False
self._save_state()
return True, "Circuit breaker deactivated - recovered"
else:
recovery_needed = recovery_target - current
return False, f"Circuit breaker active - need ${recovery_needed:.2f} recovery"
# Calculate current drawdown
peak = self.state.get('peak_equity', self.account_balance)
if self.account_balance > peak:
self.state['peak_equity'] = self.account_balance
peak = self.account_balance
self._save_state()
if peak > 0:
drawdown_pct = ((peak - self.account_balance) / peak) * 100
else:
drawdown_pct = 0
if drawdown_pct >= self.max_drawdown_pct:
# Activate circuit breaker
self.state['circuit_breaker_active'] = True
self._save_state()
return False, f"{drawdown_pct:.1f}% drawdown - circuit breaker ACTIVATED"
return True, f"Drawdown: {drawdown_pct:.1f}% (limit: {self.max_drawdown_pct:.1f}%)"
def _check_news_filter(self) -> Tuple[bool, str, float]:
"""Prüft News Filter"""
if not self.news_filter_enabled:
return True, "News filter disabled", 1.0
# Get upcoming news
news = self._get_upcoming_news()
if not news:
return True, "No high-impact news nearby", 1.0
# Check for news within buffer period
now = datetime.now()
buffer = timedelta(minutes=self.news_buffer_minutes)
for event in news:
event_time = event.get('time')
if event_time:
if isinstance(event_time, str):
try:
event_time = datetime.fromisoformat(event_time.replace('Z', '+00:00'))
except:
continue
time_diff = abs((event_time - now).total_seconds() / 60)
if time_diff <= self.news_buffer_minutes:
impact = event.get('impact', 'unknown')
title = event.get('title', 'Unknown Event')
if impact == 'high' and self.block_high_impact:
return False, f"High-impact: {title} in {time_diff:.0f}min", 0.0
elif impact == 'medium' and self.block_medium_impact:
return False, f"Medium-impact: {title} in {time_diff:.0f}min", 0.0
elif impact == 'high':
# Don't block but reduce size
return True, f"Caution: {title} in {time_diff:.0f}min", 0.5
return True, "No concerning news", 1.0
def _get_upcoming_news(self) -> List[Dict]:
"""Holt kommende News-Events (mit Caching)"""
# Use cache if recent (15 min)
if self.news_cache_time and (datetime.now() - self.news_cache_time).seconds < 900:
return self.news_cache
# Try to fetch from economic calendar API
try:
# ForexFactory-style calendar (simplified)
# In production, use a proper economic calendar API
self.news_cache = self._fetch_economic_calendar()
self.news_cache_time = datetime.now()
return self.news_cache
except Exception as e:
logger.debug(f"Could not fetch news: {e}")
return []
def _fetch_economic_calendar(self) -> List[Dict]:
"""
Fetcht Economic Calendar Events
In production sollte hier eine echte API verwendet werden:
- ForexFactory API
- Investing.com Calendar
- FXStreet Calendar
- etc.
"""
# Simplified: Return empty list or static high-impact events
# This is a placeholder - implement real API integration as needed
# Example static high-impact events (USD-focused for Gold trading)
static_events = [
# These would normally come from an API
# {"title": "FOMC Rate Decision", "time": "2026-01-30T19:00:00", "impact": "high", "currency": "USD"},
# {"title": "Non-Farm Payrolls", "time": "2026-02-07T13:30:00", "impact": "high", "currency": "USD"},
]
return static_events
# ==========================================
# TRADE RECORDING
# ==========================================
def record_trade(self, profit: float, symbol: str = "XAUUSD"):
"""
Zeichnet einen abgeschlossenen Trade auf
Args:
profit: Gewinn/Verlust des Trades
symbol: Gehandeltes Symbol
"""
self._check_daily_reset()
# Update daily P/L
self.state['daily_loss'] += profit if profit < 0 else 0
self.state['daily_profit'] += profit if profit > 0 else 0
self.state['daily_trades'] += 1
# Update consecutive losses
if profit < 0:
self.state['consecutive_losses'] += 1
self.state['consecutive_wins'] = 0
else:
self.state['consecutive_losses'] = 0
self.state['consecutive_wins'] += 1
# Update peak equity tracking
self.account_balance += profit # Approximate update
if self.account_balance > self.state.get('peak_equity', 0):
self.state['peak_equity'] = self.account_balance
# Log
logger.info(f"📊 Trade recorded: ${profit:+.2f} | Daily: ${self.state['daily_loss']:.2f} | Consec losses: {self.state['consecutive_losses']}")
self._save_state()
def reset_consecutive_losses(self):
"""Setzt Consecutive Loss Counter zurück (z.B. nach manuellem Review)"""
self.state['consecutive_losses'] = 0
self.state['cooldown_until'] = None
self._save_state()
logger.info("🔄 Consecutive losses reset")
def reset_daily_stats(self):
"""Setzt Tagesstatistiken zurück"""
self.state['daily_loss'] = 0
self.state['daily_profit'] = 0
self.state['daily_trades'] = 0
self.state['last_reset_date'] = datetime.now().date().isoformat()
self._save_state()
logger.info("🔄 Daily stats reset")
def deactivate_circuit_breaker(self):
"""Deaktiviert Circuit Breaker manuell (Vorsicht!)"""
self.state['circuit_breaker_active'] = False
self._save_state()
logger.warning("⚠️ Circuit breaker manually deactivated!")
# ==========================================
# STATUS & REPORTING
# ==========================================
def get_status(self) -> Dict:
"""Gibt vollständigen Status zurück"""
self._check_daily_reset()
# Calculate daily P/L percentage
daily_pnl = self.state['daily_profit'] + self.state['daily_loss']
daily_pnl_pct = (daily_pnl / self.account_balance * 100) if self.account_balance > 0 else 0
# Calculate drawdown
peak = self.state.get('peak_equity', self.account_balance)
drawdown_pct = ((peak - self.account_balance) / peak * 100) if peak > 0 else 0
return {
'trading_allowed': self.check_trading_allowed()[0],
# Daily Stats
'daily_loss': self.state['daily_loss'],
'daily_profit': self.state['daily_profit'],
'daily_pnl': daily_pnl,
'daily_pnl_pct': daily_pnl_pct,
'daily_trades': self.state['daily_trades'],
'daily_limit_pct': self.daily_loss_limit_pct,
'daily_limit_dollars': self.daily_loss_limit_dollars,
# Consecutive Losses
'consecutive_losses': self.state['consecutive_losses'],
'consecutive_wins': self.state['consecutive_wins'],
'max_consecutive_losses': self.max_consecutive_losses,
'cooldown_active': self.state.get('cooldown_until') is not None,
'cooldown_until': self.state.get('cooldown_until'),
# Drawdown
'current_drawdown_pct': drawdown_pct,
'max_drawdown_limit': self.max_drawdown_pct,
'circuit_breaker_active': self.state.get('circuit_breaker_active', False),
'peak_equity': peak,
'current_equity': self.account_balance,
# News
'news_filter_enabled': self.news_filter_enabled,
'upcoming_news': self._get_upcoming_news()[:3], # Top 3
}
def get_report(self) -> str:
"""Generiert formatierten Status-Report"""
status = self.get_status()
report = []
report.append("")
report.append("=" * 60)
report.append("🛡️ LOSS PROTECTION STATUS")
report.append("=" * 60)
# Overall Status
if status['trading_allowed']:
report.append(" Status: ✅ TRADING ALLOWED")
else:
report.append(" Status: 🛑 TRADING BLOCKED")
report.append("")
# Daily Loss Section
report.append("📅 DAILY LIMITS:")
daily_pct = abs(status['daily_loss']) / self.account_balance * 100 if self.account_balance > 0 else 0
report.append(f" Loss Today: ${abs(status['daily_loss']):,.2f} ({daily_pct:.1f}%)")
report.append(f" Limit: ${status['daily_limit_dollars']:,.2f} ({status['daily_limit_pct']}%)")
report.append(f" Trades Today: {status['daily_trades']}")
report.append("")
# Consecutive Losses Section
report.append("🔢 CONSECUTIVE LOSSES:")
report.append(f" Current Streak: {status['consecutive_losses']}/{status['max_consecutive_losses']}")
if status['cooldown_active']:
report.append(f" Cooldown Until: {status['cooldown_until']}")
else:
report.append(f" Cooldown: Not active")
report.append("")
# Drawdown Section
report.append("📉 DRAWDOWN CIRCUIT BREAKER:")
report.append(f" Current DD: {status['current_drawdown_pct']:.1f}%")
report.append(f" Limit: {status['max_drawdown_limit']}%")
report.append(f" Peak Equity: ${status['peak_equity']:,.2f}")
report.append(f" Current Equity: ${status['current_equity']:,.2f}")
if status['circuit_breaker_active']:
report.append(f" Circuit Breaker: 🔴 ACTIVE")
else:
report.append(f" Circuit Breaker: ✅ Inactive")
report.append("")
# News Section
report.append("📰 NEWS FILTER:")
report.append(f" Enabled: {'Yes' if status['news_filter_enabled'] else 'No'}")
if status['upcoming_news']:
report.append(f" Upcoming Events: {len(status['upcoming_news'])}")
for event in status['upcoming_news'][:2]:
report.append(f" - {event.get('title', 'Unknown')} ({event.get('impact', '?')})")
else:
report.append(f" Upcoming Events: None in buffer period")
report.append("")
report.append("=" * 60)
return "\n".join(report)
# ==========================================
# HELPER METHODS
# ==========================================
def _check_daily_reset(self):
"""Prüft ob Tagesstatistiken zurückgesetzt werden müssen"""
today = datetime.now().date().isoformat()
last_reset = self.state.get('last_reset_date', '')
if today != last_reset:
logger.info(f"📅 New day detected - resetting daily stats")
self.state['daily_loss'] = 0
self.state['daily_profit'] = 0
self.state['daily_trades'] = 0
self.state['last_reset_date'] = today
self._save_state()
def _load_state(self) -> Dict:
"""Lädt State aus Datei"""
default_state = {
'daily_loss': 0,
'daily_profit': 0,
'daily_trades': 0,
'last_reset_date': datetime.now().date().isoformat(),
'consecutive_losses': 0,
'consecutive_wins': 0,
'cooldown_until': None,
'circuit_breaker_active': False,
'peak_equity': self.account_balance,
}
try:
if os.path.exists(self.data_file):
with open(self.data_file, 'r') as f:
loaded = json.load(f)
# Merge with defaults
for key in default_state:
if key not in loaded:
loaded[key] = default_state[key]
return loaded
except Exception as e:
logger.warning(f"Could not load state: {e}")
return default_state
def _save_state(self):
"""Speichert State in Datei"""
try:
with open(self.data_file, 'w') as f:
json.dump(self.state, f, indent=2)
except Exception as e:
logger.error(f"Could not save state: {e}")
def _log_initialization(self):
"""Loggt Initialisierung"""
logger.info("=" * 60)
logger.info("🛡️ LOSS PROTECTION MANAGER INITIALIZED")
logger.info("=" * 60)
logger.info(f" Daily Loss Limit: {self.daily_loss_limit_pct}% / ${self.daily_loss_limit_dollars}")
logger.info(f" Max Consec. Losses: {self.max_consecutive_losses} (cooldown: {self.cooldown_minutes}min)")
logger.info(f" Max Drawdown: {self.max_drawdown_pct}%")
logger.info(f" News Filter: {'Enabled' if self.news_filter_enabled else 'Disabled'}")
logger.info(f" Account Balance: ${self.account_balance:,.2f}")
logger.info("=" * 60)
# ==========================================
# INTEGRATION HELPER
# ==========================================
def create_loss_protection_check(lpm: LossProtectionManager):
"""
Erstellt eine Check-Funktion für die Bot-Integration
Usage:
loss_protection_check = create_loss_protection_check(lpm)
# In trading wrapper:
allowed, reason, mult = loss_protection_check()
"""
def check(mt5_account_info=None):
return lpm.check_trading_allowed(mt5_account_info)
return check
# ==========================================
# STANDALONE TESTING
# ==========================================
if __name__ == "__main__":
print("=" * 60)
print("🛡️ LOSS PROTECTION MANAGER TEST")
print("=" * 60)
# Create manager
lpm = LossProtectionManager(
daily_loss_limit_pct=2.0,
daily_loss_limit_dollars=500.0,
max_consecutive_losses=3,
cooldown_minutes=120,
max_drawdown_pct=10.0,
news_filter_enabled=True,
account_balance=100000.0
)
# Show initial status
print(lpm.get_report())
# Simulate some trades
print("\n🧪 Simulating trades...")
# Winning trade
lpm.record_trade(150.0, "XAUUSD")
allowed, reason, mult = lpm.check_trading_allowed()
print(f"After win: {reason} | Mult: {mult}")
# Losing trades
for i in range(3):
lpm.record_trade(-100.0, "XAUUSD")
allowed, reason, mult = lpm.check_trading_allowed()
print(f"After loss {i+1}: {reason} | Mult: {mult}")
# Show final status
print(lpm.get_report())