kissbt 0.1.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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
+ )