Files
Place-Order-Trading-Bot/drawdown_protection.py
T

398 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
🛡️ Drawdown Protection System
Schützt vor übermäßigen Verlusten durch automatische Handels-Pausen
"""
from datetime import datetime, timedelta
from trading_database import TradingDatabase
from telegram_notifier import TelegramNotifier
import logging
logger = logging.getLogger(__name__)
class DrawdownProtection:
"""
Überwacht Verluste und pausiert Trading bei Überschreitung von Limits
"""
def __init__(self,
database: TradingDatabase,
telegram: TelegramNotifier = None,
max_daily_loss: float = 100.0,
max_weekly_loss: float = 300.0,
max_monthly_loss: float = 800.0,
max_consecutive_losses: int = 5,
cooldown_hours: int = 24):
"""
Args:
database: TradingDatabase instance
telegram: TelegramNotifier instance
max_daily_loss: Maximaler Tagesverlust in $
max_weekly_loss: Maximaler Wochenverlust in $
max_monthly_loss: Maximaler Monatsverlust in $
max_consecutive_losses: Maximale Anzahl aufeinanderfolgender Verluste
cooldown_hours: Pause in Stunden nach Limit-Überschreitung
"""
self.db = database
self.telegram = telegram
# Limits
self.max_daily_loss = max_daily_loss
self.max_weekly_loss = max_weekly_loss
self.max_monthly_loss = max_monthly_loss
self.max_consecutive_losses = max_consecutive_losses
self.cooldown_hours = cooldown_hours
# State
self.trading_paused = False
self.pause_until = None
self.pause_reason = None
def can_trade(self) -> tuple[bool, str]:
"""
Prüft ob Trading erlaubt ist
Returns:
(can_trade: bool, reason: str)
"""
# Prüfe ob Pause noch aktiv
if self.trading_paused and self.pause_until:
if datetime.now() < self.pause_until:
remaining = (self.pause_until - datetime.now()).total_seconds() / 3600
return False, f"Trading paused for {remaining:.1f} more hours. Reason: {self.pause_reason}"
else:
# Pause abgelaufen
self._resume_trading()
# Prüfe täglichen Verlust
daily_loss = self._get_loss_today()
if daily_loss >= self.max_daily_loss:
self._pause_trading(
f"Daily loss limit reached: ${daily_loss:.2f} / ${self.max_daily_loss:.2f}",
hours=self.cooldown_hours
)
return False, f"Daily loss limit: ${daily_loss:.2f}"
# Prüfe wöchentlichen Verlust
weekly_loss = self._get_loss_this_week()
if weekly_loss >= self.max_weekly_loss:
self._pause_trading(
f"Weekly loss limit reached: ${weekly_loss:.2f} / ${self.max_weekly_loss:.2f}",
hours=self.cooldown_hours * 2
)
return False, f"Weekly loss limit: ${weekly_loss:.2f}"
# Prüfe monatlichen Verlust
monthly_loss = self._get_loss_this_month()
if monthly_loss >= self.max_monthly_loss:
self._pause_trading(
f"Monthly loss limit reached: ${monthly_loss:.2f} / ${self.max_monthly_loss:.2f}",
hours=self.cooldown_hours * 7
)
return False, f"Monthly loss limit: ${monthly_loss:.2f}"
# Prüfe aufeinanderfolgende Verluste
consecutive = self._get_consecutive_losses()
if consecutive >= self.max_consecutive_losses:
self._pause_trading(
f"Too many consecutive losses: {consecutive} in a row",
hours=self.cooldown_hours
)
return False, f"Consecutive losses: {consecutive}"
return True, "OK"
def _get_loss_today(self) -> float:
"""Berechnet Verlust heute"""
try:
today = datetime.now().strftime('%Y-%m-%d')
conn = self.db.conn
cursor = conn.cursor()
cursor.execute("""
SELECT COALESCE(SUM(net_profit), 0)
FROM trades
WHERE DATE(exit_time) = ?
AND status = 'closed'
AND net_profit < 0
""", (today,))
loss = cursor.fetchone()[0]
return abs(loss) if loss else 0.0
except Exception as e:
logger.error(f"Error calculating daily loss: {e}")
return 0.0
def _get_loss_this_week(self) -> float:
"""Berechnet Verlust diese Woche"""
try:
week_start = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
conn = self.db.conn
cursor = conn.cursor()
cursor.execute("""
SELECT COALESCE(SUM(net_profit), 0)
FROM trades
WHERE DATE(exit_time) >= ?
AND status = 'closed'
AND net_profit < 0
""", (week_start,))
loss = cursor.fetchone()[0]
return abs(loss) if loss else 0.0
except Exception as e:
logger.error(f"Error calculating weekly loss: {e}")
return 0.0
def _get_loss_this_month(self) -> float:
"""Berechnet Verlust diesen Monat"""
try:
month_start = datetime.now().replace(day=1).strftime('%Y-%m-%d')
conn = self.db.conn
cursor = conn.cursor()
cursor.execute("""
SELECT COALESCE(SUM(net_profit), 0)
FROM trades
WHERE DATE(exit_time) >= ?
AND status = 'closed'
AND net_profit < 0
""", (month_start,))
loss = cursor.fetchone()[0]
return abs(loss) if loss else 0.0
except Exception as e:
logger.error(f"Error calculating monthly loss: {e}")
return 0.0
def _get_consecutive_losses(self) -> int:
"""Zählt aufeinanderfolgende Verluste"""
try:
conn = self.db.conn
cursor = conn.cursor()
cursor.execute("""
SELECT net_profit
FROM trades
WHERE status = 'closed'
ORDER BY exit_time DESC
LIMIT 20
""")
trades = cursor.fetchall()
consecutive = 0
for trade in trades:
if trade[0] < 0:
consecutive += 1
else:
break
return consecutive
except Exception as e:
logger.error(f"Error counting consecutive losses: {e}")
return 0
def _pause_trading(self, reason: str, hours: int):
"""Pausiert Trading"""
self.trading_paused = True
self.pause_until = datetime.now() + timedelta(hours=hours)
self.pause_reason = reason
logger.warning(f"🛑 Trading paused: {reason}")
logger.warning(f" Resuming at: {self.pause_until}")
if self.telegram:
self.telegram.send_message(
f"🛑 **TRADING PAUSED**\n\n"
f"Reason: {reason}\n"
f"Duration: {hours} hours\n"
f"Resume at: {self.pause_until.strftime('%Y-%m-%d %H:%M')}\n\n"
f"️ Review your strategy and market conditions."
)
def _resume_trading(self):
"""Setzt Trading fort"""
self.trading_paused = False
self.pause_until = None
previous_reason = self.pause_reason
self.pause_reason = None
logger.info(f"✅ Trading resumed after: {previous_reason}")
if self.telegram:
self.telegram.send_message(
f"✅ **TRADING RESUMED**\n\n"
f"Previous pause reason: {previous_reason}\n"
f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
)
def get_status(self) -> dict:
"""Gibt aktuellen Status zurück"""
return {
'trading_allowed': not self.trading_paused,
'paused': self.trading_paused,
'pause_reason': self.pause_reason,
'pause_until': self.pause_until.isoformat() if self.pause_until else None,
'daily_loss': self._get_loss_today(),
'daily_limit': self.max_daily_loss,
'weekly_loss': self._get_loss_this_week(),
'weekly_limit': self.max_weekly_loss,
'monthly_loss': self._get_loss_this_month(),
'monthly_limit': self.max_monthly_loss,
'consecutive_losses': self._get_consecutive_losses(),
'consecutive_limit': self.max_consecutive_losses
}
def force_pause(self, reason: str, hours: int = 24):
"""Manuelles Pausieren"""
self._pause_trading(reason, hours)
def force_resume(self):
"""Manuelles Fortsetzen"""
self._resume_trading()
# ==========================================
# INTEGRATION WRAPPER
# ==========================================
def create_protected_trading_check(infra, original_check_function):
"""
Wrapper der Drawdown Protection um Trading Check legt
Usage:
# Im Notebook:
from drawdown_protection import DrawdownProtection, create_protected_trading_check
# Erstelle Protection
drawdown_protection = DrawdownProtection(
database=infra.db,
telegram=infra.telegram,
max_daily_loss=100,
max_weekly_loss=300,
max_consecutive_losses=5
)
# Wrapping
original_trading_check = trading_check
trading_check = create_protected_trading_check(
infra,
original_trading_check
)
"""
# Create drawdown protection instance
protection = DrawdownProtection(
database=infra.db,
telegram=infra.telegram,
max_daily_loss=100, # $100 per day
max_weekly_loss=300, # $300 per week
max_monthly_loss=800, # $800 per month
max_consecutive_losses=5,
cooldown_hours=24
)
def protected_check():
"""Geschützter Trading Check"""
# Prüfe Drawdown Protection
can_trade, reason = protection.can_trade()
if not can_trade:
logger.warning(f"Trading blocked by drawdown protection: {reason}")
return
# Wenn OK, führe normalen Check aus
return original_check_function()
# Attach protection instance for access
protected_check.protection = protection
return protected_check
# ==========================================
# MONITORING HELPER
# ==========================================
def print_protection_status(protection: DrawdownProtection):
"""Gibt Protection Status formatiert aus"""
status = protection.get_status()
print("=" * 70)
print("🛡️ DRAWDOWN PROTECTION STATUS")
print("=" * 70)
if status['trading_allowed']:
print("\n✅ Trading ALLOWED")
else:
print("\n🛑 Trading PAUSED")
print(f" Reason: {status['pause_reason']}")
print(f" Until: {status['pause_until']}")
print("\n📊 Current Losses:")
print(f" Daily: ${status['daily_loss']:.2f} / ${status['daily_limit']:.2f}")
print(f" Weekly: ${status['weekly_loss']:.2f} / ${status['weekly_limit']:.2f}")
print(f" Monthly: ${status['monthly_loss']:.2f} / ${status['monthly_limit']:.2f}")
print(f"\n📉 Consecutive Losses: {status['consecutive_losses']} / {status['consecutive_limit']}")
print("=" * 70)
# ==========================================
# USAGE EXAMPLE
# ==========================================
"""
INTEGRATION IN NOTEBOOK:
# Cell: Drawdown Protection Setup (nach Infrastructure)
from drawdown_protection import DrawdownProtection, create_protected_trading_check, print_protection_status
# Erstelle Drawdown Protection
drawdown_protection = DrawdownProtection(
database=infra.db,
telegram=infra.telegram,
max_daily_loss=100, # $100/Tag
max_weekly_loss=300, # $300/Woche
max_monthly_loss=800, # $800/Monat
max_consecutive_losses=5,
cooldown_hours=24
)
print("✅ Drawdown Protection aktiviert")
print(f" Max Daily Loss: ${drawdown_protection.max_daily_loss}")
print(f" Max Weekly Loss: ${drawdown_protection.max_weekly_loss}")
print(f" Max Consecutive Losses: {drawdown_protection.max_consecutive_losses}")
# Wrapper um trading_check
original_trading_check = trading_check
trading_check = create_protected_trading_check(infra, original_trading_check)
print("✅ Trading Check ist jetzt geschützt!")
# Cell: Status prüfen (optional, jederzeit ausführbar)
print_protection_status(drawdown_protection)
# Cell: Manuell pausieren/fortsetzen (optional)
# Manuell pausieren:
# drawdown_protection.force_pause("Manual review needed", hours=12)
# Manuell fortsetzen:
# drawdown_protection.force_resume()
"""