# VERSION: v1.11 - fire/ui_components/forecasting/forecast_backtest_widget.py (pre refactoring)
# RESP: Fornisce l'interfaccia utente per configurare, avviare e monitorare un'operazione di backtest di un modello di forecasting.
# DEPS: AppState, DataManager, ThemeManager
# ARCH_ROLE: feature
# LAYER: fire-app
from typing import Optional, Dict, Any
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QPushButton,
QSpinBox, QComboBox, QGroupBox, QProgressBar,
QTextBrowser, QHBoxLayout, QFormLayout
)
from PySide6.QtCore import Qt, Signal, Slot
from fire.app_state import AppState
from fire.core.data.data_manager import DataManager
from fire.ui_components.theme_manager import ThemeManager
class ForecastBacktestWidget(QWidget):
"""
Widget UI per configurare, eseguire e monitorare un backtest
di un modello di forecasting. Delega la visualizzazione dei risultati.
"""
backtest_requested = Signal(dict)
results_ready = Signal(dict)
def __init__(self, app_state: AppState, data_manager: DataManager, parent: Optional[QWidget] = None):
super().__init__(parent)
self.app_state = app_state
self.data_manager = data_manager
self._setup_ui()
self._apply_theme()
self._connect_signals()
self.progress_group.setVisible(False)
self.set_controls_enabled(True)
def _setup_ui(self):
main_layout = QVBoxLayout(self)
config_group = QGroupBox("Configuration")
form_layout = QFormLayout(config_group)
self.model_combo = QComboBox()
self.model_combo.addItem("TimesFM (Precision Focus)", "timesfm")
self.model_combo.addItem("Chronos-2 (Universal Model)", "chronos-2")
self.model_combo.addItem("Prophet (Trend & Direction)", "prophet")
self.model_combo.setToolTip("Seleziona il modello di forecasting da usare per il backtest.")
self.initial_window_spinbox = QSpinBox()
self.initial_window_spinbox.setRange(30, 2000)
self.initial_window_spinbox.setValue(252)
self.initial_window_spinbox.setSingleStep(10)
self.horizon_spinbox = QSpinBox()
self.horizon_spinbox.setRange(1, 100)
self.horizon_spinbox.setValue(10)
self.horizon_spinbox.setToolTip("Il numero di giorni futuri da prevedere in ogni step.")
self.run_backtest_button = QPushButton("Run Backtest")
form_layout.addRow("Model:", self.model_combo)
form_layout.addRow("Initial Training Window (days):", self.initial_window_spinbox)
form_layout.addRow("Forecast Horizon (days):", self.horizon_spinbox)
self.progress_group = QGroupBox("Backtest in Progress...")
progress_layout = QVBoxLayout(self.progress_group)
self.progress_bar = QProgressBar()
self.progress_log = QTextBrowser(readOnly=True)
self.progress_log.setMaximumHeight(150)
self.cancel_button = QPushButton("Cancel")
progress_layout.addWidget(self.progress_bar)
progress_layout.addWidget(self.progress_log)
progress_layout.addWidget(self.cancel_button, 0, Qt.AlignmentFlag.AlignRight)
main_layout.addWidget(config_group)
main_layout.addWidget(self.run_backtest_button)
main_layout.addWidget(self.progress_group)
main_layout.addStretch(1)
def _apply_theme(self):
"""Applica lo stile standard centralizzato al pulsante di azione."""
self.run_backtest_button.setStyleSheet(ThemeManager.get_action_button_style())
def _connect_signals(self):
self.run_backtest_button.clicked.connect(self._on_run_backtest_clicked)
def set_controls_enabled(self, enabled: bool):
self.model_combo.setEnabled(enabled)
self.initial_window_spinbox.setEnabled(enabled)
self.horizon_spinbox.setEnabled(enabled)
self.run_backtest_button.setEnabled(enabled)
@Slot()
def _on_run_backtest_clicked(self):
ticker = self.app_state.current_ticker
if not ticker:
self.app_state.log("Seleziona un ticker dalla watchlist prima di eseguire un backtest.", "WARNING")
return
params = {
"model_name": self.model_combo.currentData(),
"ticker": ticker,
"start_date": self.app_state.global_start_date.toString("yyyy-MM-dd"),
"end_date": self.app_state.global_end_date.toString("yyyy-MM-dd"),
"initial_window": self.initial_window_spinbox.value(),
"horizon": self.horizon_spinbox.value()
}
self.progress_group.setVisible(True)
self.set_controls_enabled(False)
self.progress_log.clear()
self.backtest_requested.emit(params)
@Slot(int, int)
def on_progress_update(self, current_step: int, total_steps: int):
self.progress_bar.setRange(0, total_steps)
self.progress_bar.setValue(current_step)
self.progress_bar.setFormat(f"Step {current_step} / {total_steps}")
@Slot(str)
def on_log_message(self, message: str):
self.progress_log.append(message)
@Slot(dict)
def on_backtest_finished(self, results: Dict[str, Any]):
self.progress_group.setVisible(False)
self.set_controls_enabled(True)
self.app_state.log("Backtest di forecasting completato. Risultati pronti.", "SUCCESS")
self.results_ready.emit(results)
@Slot()
def on_backtest_error(self, error_msg: str):
self.progress_group.setVisible(False)
self.set_controls_enabled(True)
self.app_state.log(f"Backtest di forecasting fallito: {error_msg}", "ERROR")