Skip to content

Commit cf0cbf6

Browse files
adrianhasseCopilot
andauthored
Add boxplot functionality to Analyzer (#20)
* add boxplot functionality to Analyzer * Update kissbt/analyzer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update kissbt/analyzer.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent a004944 commit cf0cbf6

File tree

4 files changed

+175
-109
lines changed

4 files changed

+175
-109
lines changed

.devcontainer/devcontainer.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
},
2626
"ruff.enable": true,
2727
"ruff.organizeImports": true,
28-
"python.analysis.typeCheckingMode": "strict",
2928
"python.analysis.autoImportCompletions": true,
3029
"python.analysis.indexing": true,
3130
"mypy.enabled": true,

examples/20241227_introduction.ipynb

Lines changed: 114 additions & 101 deletions
Large diffs are not rendered by default.

kissbt/analyzer.py

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

tests/test_integration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,4 @@ def test_analyzer_with_golden_cross(tech_stock_data):
104104
# Ensure running the plot functions does not raise an exception
105105
analyzer.plot_equity_curve()
106106
analyzer.plot_drawdowns()
107+
analyzer.plot_rolling_returns_distribution(252)

0 commit comments

Comments
 (0)