@@ -15,7 +15,13 @@ class Analyzer:
1515 key metrics used in financial analysis and portfolio management.
1616 """
1717
18- def __init__ (self , broker : Broker , bar_size : str = "1D" ) -> None :
18+ def __init__ (
19+ self ,
20+ broker : Broker ,
21+ bar_size : str = "1D" ,
22+ trading_hours_per_day : float = 6.5 ,
23+ trading_days_per_year : int = 252 ,
24+ ) -> None :
1925 """
2026 Initialize the Analyzer with a Broker instance and the bar size, which is the
2127 time interval of each bar in the data.
@@ -25,15 +31,27 @@ def __init__(self, broker: Broker, bar_size: str = "1D") -> None:
2531 bar_size (str): The time interval of each bar in the data, supported units
2632 are 'S' for seconds, 'T' for minutes, 'H' for hours and 'D' for days
2733 (default is "1D").
34+ trading_hours_per_day (float): Number of trading hours per day (default is
35+ 6.5, which assumes US equities market hours; adjust as needed for other
36+ markets).
37+ trading_days_per_year (int): Number of trading days per year (default is
38+ 252, which assumes US equities; adjust as needed for other markets).
2839 """
2940
3041 value = int (bar_size [:- 1 ])
31- unit = bar_size [- 1 ]
32- seconds_multiplier = {"S" : 1 , "T" : 60 , "H" : 3600 , "D" : 3600 * 6.5 }
33- if unit not in seconds_multiplier :
34- raise ValueError (f"Unsupported bar size unit: { unit } " )
35- self .seconds_per_bar = value * seconds_multiplier [unit ]
36- self .trading_seconds_per_year = 252 * 6.5 * 3600
42+ bar_unit = bar_size [- 1 ]
43+ seconds_multiplier = {
44+ "S" : 1 ,
45+ "T" : 60 ,
46+ "H" : 3600 ,
47+ "D" : 3600 * trading_hours_per_day ,
48+ }
49+ if bar_unit not in seconds_multiplier :
50+ raise ValueError (f"Unsupported bar size unit: { bar_unit } " )
51+ self .seconds_per_bar = value * seconds_multiplier [bar_unit ]
52+ self .trading_seconds_per_year = (
53+ trading_days_per_year * trading_hours_per_day * 3600
54+ )
3755
3856 self .broker = broker
3957 self .analysis_df = pd .DataFrame (self .broker .history )
@@ -321,3 +339,38 @@ def plot_equity_curve(self, logy: bool = False, **kwargs: Dict[str, Any]) -> Non
321339 logy = logy ,
322340 ** kwargs ,
323341 )
342+
343+ def plot_rolling_returns_distribution (
344+ self , window_bars : int , include_benchmark : bool = True , ** kwargs : Dict [str , Any ]
345+ ) -> None :
346+ """
347+ Plot box plots of rolling returns for the portfolio and optionally benchmark.
348+
349+ Parameters:
350+ window_bars (int): Window size as number of bars.
351+ include_benchmark (bool): Whether to include benchmark if available
352+ (default True).
353+ **kwargs (dict): Additional keyword arguments to pass to the pandas
354+ DataFrame.boxplot function for customizing the appearance and behavior
355+ of the box plot.
356+ """
357+ if window_bars >= len (self .analysis_df ):
358+ raise ValueError (
359+ f"Window size { window_bars } is too large for the available data { len (self .analysis_df )} ." # noqa: E501
360+ )
361+
362+ result = {
363+ "Portfolio" : self .analysis_df ["total_value" ]
364+ .pct_change (periods = window_bars )
365+ .dropna ()
366+ .reset_index (drop = True )
367+ }
368+ if include_benchmark and "benchmark" in self .analysis_df .columns :
369+ result ["Benchmark" ] = (
370+ self .analysis_df ["benchmark" ]
371+ .pct_change (periods = window_bars )
372+ .dropna ()
373+ .reset_index (drop = True )
374+ )
375+
376+ pd .DataFrame (result ).boxplot (** kwargs )
0 commit comments