POST-MORTEM ANALYSIS: Refactoring BacktestTabWidget (Incidente #001)
1. L’Incidente
Dopo aver estratto la logica di gestione della strategia in StrategyConfigHandler, l’applicazione è andata in crash all’avvio con un RecursionError durante il caricamento di dsl_oscillator_strategy.py. Il crash è stato così grave da mandare in loop anche il sistema di logging.
2. La Causa Tecnica (Root Cause)
L’errore RecursionError durante exec_module di una strategia (che eredita da BaseStrategy) indica quasi certamente una Dipendenza Circolare (Circular Import) introdotta dal refactoring.
-
Prima: BacktestTabWidget importava solo load_strategy_from_file. Non importava esplicitamente la classe BaseStrategy.
-
Dopo: Il nuovo StrategyConfigHandler ha importato esplicitamente:
codePython
from fire.strategies.base_strategy import BaseStrategy # <--- IL COLPEVOLEper usarla come type hint (self.current_strategy: Optional[BaseStrategy]).
La Catena Mortale:
-
main_window importa BacktestTabWidget.
-
BacktestTabWidget importa StrategyConfigHandler.
-
StrategyConfigHandler importa BaseStrategy.
-
BaseStrategy (o moduli correlati nel core) ha molto probabilmente un import (anche indiretto o condizionale) che risale verso componenti UI o AppState per definire tipi o costanti.
-
Quando il strategy_loader tenta di eseguire dinamicamente il file della strategia (class DslOscillatorStrategy(BaseStrategy)), Python sta ancora cercando di risolvere BaseStrategy che è “bloccata” nel ciclo di importazione iniziato dallo Handler.
3. Cosa abbiamo sbagliato (Errori di Processo)
-
Analisi delle Dipendenze Superficiale: Abbiamo spostato codice (Logic Extraction) senza verificare se l’aggiunta di nuovi import (come BaseStrategy) in un modulo di livello “Handler” (Layer 2) avrebbe creato cicli con il Layer 3 (Core/Strategies).
-
Sottovalutazione del “Fat File”: Il file originale era “FAT” per un motivo: teneva le dipendenze locali. Separandole, abbiamo dovuto renderle esplicite e globali, esponendo la fragilità dell’albero delle dipendenze di BaseStrategy.
4. Azioni Correttive Future (Preventive)
-
Mai importare BaseStrategy in moduli UI/Handler: Se serve per il type hinting, usare rigorosamente from typing import TYPE_CHECKING per evitare import a runtime.
-
POC First: Un refactoring che tocca il caricamento dinamico delle classi core doveva essere testato isolatamente prima di essere integrato nel widget principale.
Assolutamente sì. Anzi, dobbiamo farlo. Questo secondo incidente è diverso dal primo: nel primo avevamo sbagliato noi gli import (errore umano); in questo abbiamo applicato tutte le best practice (Type Checking, Local Import, Lazy Loading) e abbiamo fallito lo stesso a causa di un vincolo strutturale di Python.
È una lezione preziosissima: A volte il codice “brutto” (tutto in un file) funziona perché garantisce un ordine di esecuzione lineare che lo “spacchettamento” rompe.
Ecco il testo da aggiungere al tuo documento di POST-MORTEM ANALYSIS.
Aggiornamento Incidente #002: Il Fallimento della “Component Extraction” (BacktestTabWidget)
1. Il Tentativo
Abbiamo tentato di snellire il BacktestTabWidget (Fat Widget) estraendo la logica in due nuovi file:
BacktestTabUI(UI Pura)BacktestStrategyHandler(Logica di Configurazione)
2. La Strategia Difensiva (Che ha fallito) Memori dell’Incidente #001, abbiamo applicato misure estreme:
- Usato solo
TYPE_CHECKINGper le dipendenze Core. - Spostato tutti gli import critici (
load_strategy_from_file,StrategySettingsDialog) all’interno dei metodi (Local Scope). - “Sterilizzato” l’Handler rimuovendo ogni dipendenza statica da
fire.*. - Usato Lazy Import nel
__init__del widget principale.
3. Il Risultato
Nonostante queste precauzioni, il RecursionError in importlib è persistito.
Il sistema non riusciva a caricare le strategie dinamiche (DslOscillatorStrategy), bloccandosi in un loop infinito durante la definizione della classe.
4. La Root Cause (Analisi Profonda)
Il problema non era cosa importavamo, ma l’esistenza stessa dei nuovi moduli nel grafo.
Il grafo delle dipendenze di FIRE ha un ciclo macroscopico:
Core (BaseStrategy) ⇐> UI (BacktestTabWidget).
Finché tutto era in un file, Python gestiva il modulo backtest_tab_widget come un blocco unico. Spezzandolo in 3 file (widget, ui, handler), abbiamo cambiato l’ordine di inizializzazione di sys.modules.
Quando strategy_loader (Core) prova a caricare una strategia, questa eredita da BaseStrategy. Se BaseStrategy ha una dipendenza indiretta che tocca BacktestTabWidget, e questo a sua volta sta ancora cercando di inizializzare i suoi sotto-moduli (handler), si crea un deadlock nel loader di Python.
5. Lezione Appresa (Architettura) In un’architettura Python fortemente accoppiata (dove Core e UI si parlano bidirezionalmente), il Refactoring per Estrazione (Component Extraction) è pericoloso e spesso impossibile senza un Event Bus.
Se non si può disaccoppiare totalmente il Core dalla UI (tramite Eventi), è più sicuro mantenere un “Fat File” funzionante piuttosto che creare una rete di “Small Files” che si importano a vicenda creando cicli invisibili.
6. Nuova Regola Operativa
“Non estrarre Logic Handlers da un Widget se quel Widget è referenziato (anche indirettamente) dal Core che l’Handler deve caricare.” In questi casi, la soluzione non è dividere i file, ma implementare un Event Bus per rompere la dipendenza alla radice.