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 +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
|
+
)
|