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 +0 -0
- kissbt/analyzer.py +323 -0
- kissbt/broker.py +590 -0
- kissbt/engine.py +29 -0
- kissbt/entities.py +70 -0
- kissbt/strategy.py +98 -0
- kissbt-0.1.1.dist-info/LICENSE +201 -0
- kissbt-0.1.1.dist-info/METADATA +346 -0
- kissbt-0.1.1.dist-info/RECORD +11 -0
- kissbt-0.1.1.dist-info/WHEEL +5 -0
- kissbt-0.1.1.dist-info/top_level.txt +1 -0
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
|
+
)
|