2026-01-30 17:18:29 +01:00
|
|
|
#!/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)"""
|
2026-05-12 09:55:09 +02:00
|
|
|
# Use cache if recent (15 min) — must use .total_seconds(), not .seconds
|
|
|
|
|
# .seconds returns only the seconds component (resets at 1h), so after 2h it would be 0
|
|
|
|
|
if self.news_cache_time and (datetime.now() - self.news_cache_time).total_seconds() < 900:
|
2026-01-30 17:18:29 +01:00
|
|
|
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]:
|
|
|
|
|
"""
|
2026-05-12 09:55:09 +02:00
|
|
|
Fetcht Economic Calendar Events.
|
2026-01-30 17:18:29 +01:00
|
|
|
|
2026-05-12 09:55:09 +02:00
|
|
|
NOTE: Currently returns an empty list — the news filter is inactive.
|
|
|
|
|
To activate it, either:
|
|
|
|
|
a) Use news_filter_simple.py: load events from news_events_manual.json
|
|
|
|
|
b) Integrate a real API (e.g. Finnhub, see news_filter_v2.py as reference)
|
|
|
|
|
|
|
|
|
|
Example integration with news_filter_simple:
|
|
|
|
|
from news_filter_simple import get_upcoming_events
|
|
|
|
|
return get_upcoming_events()
|
2026-01-30 17:18:29 +01:00
|
|
|
"""
|
2026-05-12 09:55:09 +02:00
|
|
|
try:
|
|
|
|
|
from news_filter_simple import EconomicCalendarSimple
|
|
|
|
|
cal = EconomicCalendarSimple()
|
|
|
|
|
return cal.get_upcoming_events(minutes_ahead=self.news_buffer_minutes,
|
|
|
|
|
minutes_after=self.news_buffer_minutes)
|
|
|
|
|
except ImportError:
|
|
|
|
|
pass
|
|
|
|
|
return []
|
2026-01-30 17:18:29 +01:00
|
|
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
# 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
|
|
|
|
|
|
2026-05-12 09:55:09 +02:00
|
|
|
# Approximate balance update — drifts from real MT5 balance over time.
|
|
|
|
|
# Pass mt5_account_info to check_trading_allowed() for accurate values.
|
|
|
|
|
self.account_balance += profit
|
2026-01-30 17:18:29 +01:00
|
|
|
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())
|