kissbt 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kissbt/__init__.py ADDED
File without changes
kissbt/analyzer.py ADDED
@@ -0,0 +1,323 @@
1
+ from typing import Any, Dict
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from kissbt.broker import Broker
7
+
8
+
9
+ class Analyzer:
10
+ """
11
+ A class for analyzing trading performance and calculating various performance
12
+ metrics.
13
+
14
+ This class provides comprehensive analysis of trading performance by calculating
15
+ key metrics used in financial analysis and portfolio management.
16
+ """
17
+
18
+ def __init__(self, broker: Broker, bar_size: str = "1D") -> None:
19
+ """
20
+ Initialize the Analyzer with a Broker instance and the bar size, which is the
21
+ time interval of each bar in the data.
22
+
23
+ Parameters:
24
+ broker (Broker): The broker instance containing the trading history.
25
+ bar_size (str): The time interval of each bar in the data, supported units
26
+ are 'S' for seconds, 'M' for minutes, 'H' for hours and 'D' for days
27
+ (default is "1D").
28
+ """
29
+
30
+ value = int(bar_size[:-1])
31
+ unit = bar_size[-1]
32
+ seconds_multiplier = {"S": 1, "M": 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
37
+
38
+ self.broker = broker
39
+ self.analysis_df = pd.DataFrame(self.broker.history)
40
+ self.analysis_df["returns"] = self.analysis_df["total_value"].pct_change()
41
+ self.analysis_df["drawdown"] = (
42
+ self.analysis_df["total_value"].cummax() - self.analysis_df["total_value"]
43
+ ) / self.analysis_df["total_value"].cummax()
44
+
45
+ if "benchmark" in self.analysis_df.columns:
46
+ self.analysis_df["benchmark_returns"] = self.analysis_df[
47
+ "benchmark"
48
+ ].pct_change()
49
+ self.analysis_df["benchmark_drawdown"] = (
50
+ self.analysis_df["benchmark"].cummax() - self.analysis_df["benchmark"]
51
+ ) / self.analysis_df["benchmark"].cummax()
52
+
53
+ def get_performance_metrics(self) -> Dict[str, float]:
54
+ """
55
+ Calculate and return key performance metrics of the trading strategy.
56
+
57
+ This method computes various performance metrics used in financial analysis
58
+ and portfolio management. The returned dictionary includes the following keys:
59
+
60
+ - total_return: The total return of the portfolio as a decimal.
61
+ - annual_return: The annualized return of the portfolio as a decimal.
62
+ - sharpe_ratio: The Sharpe ratio of the trading strategy, a measure of
63
+ risk-adjusted return.
64
+ - max_drawdown: The maximum drawdown of the portfolio as a decimal.
65
+ - volatility: The annualized volatility of the portfolio returns.
66
+ - win_rate: The win rate of the trading strategy as a decimal.
67
+ - profit_factor: The profit factor of the trading strategy, a ratio of gross
68
+ profits to gross losses.
69
+
70
+ If a benchmark is available in the data, the dictionary also includes:
71
+ - total_benchmark_return: The total return of the benchmark as a decimal.
72
+ - annual_benchmark_return: The annualized return of the benchmark as a decimal.
73
+
74
+ Returns:
75
+ Dict[str, float]: A dictionary containing the calculated performance
76
+ metrics.
77
+ """
78
+
79
+ metrics = {
80
+ "total_return": self._calculate_total_return("total_value"),
81
+ "annual_return": self._calculate_annual_return("total_value"),
82
+ "sharpe_ratio": self._calculate_sharpe_ratio(),
83
+ "max_drawdown": self._calculate_max_drawdown(),
84
+ "volatility": self._calculate_annualized_volatility(),
85
+ "win_rate": self._calculate_win_rate(),
86
+ "profit_factor": self._calculate_profit_factor(),
87
+ }
88
+
89
+ if "benchmark" in self.analysis_df.columns:
90
+ metrics["total_benchmark_return"] = self._calculate_total_return(
91
+ "benchmark"
92
+ )
93
+ metrics["annual_benchmark_return"] = self._calculate_annual_return(
94
+ "benchmark"
95
+ )
96
+
97
+ return metrics
98
+
99
+ def _calculate_total_return(self, column: str) -> float:
100
+ """
101
+ Calculate the total return of either the portfolio or a benchmark.
102
+
103
+ Parameters:
104
+ column (str): The column name to calculate the total return for.
105
+
106
+ Returns:
107
+ float: The total return as a decimal (e.g., 0.10 for 10% total return).
108
+ """
109
+ return (
110
+ float(self.analysis_df[column].iloc[-1] / self.analysis_df[column].iloc[0])
111
+ - 1
112
+ )
113
+
114
+ def _calculate_annual_return(self, column: str) -> float:
115
+ """
116
+ Calculate the annualized return of the portfolio.
117
+
118
+ This method computes the annual rate of return by taking into account the total
119
+ return over the analysis period and normalizing it to a yearly basis. The
120
+ annualized return is a key performance metric that allows comparing investments
121
+ over different time periods by expressing returns as if they were earned at a
122
+ compound annual rate.
123
+
124
+ For example, if a portfolio earned 21% over 2 years, the annualized return would
125
+ be approximately 10% per year. This metric is particularly useful for:
126
+ - Comparing performance across different time periods
127
+ - Evaluating investment strategies against benchmarks
128
+
129
+ Note, that we assume that one year has 252 trading days.
130
+
131
+ Parameters:
132
+ column (str): The column name to calculate the total return for.
133
+
134
+ Returns:
135
+ float: The annualized return as a decimal (e.g., 0.10 for 10% annual return)
136
+ """
137
+ number_of_bars = len(self.analysis_df)
138
+ years = number_of_bars * self.seconds_per_bar / self.trading_seconds_per_year
139
+
140
+ total_return = (
141
+ self.analysis_df[column].iloc[-1] / self.analysis_df[column].iloc[0]
142
+ )
143
+ annualized_return = float((total_return ** (1 / years)) - 1)
144
+ return annualized_return
145
+
146
+ def _calculate_sharpe_ratio(self, risk_free_rate: float = 0.0) -> float:
147
+ """
148
+ Calculate the Sharpe ratio of the trading strategy.
149
+
150
+ The Sharpe ratio is a widely used metric in finance to evaluate the performance
151
+ of investment strategies and portfolios. It provides a way to compare the return
152
+ of an investment to its risk, with higher values indicating better risk-adjusted
153
+ returns.
154
+
155
+ The Sharpe ratio is calculated as the annualized return of the strategy minus
156
+ the risk-free rate, divided by the standard deviation of the strategy's returns.
157
+ The risk-free rate is typically the return on a risk-free investment, such as a
158
+ US Treasury bond.
159
+
160
+ Parameters:
161
+ risk_free_rate (float): The annual risk-free rate (default is 0.0).
162
+ """
163
+ bars_per_year = self.trading_seconds_per_year / self.seconds_per_bar
164
+ rf_rate_per_bar = (1 + risk_free_rate) ** (1 / bars_per_year) - 1
165
+ excess_returns = self.analysis_df["returns"] - rf_rate_per_bar
166
+
167
+ if np.isclose(excess_returns.std(), 0):
168
+ return 0
169
+ return float(
170
+ np.sqrt(bars_per_year) * excess_returns.mean() / excess_returns.std()
171
+ )
172
+
173
+ def _calculate_max_drawdown(self) -> float:
174
+ """
175
+ Calculate the maximum drawdown of the trading strategy.
176
+
177
+ The maximum drawdown is a risk metric that measures the largest peak-to-trough
178
+ decline in the value of a portfolio over a specified period. It represents the
179
+ worst loss an investor could have experienced if they had bought at the highest
180
+ point and sold at the lowest point during the period.
181
+
182
+ This metric is important because it provides insight into the potential downside
183
+ risk of an investment strategy. A lower maximum drawdown indicates a more stable
184
+ and less volatile strategy, while a higher maximum drawdown suggests greater
185
+ risk and potential for significant losses.
186
+
187
+ Returns:
188
+ float: The maximum drawdown as a decimal (e.g., 0.20 for a 20% drawdown).
189
+ """
190
+ max_value = self.analysis_df["total_value"].expanding().max()
191
+ drawdowns = (max_value - self.analysis_df["total_value"]) / max_value
192
+ return float(drawdowns.max())
193
+
194
+ def _calculate_annualized_volatility(self) -> float:
195
+ """
196
+ Calculate the annualized volatility of the portfolio returns.
197
+
198
+ The calculation takes into account the bar size by using the number of bars
199
+ per year based on the trading seconds per year and seconds per bar.
200
+
201
+ Returns:
202
+ float: The annualized volatility of the portfolio returns
203
+ """
204
+ bars_per_year = self.trading_seconds_per_year / self.seconds_per_bar
205
+ return float(self.analysis_df["returns"].std() * np.sqrt(bars_per_year))
206
+
207
+ def _calculate_win_rate(self) -> float:
208
+ """
209
+ Calculate the win rate of the trading strategy.
210
+
211
+ The win rate is a performance metric that measures the proportion of profitable
212
+ trades out of the total number of trades. It is calculated as the number of
213
+ profitable trades divided by the total number of closed trades.
214
+
215
+ This metric is important because it provides insight into the effectiveness of
216
+ the trading strategy. A higher win rate indicates a greater proportion of
217
+ successful trades, while a lower win rate suggests a higher proportion of losing
218
+ trades.
219
+
220
+ Note that this calculation does not consider trading costs such as commissions
221
+ and fees, which can impact the overall profitability of the strategy.
222
+
223
+ Returns:
224
+ float: The win rate as a decimal (e.g., 0.60 for a 60% win rate).
225
+ """
226
+ if not self.broker.closed_positions:
227
+ return 0
228
+ profitable_trades = sum(
229
+ 1
230
+ for pos in self.broker.closed_positions
231
+ if (pos.selling_price - pos.purchase_price) * pos.size > 0
232
+ )
233
+ return float(profitable_trades / len(self.broker.closed_positions))
234
+
235
+ def _calculate_profit_factor(self) -> float:
236
+ """
237
+ Calculate the profit factor of the trading strategy.
238
+
239
+ The profit factor is a performance metric that measures the ratio of gross
240
+ profits to gross losses for a trading strategy. It is calculated as the total
241
+ gross profits divided by the total gross losses. This metric provides insight
242
+ into the overall profitability of the strategy.
243
+
244
+ A profit factor greater than 1 indicates that the strategy is profitable, as the
245
+ gross profits exceed the gross losses. Conversely, a profit factor less than 1
246
+ indicates that the strategy is unprofitable. A higher profit factor suggests a
247
+ more robust and potentially more profitable strategy.
248
+
249
+ This metric is important because it helps in assessing the risk-reward profile
250
+ of the trading strategy, complementing other metrics such as the Sharpe ratio
251
+ and win rate.
252
+
253
+ Returns:
254
+ float: The profit factor as a ratio (e.g., 1.5 for a strategy that gains
255
+ $1.50 for every $1.00 lost).
256
+ """
257
+ profits = sum(
258
+ (pos.selling_price - pos.purchase_price) * pos.size
259
+ for pos in self.broker.closed_positions
260
+ if (pos.selling_price - pos.purchase_price) * pos.size > 0
261
+ )
262
+ losses = abs(
263
+ sum(
264
+ (pos.selling_price - pos.purchase_price) * pos.size
265
+ for pos in self.broker.closed_positions
266
+ if (pos.selling_price - pos.purchase_price) * pos.size < 0
267
+ )
268
+ )
269
+ return float(profits / losses) if losses != 0 else float("inf")
270
+
271
+ def plot_drawdowns(self, **kwargs: Dict[str, Any]) -> None:
272
+ """
273
+ Plot the drawdown over time for both the portfolio and benchmark (if available).
274
+
275
+ This method creates a line plot showing the drawdown percentage over time. If a
276
+ benchmark is present in the data, it will show both the portfolio and benchmark
277
+ drawdowns for comparison.
278
+
279
+ Parameters:
280
+ **kwargs(Dict[str, Any]): Additional keyword arguments to pass to the plot
281
+ function of pandas.
282
+ """
283
+ columns_to_plot = ["date", "drawdown"]
284
+ if "benchmark_drawdown" in self.analysis_df.columns:
285
+ columns_to_plot.append("benchmark_drawdown")
286
+
287
+ self.analysis_df.loc[:, columns_to_plot].plot(
288
+ x="date",
289
+ title="Portfolio Drawdown Over Time",
290
+ xlabel="Date",
291
+ ylabel="Drawdown %",
292
+ **kwargs,
293
+ )
294
+
295
+ def plot_equity_curve(self, logy: bool = False, **kwargs: Dict[str, Any]) -> None:
296
+ """
297
+ Plot the portfolio's cash, total value, and benchmark (if available) over time.
298
+
299
+ This method creates a line plot showing the portfolio's cash, total value, and
300
+ benchmark over time for comparison. If the benchmark is not available, it will
301
+ plot only the available columns. If logy is True, the cash column will be
302
+ excluded, to focus on the total value and benchmark on logaritmic scale.
303
+
304
+ Parameters:
305
+ logy (bool): If True, use a logarithmic scale for the y-axis and exclude the
306
+ cash column.
307
+ **kwargs(Dict[str, Any]): Additional keyword arguments to pass to the plot
308
+ function of pandas.
309
+ """
310
+ columns_to_plot = ["date", "total_value"]
311
+ if "benchmark" in self.analysis_df.columns:
312
+ columns_to_plot.append("benchmark")
313
+ if not logy:
314
+ columns_to_plot.append("cash")
315
+
316
+ self.analysis_df.loc[:, columns_to_plot].plot(
317
+ x="date",
318
+ title="Portfolio Equity Curve Over Time",
319
+ xlabel="Date",
320
+ ylabel="Value",
321
+ logy=logy,
322
+ **kwargs,
323
+ )