11import streamlit as st
22import pandas as pd
3+ import plotly .express as px
4+ import plotly .graph_objects as go
35from pathlib import Path
46import json
5- from services .backtest import run_backtest # Importiamo la funzione backend
7+ import subprocess
8+ from src .strategies import STRATEGY_MAP
9+ from src .settings_manager import SettingsManager
610
711st .set_page_config (page_title = "Backtest Lab" , page_icon = "🧪" , layout = "wide" )
8-
912st .title ("🧪 Backtest Laboratory" )
1013
11- # Sidebar: Configurazione
14+ # --- HELPER FUNCTIONS ---
15+ def load_benchmark_data (session_path : Path ):
16+ """
17+ Scansiona una cartella di sessione (TIMESTAMP) e carica i dati di tutte le strategie trovate.
18+ Ritorna:
19+ - strategies_data: dict { 'StrategyName': {'equity': df, 'config': dict} }
20+ - comparison_df: DataFrame riassuntivo per la tabella
21+ """
22+ strategies_data = {}
23+ summary_list = []
24+
25+ # Cerca tutte le sottocartelle (ogni sottocartella è una strategia)
26+ subdirs = [x for x in session_path .iterdir () if x .is_dir ()]
27+
28+ for strat_dir in subdirs :
29+ strat_name = strat_dir .name
30+
31+ # Carica Equity Curve
32+ eq_path = strat_dir / "equity_curve.csv"
33+ conf_path = strat_dir / "config.json"
34+
35+ if eq_path .exists () and conf_path .exists ():
36+ df_eq = pd .read_csv (eq_path )
37+ df_eq ['date' ] = pd .to_datetime (df_eq ['date' ])
38+
39+ with open (conf_path ) as f :
40+ conf = json .load (f )
41+
42+ strategies_data [strat_name ] = {
43+ "equity" : df_eq ,
44+ "config" : conf ,
45+ "path" : strat_dir
46+ }
47+
48+ # Dati per la tabella riassuntiva
49+ initial = conf .get ('initial_capital' , 0 )
50+ final = conf .get ('final_equity' , 0 )
51+ roi = ((final - initial ) / initial ) * 100 if initial > 0 else 0
52+
53+ summary_list .append ({
54+ "Strategy" : strat_name ,
55+ "Final Equity" : final ,
56+ "ROI %" : roi ,
57+ "Trades" : conf .get ('total_trades' , 0 ),
58+ "Params" : str (conf .get ('params' , {}))
59+ })
60+
61+ return strategies_data , pd .DataFrame (summary_list )
62+
63+ def plot_comparison (strategies_data ):
64+ """Genera un grafico Plotly unificato."""
65+ fig = go .Figure ()
66+
67+ for name , data in strategies_data .items ():
68+ df = data ['equity' ]
69+ if not df .empty :
70+ fig .add_trace (go .Scatter (
71+ x = df ['date' ],
72+ y = df ['equity' ],
73+ mode = 'lines' ,
74+ name = name
75+ ))
76+
77+ fig .update_layout (
78+ title = "Equity Curve Comparison" ,
79+ xaxis_title = "Date" ,
80+ yaxis_title = "Capital (€)" ,
81+ hovermode = "x unified" ,
82+ legend = dict (orientation = "h" , y = 1.02 , yanchor = "bottom" , x = 1 , xanchor = "right" )
83+ )
84+ return fig
85+
86+ # --- SIDEBAR CONFIGURATION ---
1287with st .sidebar :
1388 st .header ("⚙️ Configuration" )
1489
15- strategy = st .selectbox ("Strategy" , ["RSI" , "MACD (Coming Soon)" ])
16-
17- st .subheader ("Parameters" )
18- initial_cap = st .number_input ("Initial Capital (€)" , value = 10000 , step = 1000 )
19- years = st .slider ("Years History" , 1 , 5 , 2 )
90+ mode = st .radio ("Run Mode" , ["Single Strategy" , "Benchmark (Run All)" ])
2091
21- # Parametri Dinamici per RSI
92+ # Carica parametri salvati per pre-compilare
93+ try :
94+ manager = SettingsManager ()
95+ saved_config = manager .load_config ()
96+ except :
97+ saved_config = {}
98+
99+ selected_strat = None
22100 params = {}
23- if strategy == "RSI" :
24- params ['rsi_period' ] = st .slider ("RSI Period" , 5 , 30 , 14 )
25- params ['rsi_lower' ] = st .slider ("Oversold (<)" , 10 , 45 , 30 )
26- params ['rsi_upper' ] = st .slider ("Overbought (>)" , 55 , 90 , 70 )
27- params ['atr_period' ] = st .slider ("ATR Period" , 5 , 30 , 14 )
28101
29- run_btn = st .button ("🚀 RUN BACKTEST" , type = "primary" )
102+ if mode == "Single Strategy" :
103+ selected_strat = st .selectbox ("Select Strategy" , list (STRATEGY_MAP .keys ()))
104+
105+ # Parametri override (UI semplificata per RSI/EMA)
106+ default_params = saved_config .get ("strategies_params" , {}).get (selected_strat , {})
107+ st .subheader ("Parameters Override" )
108+
109+ if selected_strat == "RSI" :
110+ params ['rsi_period' ] = st .number_input ("RSI Period" , value = default_params .get ('rsi_period' , 14 ))
111+ params ['rsi_lower' ] = st .number_input ("Lower" , value = default_params .get ('rsi_lower' , 30 ))
112+ params ['rsi_upper' ] = st .number_input ("Upper" , value = default_params .get ('rsi_upper' , 70 ))
113+ params ['atr_period' ] = st .number_input ("ATR" , value = default_params .get ('atr_period' , 14 ))
114+ elif selected_strat == "EMA" :
115+ params ['short_window' ] = st .number_input ("Fast EMA" , value = default_params .get ('short_window' , 50 ))
116+ params ['long_window' ] = st .number_input ("Slow EMA" , value = default_params .get ('long_window' , 200 ))
117+ params ['atr_period' ] = st .number_input ("ATR" , value = default_params .get ('atr_period' , 14 ))
118+
119+ st .markdown ("---" )
120+ initial_cap = st .number_input ("Initial Capital (€)" , value = 10000 , step = 1000 )
121+ years = st .slider ("History (Years)" , 1 , 5 , 2 )
122+
123+ run_btn = st .button ("🚀 RUN SIMULATION" , type = "primary" )
30124
31- # Main Area
32- tab_run , tab_history = st .tabs (["Current Run" , "History Archive " ])
125+ # --- MAIN AREA ---
126+ tab_run , tab_results = st .tabs (["🚀 Run Simulation " , "📊 Results Analysis " ])
33127
34128with tab_run :
35129 if run_btn :
36- with st .spinner ("Running simulation... (This may take a moment)" ):
37- try :
38- # Eseguiamo il backtest
39- result_path = run_backtest (
40- strategy_name = strategy ,
41- initial_capital = initial_cap ,
42- days_history = years * 365 ,
43- strategy_params = params
44- )
45- st .success (f"Simulation completed! Saved in: { result_path } " )
46-
47- # Visualizzazione Rapida Risultati
48- p = Path (result_path )
49-
50- # Immagine
51- if (p / "chart.png" ).exists ():
52- # CORRETTO: use_container_width invece di width
53- st .image (str (p / "chart.png" ), use_container_width = True )
130+ st .info ("Simulation started... please wait." )
131+ progress_bar = st .progress (0 )
132+
133+ try :
134+ # Costruzione comando per subprocess
135+ # Usiamo subprocess per garantire che il backtest giri in un processo pulito
136+ # e per supportare facilmente la modalità 'ALL' gestita dal main() del backend.
137+ cmd = ["python" , "-m" , "services.backtest" ]
138+
139+ if mode == "Benchmark (Run All)" :
140+ cmd .append ("ALL" )
141+ else :
142+ cmd .append (selected_strat )
143+ # Nota: Per passare i parametri override al subprocess servirebbe un meccanismo CLI più complesso.
144+ # Per ora, in modalità Single, questo userà i parametri del JSON salvato se non modifichiamo services/backtest.py
145+ # PER ORA: Accettiamo che il 'Single' da UI legga dal JSON o implementiamo un fix rapido.
146+ # FIX RAPIDO: Per semplicità, in questa versione 'Single' usa i parametri salvati nel JSON.
147+ # Se vuoi l'override live, dovremmo passare i parametri come stringa JSON al comando CLI.
148+
149+ # Esecuzione
150+ process = subprocess .run (cmd , capture_output = True , text = True )
151+ progress_bar .progress (100 )
152+
153+ if process .returncode == 0 :
154+ st .success ("Simulation completed successfully!" )
54155
55- # Metriche da JSON
56- if (p / "config.json" ).exists ():
57- with open (p / "config.json" ) as f :
58- res = json .load (f )
156+ # Parsing dell'output per trovare dove ha salvato i dati
157+ # Cerchiamo la riga "Cartella Sessione: ..." nei log
158+ for line in process .stdout .splitlines () + process .stderr .splitlines ():
159+ if "Cartella Sessione:" in line :
160+ session_path_str = line .split ("Cartella Sessione:" )[- 1 ].strip ()
161+ st .session_state ['last_run_path' ] = session_path_str
162+ st .experimental_rerun () # Ricarica per mostrare i risultati nel tab Results
163+ else :
164+ st .error ("Error during execution." )
165+ with st .expander ("Show Error Logs" ):
166+ st .code (process .stderr )
167+ st .code (process .stdout )
59168
60- m1 , m2 , m3 = st .columns (3 )
61- m1 .metric ("Final Equity" , f"€ { res .get ('final_equity' , 0 ):,.2f} " )
62- m2 .metric ("Total Trades" , res .get ('total_trades' , 0 ))
63- # Calcolo ROI al volo
64- roi = ((res .get ('final_equity' , 0 ) - initial_cap ) / initial_cap ) * 100
65- m3 .metric ("Total ROI" , f"{ roi :+.2f} %" )
66-
67- except Exception as e :
68- st .error (f"Errore durante il backtest: { e } " )
169+ except Exception as e :
170+ st .error (f"Critical Error: { e } " )
69171
70- with tab_history :
71- st .write ("📂 **Previous Runs Browser**" )
72- base_dir = Path ("data/backtests" )
172+ with tab_results :
173+ st .write ("### 📂 Results Browser" )
73174
74- if base_dir .exists ():
75- # Logica per esplorare le cartelle
76- strategies = [x .name for x in base_dir .iterdir () if x .is_dir ()]
175+ base_dir = Path ("data/backtests" )
176+ if not base_dir .exists ():
177+ st .warning ("No backtests found." )
178+ st .stop ()
77179
78- # --- LAYOUT A COLONNE PER I SELETTORI ---
79- c_strat , c_run = st .columns (2 )
180+ # Elenco sessioni (Timestamp) ordinate dalla più recente
181+ sessions = sorted ([x .name for x in base_dir .iterdir () if x .is_dir ()], reverse = True )
182+
183+ # Se abbiamo appena finito una run, selezionala di default
184+ default_idx = 0
185+ if 'last_run_path' in st .session_state :
186+ last_name = Path (st .session_state ['last_run_path' ]).name
187+ if last_name in sessions :
188+ default_idx = sessions .index (last_name )
189+
190+ selected_session = st .selectbox ("Select Session (Timestamp)" , sessions , index = default_idx )
191+
192+ if selected_session :
193+ session_path = base_dir / selected_session
194+ st .caption (f"Path: { session_path } " )
80195
81- with c_strat :
82- sel_strat = st . selectbox ( "Select Strategy Folder" , strategies )
196+ # CARICAMENTO DATI
197+ strat_data , summary_df = load_benchmark_data ( session_path )
83198
84- sel_run = None
85- if sel_strat :
86- with c_run :
87- # Controllo se la cartella esiste e non è vuota
88- strat_path = base_dir / sel_strat
89- if strat_path .exists ():
90- runs = sorted ([x .name for x in strat_path .iterdir ()], reverse = True )
91- sel_run = st .selectbox ("Select Timestamp" , runs )
199+ if not strat_data :
200+ st .warning ("Empty session folder." )
201+ else :
202+ # 1. GRAFICO COMPARATIVO
203+ st .subheader ("📈 Equity Curve Comparison" )
204+ fig = plot_comparison (strat_data )
205+ st .plotly_chart (fig , use_container_width = True )
92206
93- if sel_run :
94- run_path = base_dir / sel_strat / sel_run
95- st .caption (f"Path: { run_path } " )
96- st .markdown ("---" )
97-
98- # Visualizzazione Dati
99- col_img , col_data = st .columns ([1 , 1 ])
207+ # 2. TABELLA METRICHE
208+ st .subheader ("🏆 Performance Summary" )
209+ # Formattazione colonne
210+ st .dataframe (
211+ summary_df .style .format ({
212+ "Final Equity" : "€ {:,.2f}" ,
213+ "ROI %" : "{:+.2f}%"
214+ }).background_gradient (subset = ["ROI %" ], cmap = "RdYlGn" ),
215+ use_container_width = True ,
216+ hide_index = True
217+ )
218+
219+ st .markdown ("---" )
220+
221+ # 3. DETTAGLIO SINGOLA STRATEGIA
222+ st .subheader ("🔍 Deep Dive: Single Strategy Details" )
223+ strat_keys = list (strat_data .keys ())
224+ detail_strat = st .selectbox ("Inspect Strategy:" , strat_keys )
225+
226+ if detail_strat :
227+ d_data = strat_data [detail_strat ]
228+ d_path = d_data ['path' ]
100229
101- with col_img :
102- if ( run_path / "chart.png" ). exists () :
103- # CORRETTO: use_container_width
104- st .image ( str ( run_path / "chart.png" ), use_container_width = True )
230+ c1 , c2 = st . columns ([ 1 , 1 ])
231+ with c1 :
232+ st . write ( "**Configuration Used:**" )
233+ st .json ( d_data [ 'config' ]. get ( 'params' , {}) )
105234
106- with col_data :
107- if (run_path / "config.json" ).exists ():
108- with open (run_path / "config.json" ) as f :
109- conf = json .load (f )
110- st .json (conf , expanded = False )
111-
112- if (run_path / "trades.csv" ).exists ():
113- # CORRETTO: use_container_width e height numerico
114- st .dataframe (pd .read_csv (run_path / "trades.csv" ), use_container_width = True , height = 300 )
235+ with c2 :
236+ st .write ("**Trade History:**" )
237+ trades_csv = d_path / "trades.csv"
238+ if trades_csv .exists ():
239+ df_trades = pd .read_csv (trades_csv )
240+ st .dataframe (df_trades , height = 200 , use_container_width = True )
241+ else :
242+ st .info ("No trades executed." )
0 commit comments