drawdown_monitor.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. from __future__ import annotations
  2. import logging
  3. from typing import TYPE_CHECKING, Optional
  4. from datetime import datetime, timezone
  5. if TYPE_CHECKING:
  6. from src.stats import TradingStats
  7. logger = logging.getLogger(__name__)
  8. class DrawdownMonitor:
  9. """
  10. Tracks portfolio balance in memory to calculate max drawdown in real-time
  11. and persists its state to the database.
  12. """
  13. def __init__(self, stats: "TradingStats"):
  14. """
  15. Initializes the DrawdownMonitor.
  16. Args:
  17. stats: An instance of the TradingStats class to persist state.
  18. """
  19. self.stats = stats
  20. self.peak_balance = 0.0
  21. self.peak_balance_timestamp: Optional[datetime] = None
  22. self.max_drawdown_pct = 0.0
  23. self.current_drawdown_pct = 0.0
  24. self._load_state()
  25. def _load_state(self):
  26. """Load the persisted state from the database."""
  27. try:
  28. peak_balance_str = self.stats._get_metadata('drawdown_peak_balance')
  29. max_drawdown_pct_str = self.stats._get_metadata('drawdown_max_drawdown_pct')
  30. peak_balance_timestamp_str = self.stats._get_metadata('drawdown_peak_balance_timestamp')
  31. self.peak_balance = float(peak_balance_str) if peak_balance_str else 0.0
  32. self.max_drawdown_pct = float(max_drawdown_pct_str) if max_drawdown_pct_str else 0.0
  33. if peak_balance_timestamp_str:
  34. self.peak_balance_timestamp = datetime.fromisoformat(peak_balance_timestamp_str)
  35. # If peak balance is zero, initialize it with the initial account balance.
  36. if self.peak_balance == 0.0:
  37. initial_balance_str = self.stats._get_metadata('initial_balance')
  38. if initial_balance_str:
  39. self.peak_balance = float(initial_balance_str)
  40. self.peak_balance_timestamp = datetime.now(timezone.utc)
  41. logger.info(f"DrawdownMonitor state loaded: Peak Balance=${self.peak_balance:,.2f}, Max Drawdown={self.max_drawdown_pct:.2f}%")
  42. except Exception as e:
  43. logger.error(f"Error loading DrawdownMonitor state: {e}", exc_info=True)
  44. def _save_state(self):
  45. """Save the current state to the database."""
  46. try:
  47. self.stats._set_metadata('drawdown_peak_balance', str(self.peak_balance))
  48. self.stats._set_metadata('drawdown_max_drawdown_pct', str(self.max_drawdown_pct))
  49. if self.peak_balance_timestamp:
  50. self.stats._set_metadata('drawdown_peak_balance_timestamp', self.peak_balance_timestamp.isoformat())
  51. logger.debug("DrawdownMonitor state saved.")
  52. except Exception as e:
  53. logger.error(f"Error saving DrawdownMonitor state: {e}", exc_info=True)
  54. def update_balance(self, current_balance: float):
  55. """
  56. Update the balance and recalculate the drawdown if necessary.
  57. Args:
  58. current_balance: The current total balance of the portfolio.
  59. """
  60. state_changed = False
  61. if current_balance > self.peak_balance:
  62. self.peak_balance = current_balance
  63. self.peak_balance_timestamp = datetime.now(timezone.utc)
  64. state_changed = True
  65. if self.peak_balance > 0:
  66. drawdown = (self.peak_balance - current_balance) / self.peak_balance
  67. self.current_drawdown_pct = drawdown * 100
  68. # Only update if the new drawdown is significantly larger
  69. if self.current_drawdown_pct > self.max_drawdown_pct + 0.01:
  70. self.max_drawdown_pct = self.current_drawdown_pct
  71. state_changed = True
  72. if state_changed:
  73. self._save_state()
  74. def get_max_drawdown(self) -> float:
  75. """Returns the maximum drawdown percentage."""
  76. return self.max_drawdown_pct
  77. def get_current_drawdown(self) -> float:
  78. """Returns the current drawdown percentage since the last peak."""
  79. return self.current_drawdown_pct
  80. def get_peak_balance_timestamp(self) -> Optional[datetime]:
  81. """Returns the timestamp of the last peak balance."""
  82. return self.peak_balance_timestamp