forexsmartbot 3.2.1__tar.gz → 3.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/PKG-INFO +1 -1
  2. forexsmartbot-3.2.2/forexsmartbot/analytics/__init__.py +43 -0
  3. forexsmartbot-3.2.2/forexsmartbot/analytics/live_metrics.py +161 -0
  4. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/portfolio_analytics.py +20 -11
  5. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/risk_analytics.py +6 -1
  6. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/analytics_dialog.py +33 -19
  7. forexsmartbot-3.2.2/forexsmartbot/ui/analytics_widget.py +202 -0
  8. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/enhanced_main_window.py +7 -0
  9. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/PKG-INFO +1 -1
  10. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/SOURCES.txt +2 -0
  11. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/pyproject.toml +1 -1
  12. forexsmartbot-3.2.2/tests/test_live_metrics.py +70 -0
  13. forexsmartbot-3.2.1/forexsmartbot/analytics/__init__.py +0 -28
  14. forexsmartbot-3.2.1/forexsmartbot/ui/analytics_widget.py +0 -68
  15. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/LICENSE +0 -0
  16. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/README.md +0 -0
  17. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/__init__.py +0 -0
  18. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/__init__.py +0 -0
  19. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/__init__.py +0 -0
  20. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
  21. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
  22. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
  23. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
  24. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/__init__.py +0 -0
  25. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
  26. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/config.py +0 -0
  27. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/csv_provider.py +0 -0
  28. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
  29. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
  30. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/multi_provider.py +0 -0
  31. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
  32. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
  33. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
  34. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
  35. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/chart_patterns.py +0 -0
  36. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/correlation_matrix.py +0 -0
  37. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/economic_calendar.py +0 -0
  38. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/enhanced_chart.py +0 -0
  39. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/market_depth.py +0 -0
  40. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/performance_attribution.py +0 -0
  41. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/strategy_comparison.py +0 -0
  42. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/trade_journal.py +0 -0
  43. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/brokers/mt4_bridge.py +0 -0
  44. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/__init__.py +0 -0
  45. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/code_generator.py +0 -0
  46. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/strategy_builder.py +0 -0
  47. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/strategy_template.py +0 -0
  48. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/__init__.py +0 -0
  49. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/api_client.py +0 -0
  50. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/api_server.py +0 -0
  51. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/cloud_sync.py +0 -0
  52. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/error_handler.py +0 -0
  53. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/mobile_api.py +0 -0
  54. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/remote_monitor.py +0 -0
  55. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/security.py +0 -0
  56. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/backtester.py +0 -0
  57. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/config.py +0 -0
  58. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/interfaces.py +0 -0
  59. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/paper_broker.py +0 -0
  60. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/portfolio.py +0 -0
  61. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/risk.py +0 -0
  62. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/risk_engine.py +0 -0
  63. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/strategy.py +0 -0
  64. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/trade_manager.py +0 -0
  65. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/gui/main_window.py +0 -0
  66. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/gui/theme.py +0 -0
  67. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/marketplace/__init__.py +0 -0
  68. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/marketplace/marketplace.py +0 -0
  69. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/marketplace/strategy_rating.py +0 -0
  70. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/__init__.py +0 -0
  71. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/health_check.py +0 -0
  72. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/performance_tracker.py +0 -0
  73. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/strategy_monitor.py +0 -0
  74. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/__init__.py +0 -0
  75. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
  76. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
  77. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
  78. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/monte_carlo.py +0 -0
  79. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
  80. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
  81. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/walk_forward.py +0 -0
  82. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/__init__.py +0 -0
  83. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/backtest.py +0 -0
  84. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/calculation_engine.py +0 -0
  85. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/controller.py +0 -0
  86. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/data_streaming.py +0 -0
  87. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/enhanced_backtest.py +0 -0
  88. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/fast_trading_controller.py +0 -0
  89. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/gpu_backtest.py +0 -0
  90. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/language_manager.py +0 -0
  91. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/notification_service.py +0 -0
  92. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/persistence.py +0 -0
  93. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/startup_manager.py +0 -0
  94. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/__init__.py +0 -0
  95. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
  96. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/breakout_atr.py +0 -0
  97. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
  98. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/fear_index.py +0 -0
  99. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/lstm_strategy.py +0 -0
  100. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/mean_reversion.py +0 -0
  101. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
  102. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/momentum_breakout.py +0 -0
  103. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
  104. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/news_trading.py +0 -0
  105. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/rl_strategy.py +0 -0
  106. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/rsi_reversion.py +0 -0
  107. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/scalping_ma.py +0 -0
  108. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/sma_crossover.py +0 -0
  109. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/svm_strategy.py +0 -0
  110. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/transformer_strategy.py +0 -0
  111. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/__init__.py +0 -0
  112. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/ci_cd_pipeline.py +0 -0
  113. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/paper_account_tester.py +0 -0
  114. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/strategy_sandbox.py +0 -0
  115. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/__init__.py +0 -0
  116. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/backtest_dialog.py +0 -0
  117. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/charts.py +0 -0
  118. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/cloud_dialog.py +0 -0
  119. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/enhanced_charts.py +0 -0
  120. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/export_dialog.py +0 -0
  121. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/main_window.py +0 -0
  122. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/marketplace_dialog.py +0 -0
  123. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/monitoring_dialog.py +0 -0
  124. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/monitoring_widget.py +0 -0
  125. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/optimization_dialog.py +0 -0
  126. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/settings_dialog.py +0 -0
  127. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/simple_chart.py +0 -0
  128. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
  129. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/theme.py +0 -0
  130. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/tradingview_chart.py +0 -0
  131. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/__init__.py +0 -0
  132. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/gpu_numpy.py +0 -0
  133. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/gpu_utils.py +0 -0
  134. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/native_indicators.py +0 -0
  135. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/dependency_links.txt +0 -0
  136. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/entry_points.txt +0 -0
  137. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/requires.txt +0 -0
  138. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/top_level.txt +0 -0
  139. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/setup.cfg +0 -0
  140. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_config.py +0 -0
  141. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_multi_timeframe_strategy.py +0 -0
  142. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_native_indicators.py +0 -0
  143. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_risk.py +0 -0
  144. {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_strategies.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Professional forex trading bot with advanced risk management and strategy plugins
5
5
  Author-email: VoxHash <contact@voxhash.dev>
6
6
  License: MIT
@@ -0,0 +1,43 @@
1
+ """
2
+ Analytics module for ForexSmartBot v3.2.0
3
+ Provides portfolio analytics, risk analytics, and advanced charting features.
4
+ """
5
+
6
+ from .portfolio_analytics import PortfolioAnalytics
7
+ from .risk_analytics import RiskAnalytics
8
+ from .chart_patterns import ChartPatternRecognizer
9
+ from .performance_attribution import PerformanceAttribution
10
+ from .live_metrics import LiveMetricsSnapshot, build_live_metrics
11
+
12
+ # Qt-backed widgets are optional (headless / test environments may lack PyQt6)
13
+ try:
14
+ from .market_depth import MarketDepthWidget, MarketDepthProvider
15
+ from .correlation_matrix import CorrelationMatrixWidget, CorrelationAnalyzer
16
+ from .economic_calendar import EconomicCalendarWidget, EconomicCalendarProvider
17
+ from .trade_journal import TradeJournalWidget, TradeJournalManager
18
+ except ImportError: # pragma: no cover - UI optional
19
+ MarketDepthWidget = None # type: ignore[misc, assignment]
20
+ MarketDepthProvider = None # type: ignore[misc, assignment]
21
+ CorrelationMatrixWidget = None # type: ignore[misc, assignment]
22
+ CorrelationAnalyzer = None # type: ignore[misc, assignment]
23
+ EconomicCalendarWidget = None # type: ignore[misc, assignment]
24
+ EconomicCalendarProvider = None # type: ignore[misc, assignment]
25
+ TradeJournalWidget = None # type: ignore[misc, assignment]
26
+ TradeJournalManager = None # type: ignore[misc, assignment]
27
+
28
+ __all__ = [
29
+ 'PortfolioAnalytics',
30
+ 'RiskAnalytics',
31
+ 'ChartPatternRecognizer',
32
+ 'PerformanceAttribution',
33
+ 'MarketDepthWidget',
34
+ 'MarketDepthProvider',
35
+ 'CorrelationMatrixWidget',
36
+ 'CorrelationAnalyzer',
37
+ 'EconomicCalendarWidget',
38
+ 'EconomicCalendarProvider',
39
+ 'TradeJournalWidget',
40
+ 'TradeJournalManager',
41
+ 'LiveMetricsSnapshot',
42
+ 'build_live_metrics',
43
+ ]
@@ -0,0 +1,161 @@
1
+ """
2
+ Live portfolio and risk metrics for the analytics dashboard.
3
+
4
+ Pure computation (no Qt) so the snapshot can be unit-tested and reused by
5
+ dialogs, widgets, and remote monitors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass
11
+ from datetime import datetime
12
+ from typing import Any, Dict, List, Optional, Sequence
13
+
14
+ from ..core.interfaces import Trade
15
+ from .portfolio_analytics import PortfolioAnalytics
16
+ from .risk_analytics import RiskAnalytics
17
+
18
+
19
+ @dataclass
20
+ class LiveMetricsSnapshot:
21
+ """Point-in-time analytics snapshot for UI and monitoring."""
22
+
23
+ timestamp: datetime
24
+ equity: float
25
+ balance: float
26
+ realized_pnl: float
27
+ unrealized_pnl: float
28
+ daily_pnl: float
29
+ open_positions: int
30
+ total_trades: int
31
+ win_rate: float
32
+ profit_factor: float
33
+ sharpe_ratio: float
34
+ sortino_ratio: float
35
+ max_drawdown: float
36
+ max_drawdown_pct: float
37
+ current_drawdown: float
38
+ var_95: float
39
+ cvar_95: float
40
+ volatility: float
41
+ average_win: float
42
+ average_loss: float
43
+
44
+ def to_dict(self) -> Dict[str, Any]:
45
+ """Serialize for tables / JSON."""
46
+ data = asdict(self)
47
+ data["timestamp"] = self.timestamp.isoformat()
48
+ return data
49
+
50
+ def card_metrics(self) -> List[tuple]:
51
+ """Primary KPI cards: (label, formatted value, tone)."""
52
+ tone_pnl = "positive" if self.daily_pnl >= 0 else "negative"
53
+ tone_eq = "positive" if self.unrealized_pnl + self.realized_pnl >= 0 else "negative"
54
+ return [
55
+ ("Equity", f"${self.equity:,.2f}", tone_eq),
56
+ ("Daily P&L", f"${self.daily_pnl:,.2f}", tone_pnl),
57
+ ("Win Rate", f"{self.win_rate:.1f}%", "neutral"),
58
+ ("Sharpe", f"{self.sharpe_ratio:.3f}", "neutral"),
59
+ ("Max DD", f"{self.max_drawdown_pct:.2f}%", "negative" if self.max_drawdown_pct else "neutral"),
60
+ ("VaR 95%", f"{self.var_95:.4f}", "neutral"),
61
+ ]
62
+
63
+
64
+ def _portfolio_trades(portfolio) -> List[Trade]:
65
+ """Extract Trade list from Portfolio or trade sequence."""
66
+ if portfolio is None:
67
+ return []
68
+ if hasattr(portfolio, "trades"):
69
+ return list(portfolio.trades)
70
+ if isinstance(portfolio, Sequence):
71
+ return list(portfolio)
72
+ return []
73
+
74
+
75
+ def _sync_portfolio_analytics(analytics: PortfolioAnalytics, portfolio) -> None:
76
+ """Load trades and equity curve from a live Portfolio."""
77
+ analytics.trades = _portfolio_trades(portfolio)
78
+ analytics.equity_curve = []
79
+ if portfolio is None:
80
+ return
81
+ timestamps = getattr(portfolio, "timestamps", None)
82
+ equity_history = getattr(portfolio, "equity_history", None)
83
+ if timestamps is not None and equity_history is not None:
84
+ ts = list(timestamps)
85
+ eq = list(equity_history)
86
+ n = min(len(ts), len(eq))
87
+ analytics.equity_curve = list(zip(ts[:n], eq[:n]))
88
+
89
+
90
+ def _sync_risk_analytics(risk: RiskAnalytics, portfolio) -> None:
91
+ """Load trades into RiskAnalytics from a live Portfolio."""
92
+ risk.trades = _portfolio_trades(portfolio)
93
+
94
+
95
+ def build_live_metrics(portfolio, *, now: Optional[datetime] = None) -> LiveMetricsSnapshot:
96
+ """
97
+ Build a live metrics snapshot from a Portfolio (or trade container).
98
+
99
+ Uses Portfolio balance/equity when available; otherwise derives from trades.
100
+ """
101
+ now = now or datetime.now()
102
+ portfolio_analytics = PortfolioAnalytics()
103
+ risk_analytics = RiskAnalytics()
104
+ _sync_portfolio_analytics(portfolio_analytics, portfolio)
105
+ _sync_risk_analytics(risk_analytics, portfolio)
106
+
107
+ summary = portfolio_analytics.get_performance_summary()
108
+ max_dd = portfolio_analytics.calculate_max_drawdown()
109
+ var_result = risk_analytics.calculate_var(confidence_level=0.95)
110
+ cvar_result = risk_analytics.calculate_cvar(confidence_level=0.95)
111
+
112
+ returns = risk_analytics._get_returns()
113
+ volatility = float(returns.std()) if len(returns) >= 2 else 0.0
114
+
115
+ if portfolio is not None and hasattr(portfolio, "get_total_equity"):
116
+ equity = float(portfolio.get_total_equity())
117
+ balance = float(portfolio.get_total_balance())
118
+ realized = float(portfolio.get_realized_pnl())
119
+ unrealized = float(portfolio.get_unrealized_pnl())
120
+ daily = float(portfolio.get_daily_pnl())
121
+ open_positions = len(getattr(portfolio, "positions", {}) or {})
122
+ current_dd = float(portfolio.get_current_drawdown()) * 100.0
123
+ else:
124
+ equity = 10000.0 + float(summary.get("total_pnl", 0.0))
125
+ balance = equity
126
+ realized = float(summary.get("total_pnl", 0.0))
127
+ unrealized = 0.0
128
+ daily = realized
129
+ open_positions = 0
130
+ current_dd = float(max_dd.get("max_drawdown_pct", 0.0))
131
+
132
+ profit_factor = summary.get("profit_factor", 0.0)
133
+ if profit_factor == float("inf"):
134
+ profit_factor = 0.0
135
+
136
+ sortino = summary.get("sortino_ratio", 0.0)
137
+ if sortino == float("inf"):
138
+ sortino = 0.0
139
+
140
+ return LiveMetricsSnapshot(
141
+ timestamp=now,
142
+ equity=equity,
143
+ balance=balance,
144
+ realized_pnl=realized,
145
+ unrealized_pnl=unrealized,
146
+ daily_pnl=daily,
147
+ open_positions=open_positions,
148
+ total_trades=int(summary.get("total_trades", 0)),
149
+ win_rate=float(summary.get("win_rate", 0.0)),
150
+ profit_factor=float(profit_factor),
151
+ sharpe_ratio=float(summary.get("sharpe_ratio", 0.0)),
152
+ sortino_ratio=float(sortino),
153
+ max_drawdown=float(max_dd.get("max_drawdown", 0.0)),
154
+ max_drawdown_pct=float(max_dd.get("max_drawdown_pct", 0.0)),
155
+ current_drawdown=current_dd,
156
+ var_95=float(var_result.get("var", 0.0)),
157
+ cvar_95=float(cvar_result.get("cvar", 0.0)),
158
+ volatility=volatility,
159
+ average_win=float(summary.get("average_win", 0.0)),
160
+ average_loss=float(summary.get("average_loss", 0.0)),
161
+ )
@@ -284,20 +284,29 @@ class PortfolioAnalytics:
284
284
  Calculate portfolio metrics (alias for get_performance_summary for compatibility).
285
285
 
286
286
  Args:
287
- portfolio: Optional portfolio object (currently unused, kept for compatibility)
287
+ portfolio: Optional portfolio object; when provided, trades and equity
288
+ history are loaded for the calculation.
288
289
 
289
290
  Returns:
290
291
  Dictionary of performance metrics
291
292
  """
292
- # If portfolio is provided and has trades, use them
293
- if portfolio and hasattr(portfolio, 'trades') and portfolio.trades:
294
- # Temporarily use portfolio's trades
295
- original_trades = self.trades
296
- self.trades = portfolio.trades
297
- metrics = self.get_performance_summary()
293
+ if portfolio is None:
294
+ return self.get_performance_summary()
295
+
296
+ original_trades = self.trades
297
+ original_equity = self.equity_curve
298
+ try:
299
+ if hasattr(portfolio, "trades"):
300
+ self.trades = list(portfolio.trades)
301
+ timestamps = getattr(portfolio, "timestamps", None)
302
+ equity_history = getattr(portfolio, "equity_history", None)
303
+ if timestamps is not None and equity_history is not None:
304
+ ts = list(timestamps)
305
+ eq = list(equity_history)
306
+ n = min(len(ts), len(eq))
307
+ self.equity_curve = list(zip(ts[:n], eq[:n]))
308
+ return self.get_performance_summary()
309
+ finally:
298
310
  self.trades = original_trades
299
- return metrics
300
-
301
- # Otherwise use internal trades
302
- return self.get_performance_summary()
311
+ self.equity_curve = original_equity
303
312
 
@@ -7,7 +7,6 @@ import numpy as np
7
7
  import pandas as pd
8
8
  from typing import Dict, List, Optional, Tuple
9
9
  from datetime import datetime, timedelta
10
- from scipy import stats
11
10
  from ..core.interfaces import Trade, Position
12
11
 
13
12
 
@@ -245,6 +244,12 @@ class RiskAnalytics:
245
244
  def _calculate_parametric_var(self, returns: np.ndarray,
246
245
  confidence_level: float) -> float:
247
246
  """Calculate VaR using parametric (variance-covariance) method."""
247
+ try:
248
+ from scipy import stats
249
+ except ImportError as exc:
250
+ raise ImportError(
251
+ "scipy is required for parametric VaR; use method='historical'"
252
+ ) from exc
248
253
  mean_return = np.mean(returns)
249
254
  std_return = np.std(returns)
250
255
 
@@ -86,16 +86,32 @@ class AnalyticsDialog(QDialog):
86
86
  def update_portfolio_metrics(self, portfolio=None):
87
87
  """Update portfolio metrics table."""
88
88
  try:
89
- metrics = self.analytics.calculate_metrics(portfolio)
90
-
91
- if not metrics:
89
+ from ..analytics.live_metrics import build_live_metrics
90
+
91
+ snapshot = build_live_metrics(portfolio)
92
+ metrics = {
93
+ "equity": snapshot.equity,
94
+ "balance": snapshot.balance,
95
+ "realized_pnl": snapshot.realized_pnl,
96
+ "unrealized_pnl": snapshot.unrealized_pnl,
97
+ "daily_pnl": snapshot.daily_pnl,
98
+ "total_trades": snapshot.total_trades,
99
+ "win_rate": snapshot.win_rate,
100
+ "profit_factor": snapshot.profit_factor,
101
+ "sharpe_ratio": snapshot.sharpe_ratio,
102
+ "sortino_ratio": snapshot.sortino_ratio,
103
+ "max_drawdown": snapshot.max_drawdown,
104
+ "max_drawdown_pct": snapshot.max_drawdown_pct,
105
+ }
106
+
107
+ if snapshot.total_trades == 0 and snapshot.open_positions == 0:
92
108
  self.metrics_table.setRowCount(1)
93
109
  self.metrics_table.setItem(0, 0, QTableWidgetItem("No data"))
94
110
  self.metrics_table.setItem(0, 1, QTableWidgetItem("No trades available"))
95
111
  return
96
-
112
+
97
113
  self.metrics_table.setRowCount(len(metrics))
98
-
114
+
99
115
  for i, (key, value) in enumerate(metrics.items()):
100
116
  self.metrics_table.setItem(i, 0, QTableWidgetItem(str(key).replace('_', ' ').title()))
101
117
  if isinstance(value, float):
@@ -134,24 +150,22 @@ class AnalyticsDialog(QDialog):
134
150
  self.update_risk_metrics(portfolio)
135
151
 
136
152
  def update_risk_metrics(self, portfolio=None):
137
- """Update risk metrics table."""
153
+ """Update risk metrics table from portfolio trades when available."""
138
154
  try:
139
- # Calculate VaR
140
- var_result = self.risk_analytics.calculate_var(confidence_level=0.95)
141
-
142
- # Calculate CVaR
143
- cvar_result = self.risk_analytics.calculate_cvar(confidence_level=0.95)
144
-
145
- # Combine metrics
155
+ from ..analytics.live_metrics import build_live_metrics
156
+
157
+ snapshot = build_live_metrics(portfolio)
146
158
  metrics = {
147
- 'VaR (95%)': var_result.get('var', 0.0),
148
- 'CVaR (95%)': cvar_result.get('cvar', 0.0),
149
- 'Max Drawdown': var_result.get('max_drawdown', 0.0),
150
- 'Volatility': var_result.get('volatility', 0.0)
159
+ "VaR (95%)": snapshot.var_95,
160
+ "CVaR (95%)": snapshot.cvar_95,
161
+ "Max Drawdown": snapshot.max_drawdown,
162
+ "Max Drawdown %": snapshot.max_drawdown_pct,
163
+ "Current Drawdown %": snapshot.current_drawdown,
164
+ "Volatility": snapshot.volatility,
151
165
  }
152
-
166
+
153
167
  self.risk_table.setRowCount(len(metrics))
154
-
168
+
155
169
  for i, (key, value) in enumerate(metrics.items()):
156
170
  self.risk_table.setItem(i, 0, QTableWidgetItem(key))
157
171
  if isinstance(value, float):
@@ -0,0 +1,202 @@
1
+ """
2
+ Live Analytics Dashboard widget for main window integration.
3
+
4
+ Shows KPI cards plus portfolio/risk tables, refreshed on a timer from the
5
+ live Portfolio.
6
+ """
7
+
8
+ from PyQt6.QtWidgets import (
9
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget,
10
+ QTableWidgetItem, QGroupBox, QGridLayout, QFrame, QSizePolicy,
11
+ )
12
+ from PyQt6.QtCore import Qt, QTimer
13
+ from typing import Optional
14
+
15
+
16
+ _CARD_STYLE = """
17
+ QFrame#metricCard {{
18
+ background-color: palette(base);
19
+ border: 1px solid palette(mid);
20
+ border-radius: 6px;
21
+ padding: 8px;
22
+ }}
23
+ QLabel#cardValue {{
24
+ font-size: 18px;
25
+ font-weight: bold;
26
+ }}
27
+ QLabel#cardLabel {{
28
+ font-size: 11px;
29
+ color: palette(mid);
30
+ }}
31
+ """
32
+
33
+ _TONE_COLORS = {
34
+ "positive": "#2e7d32",
35
+ "negative": "#c62828",
36
+ "neutral": "",
37
+ }
38
+
39
+
40
+ class PortfolioAnalyticsWidget(QWidget):
41
+ """Live analytics dashboard: KPI cards + portfolio/risk metric tables."""
42
+
43
+ def __init__(self, portfolio, parent=None, refresh_ms: int = 5000):
44
+ super().__init__(parent)
45
+ self.portfolio = portfolio
46
+ self.refresh_ms = refresh_ms
47
+ self._card_value_labels = []
48
+ self.setup_ui()
49
+ self.setup_timer()
50
+ self.update_metrics()
51
+
52
+ def setup_ui(self):
53
+ """Setup UI."""
54
+ layout = QVBoxLayout(self)
55
+ layout.setContentsMargins(8, 8, 8, 8)
56
+ layout.setSpacing(10)
57
+
58
+ header = QHBoxLayout()
59
+ title = QLabel("Live Analytics")
60
+ title.setStyleSheet("font-size: 16px; font-weight: bold;")
61
+ header.addWidget(title)
62
+ header.addStretch()
63
+ self.updated_label = QLabel("Updated: —")
64
+ self.updated_label.setStyleSheet("font-size: 11px; color: palette(mid);")
65
+ header.addWidget(self.updated_label)
66
+ layout.addLayout(header)
67
+
68
+ self.cards_frame = QFrame()
69
+ self.cards_frame.setObjectName("metricCards")
70
+ cards_layout = QHBoxLayout(self.cards_frame)
71
+ cards_layout.setContentsMargins(0, 0, 0, 0)
72
+ cards_layout.setSpacing(8)
73
+ self._card_value_labels = []
74
+ for _ in range(6):
75
+ card, value_label = self._make_card("—", "—")
76
+ cards_layout.addWidget(card)
77
+ self._card_value_labels.append(value_label)
78
+ layout.addWidget(self.cards_frame)
79
+
80
+ tables = QHBoxLayout()
81
+ tables.setSpacing(10)
82
+
83
+ portfolio_group = QGroupBox("Portfolio")
84
+ portfolio_layout = QVBoxLayout(portfolio_group)
85
+ self.metrics_table = QTableWidget()
86
+ self.metrics_table.setColumnCount(2)
87
+ self.metrics_table.setHorizontalHeaderLabels(["Metric", "Value"])
88
+ self.metrics_table.horizontalHeader().setStretchLastSection(True)
89
+ self.metrics_table.setAlternatingRowColors(True)
90
+ self.metrics_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
91
+ self.metrics_table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
92
+ self.metrics_table.verticalHeader().setVisible(False)
93
+ portfolio_layout.addWidget(self.metrics_table)
94
+ tables.addWidget(portfolio_group)
95
+
96
+ risk_group = QGroupBox("Risk")
97
+ risk_layout = QVBoxLayout(risk_group)
98
+ self.risk_table = QTableWidget()
99
+ self.risk_table.setColumnCount(2)
100
+ self.risk_table.setHorizontalHeaderLabels(["Metric", "Value"])
101
+ self.risk_table.horizontalHeader().setStretchLastSection(True)
102
+ self.risk_table.setAlternatingRowColors(True)
103
+ self.risk_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
104
+ self.risk_table.setSelectionMode(QTableWidget.SelectionMode.NoSelection)
105
+ self.risk_table.verticalHeader().setVisible(False)
106
+ risk_layout.addWidget(self.risk_table)
107
+ tables.addWidget(risk_group)
108
+
109
+ layout.addLayout(tables)
110
+ self.setStyleSheet(_CARD_STYLE)
111
+
112
+ def _make_card(self, label: str, value: str):
113
+ """Create a KPI card frame; returns (frame, value_label)."""
114
+ frame = QFrame()
115
+ frame.setObjectName("metricCard")
116
+ frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
117
+ grid = QGridLayout(frame)
118
+ grid.setContentsMargins(10, 8, 10, 8)
119
+ value_label = QLabel(value)
120
+ value_label.setObjectName("cardValue")
121
+ value_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
122
+ name_label = QLabel(label)
123
+ name_label.setObjectName("cardLabel")
124
+ name_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
125
+ grid.addWidget(value_label, 0, 0)
126
+ grid.addWidget(name_label, 1, 0)
127
+ frame._name_label = name_label # noqa: SLF001 — local card wiring
128
+ return frame, value_label
129
+
130
+ def setup_timer(self):
131
+ """Setup live refresh timer."""
132
+ self.update_timer = QTimer(self)
133
+ self.update_timer.timeout.connect(self.update_metrics)
134
+ self.update_timer.start(self.refresh_ms)
135
+
136
+ def set_portfolio(self, portfolio) -> None:
137
+ """Replace the portfolio source and refresh immediately."""
138
+ self.portfolio = portfolio
139
+ self.update_metrics()
140
+
141
+ def update_metrics(self):
142
+ """Refresh KPI cards and portfolio/risk tables from live portfolio."""
143
+ try:
144
+ from ..analytics.live_metrics import build_live_metrics
145
+
146
+ snapshot = build_live_metrics(self.portfolio)
147
+ self.updated_label.setText(
148
+ f"Updated: {snapshot.timestamp.strftime('%H:%M:%S')}"
149
+ )
150
+
151
+ for i, (label, value, tone) in enumerate(snapshot.card_metrics()):
152
+ if i >= len(self._card_value_labels):
153
+ break
154
+ value_label = self._card_value_labels[i]
155
+ value_label.setText(value)
156
+ color = _TONE_COLORS.get(tone, "")
157
+ if color:
158
+ value_label.setStyleSheet(f"font-size: 18px; font-weight: bold; color: {color};")
159
+ else:
160
+ value_label.setStyleSheet("font-size: 18px; font-weight: bold;")
161
+ card = value_label.parent()
162
+ if card is not None and hasattr(card, "_name_label"):
163
+ card._name_label.setText(label)
164
+
165
+ portfolio_rows = {
166
+ "Equity": f"${snapshot.equity:,.2f}",
167
+ "Balance": f"${snapshot.balance:,.2f}",
168
+ "Realized P&L": f"${snapshot.realized_pnl:,.2f}",
169
+ "Unrealized P&L": f"${snapshot.unrealized_pnl:,.2f}",
170
+ "Daily P&L": f"${snapshot.daily_pnl:,.2f}",
171
+ "Open Positions": str(snapshot.open_positions),
172
+ "Total Trades": str(snapshot.total_trades),
173
+ "Win Rate": f"{snapshot.win_rate:.1f}%",
174
+ "Profit Factor": f"{snapshot.profit_factor:.3f}",
175
+ "Sharpe Ratio": f"{snapshot.sharpe_ratio:.4f}",
176
+ "Sortino Ratio": f"{snapshot.sortino_ratio:.4f}",
177
+ "Avg Win": f"${snapshot.average_win:,.2f}",
178
+ "Avg Loss": f"${snapshot.average_loss:,.2f}",
179
+ }
180
+ self._fill_table(self.metrics_table, portfolio_rows)
181
+
182
+ risk_rows = {
183
+ "Max Drawdown": f"${snapshot.max_drawdown:,.2f}",
184
+ "Max Drawdown %": f"{snapshot.max_drawdown_pct:.2f}%",
185
+ "Current Drawdown %": f"{snapshot.current_drawdown:.2f}%",
186
+ "VaR (95%)": f"{snapshot.var_95:.4f}",
187
+ "CVaR (95%)": f"{snapshot.cvar_95:.4f}",
188
+ "Volatility": f"{snapshot.volatility:.4f}",
189
+ }
190
+ self._fill_table(self.risk_table, risk_rows)
191
+ except Exception as e:
192
+ self.metrics_table.setRowCount(1)
193
+ self.metrics_table.setItem(0, 0, QTableWidgetItem("Error"))
194
+ self.metrics_table.setItem(0, 1, QTableWidgetItem(str(e)))
195
+ self.risk_table.setRowCount(0)
196
+
197
+ @staticmethod
198
+ def _fill_table(table: QTableWidget, rows: dict) -> None:
199
+ table.setRowCount(len(rows))
200
+ for i, (key, value) in enumerate(rows.items()):
201
+ table.setItem(i, 0, QTableWidgetItem(key))
202
+ table.setItem(i, 1, QTableWidgetItem(value))
@@ -2042,6 +2042,13 @@ class EnhancedMainWindow(QMainWindow):
2042
2042
  # Update positions display (fallback if MT4 sync didn't work)
2043
2043
  if not (self.broker and self.broker.is_connected()):
2044
2044
  self.update_positions_display()
2045
+
2046
+ # Refresh live analytics dashboard when present
2047
+ if hasattr(self, "portfolio_analytics_widget") and self.portfolio_analytics_widget:
2048
+ try:
2049
+ self.portfolio_analytics_widget.set_portfolio(self.portfolio)
2050
+ except Exception:
2051
+ pass
2045
2052
 
2046
2053
  except Exception as e:
2047
2054
  self.append_log(f"Real-time update error: {str(e)}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forexsmartbot
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Professional forex trading bot with advanced risk management and strategy plugins
5
5
  Author-email: VoxHash <contact@voxhash.dev>
6
6
  License: MIT
@@ -30,6 +30,7 @@ forexsmartbot/analytics/chart_patterns.py
30
30
  forexsmartbot/analytics/correlation_matrix.py
31
31
  forexsmartbot/analytics/economic_calendar.py
32
32
  forexsmartbot/analytics/enhanced_chart.py
33
+ forexsmartbot/analytics/live_metrics.py
33
34
  forexsmartbot/analytics/market_depth.py
34
35
  forexsmartbot/analytics/performance_attribution.py
35
36
  forexsmartbot/analytics/portfolio_analytics.py
@@ -132,6 +133,7 @@ forexsmartbot/utils/gpu_numpy.py
132
133
  forexsmartbot/utils/gpu_utils.py
133
134
  forexsmartbot/utils/native_indicators.py
134
135
  tests/test_config.py
136
+ tests/test_live_metrics.py
135
137
  tests/test_multi_timeframe_strategy.py
136
138
  tests/test_native_indicators.py
137
139
  tests/test_risk.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "forexsmartbot"
7
- version = "3.2.1"
7
+ version = "3.2.2"
8
8
  description = "Professional forex trading bot with advanced risk management and strategy plugins"
9
9
  authors = [{name = "VoxHash", email = "contact@voxhash.dev"}]
10
10
  license = {text = "MIT"}
@@ -0,0 +1,70 @@
1
+ """Tests for live analytics metrics snapshot."""
2
+
3
+ from datetime import datetime, timedelta
4
+
5
+ from forexsmartbot.analytics.live_metrics import LiveMetricsSnapshot, build_live_metrics
6
+ from forexsmartbot.analytics.portfolio_analytics import PortfolioAnalytics
7
+ from forexsmartbot.core.interfaces import Trade
8
+ from forexsmartbot.core.portfolio import Portfolio
9
+
10
+
11
+ def _make_trade(pnl: float, day_offset: int = 0, qty: float = 1.0, entry: float = 1.10) -> Trade:
12
+ entry_time = datetime.now() - timedelta(days=2 + day_offset)
13
+ exit_time = datetime.now() - timedelta(days=day_offset)
14
+ return Trade(
15
+ symbol="EURUSD",
16
+ side=1,
17
+ quantity=qty,
18
+ entry_price=entry,
19
+ exit_price=entry + (pnl / qty if qty else 0),
20
+ pnl=pnl,
21
+ strategy="SMA_Crossover",
22
+ entry_time=entry_time,
23
+ exit_time=exit_time,
24
+ )
25
+
26
+
27
+ class TestLiveMetrics:
28
+ def test_empty_portfolio_snapshot(self):
29
+ portfolio = Portfolio(initial_balance=10000.0)
30
+ snap = build_live_metrics(portfolio)
31
+ assert isinstance(snap, LiveMetricsSnapshot)
32
+ assert snap.equity == 10000.0
33
+ assert snap.total_trades == 0
34
+ assert snap.var_95 == 0.0
35
+ assert len(snap.card_metrics()) == 6
36
+
37
+ def test_snapshot_with_winning_and_losing_trades(self):
38
+ portfolio = Portfolio(initial_balance=10000.0)
39
+ portfolio.add_trade(_make_trade(50.0, day_offset=1))
40
+ portfolio.add_trade(_make_trade(-20.0, day_offset=0))
41
+ portfolio.update_equity(10030.0)
42
+
43
+ snap = build_live_metrics(portfolio)
44
+ assert snap.total_trades == 2
45
+ assert snap.realized_pnl == 30.0
46
+ assert snap.win_rate == 50.0
47
+ assert snap.average_win == 50.0
48
+ assert snap.average_loss == 20.0
49
+ assert snap.equity == portfolio.get_total_equity()
50
+ data = snap.to_dict()
51
+ assert "timestamp" in data
52
+ assert data["total_trades"] == 2
53
+
54
+ def test_portfolio_analytics_loads_equity_from_portfolio(self):
55
+ portfolio = Portfolio(initial_balance=10000.0)
56
+ portfolio.add_trade(_make_trade(100.0))
57
+ portfolio.update_equity(10100.0)
58
+ analytics = PortfolioAnalytics()
59
+ metrics = analytics.calculate_metrics(portfolio)
60
+ assert metrics["total_trades"] == 1
61
+ assert metrics["total_pnl"] == 100.0
62
+ # Internal state restored after calculate_metrics
63
+ assert analytics.trades == []
64
+
65
+ def test_card_metrics_tone_for_negative_daily_pnl(self):
66
+ portfolio = Portfolio(initial_balance=10000.0)
67
+ portfolio.add_trade(_make_trade(-40.0, day_offset=0))
68
+ snap = build_live_metrics(portfolio)
69
+ cards = dict((label, (value, tone)) for label, value, tone in snap.card_metrics())
70
+ assert cards["Daily P&L"][1] == "negative"