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.
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/PKG-INFO +1 -1
- forexsmartbot-3.2.2/forexsmartbot/analytics/__init__.py +43 -0
- forexsmartbot-3.2.2/forexsmartbot/analytics/live_metrics.py +161 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/portfolio_analytics.py +20 -11
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/risk_analytics.py +6 -1
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/analytics_dialog.py +33 -19
- forexsmartbot-3.2.2/forexsmartbot/ui/analytics_widget.py +202 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/enhanced_main_window.py +7 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/PKG-INFO +1 -1
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/SOURCES.txt +2 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/pyproject.toml +1 -1
- forexsmartbot-3.2.2/tests/test_live_metrics.py +70 -0
- forexsmartbot-3.2.1/forexsmartbot/analytics/__init__.py +0 -28
- forexsmartbot-3.2.1/forexsmartbot/ui/analytics_widget.py +0 -68
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/LICENSE +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/README.md +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/ib_tws_broker.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/mt4_broker.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/paper_broker.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/brokers/rest_broker.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/alpha_vantage_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/config.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/csv_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/dummy_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/mt4_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/multi_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/oanda_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/stooq_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/twelve_data_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/adapters/data/yfinance_provider.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/chart_patterns.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/correlation_matrix.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/economic_calendar.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/enhanced_chart.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/market_depth.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/performance_attribution.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/strategy_comparison.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/analytics/trade_journal.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/brokers/mt4_bridge.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/code_generator.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/strategy_builder.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/builder/strategy_template.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/api_client.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/api_server.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/cloud_sync.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/error_handler.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/mobile_api.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/remote_monitor.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/cloud/security.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/backtester.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/config.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/interfaces.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/paper_broker.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/portfolio.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/risk.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/risk_engine.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/core/trade_manager.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/gui/main_window.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/gui/theme.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/marketplace/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/marketplace/marketplace.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/marketplace/strategy_rating.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/health_check.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/performance_tracker.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/monitoring/strategy_monitor.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/adaptive_parameters.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/genetic_optimizer.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/hyperparameter_optimizer.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/monte_carlo.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/multi_objective_optimizer.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/parameter_sensitivity.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/optimization/walk_forward.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/backtest.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/calculation_engine.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/controller.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/data_streaming.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/enhanced_backtest.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/fast_trading_controller.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/gpu_backtest.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/language_manager.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/notification_service.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/persistence.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/services/startup_manager.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/adaptive_trend_flow.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/breakout_atr.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/ensemble_ml_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/fear_index.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/lstm_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/mean_reversion.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/ml_adaptive_supertrend.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/momentum_breakout.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/multi_timeframe_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/news_trading.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/rl_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/rsi_reversion.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/scalping_ma.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/sma_crossover.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/svm_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/strategies/transformer_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/ci_cd_pipeline.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/paper_account_tester.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/testing/strategy_sandbox.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/backtest_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/charts.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/cloud_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/enhanced_charts.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/export_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/main_window.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/marketplace_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/monitoring_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/monitoring_widget.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/optimization_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/settings_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/simple_chart.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/strategy_builder_dialog.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/theme.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/ui/tradingview_chart.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/__init__.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/gpu_numpy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/gpu_utils.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot/utils/native_indicators.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/dependency_links.txt +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/entry_points.txt +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/requires.txt +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/forexsmartbot.egg-info/top_level.txt +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/setup.cfg +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_config.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_multi_timeframe_strategy.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_native_indicators.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_risk.py +0 -0
- {forexsmartbot-3.2.1 → forexsmartbot-3.2.2}/tests/test_strategies.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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)}")
|
|
@@ -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.
|
|
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"
|