kissbt 0.1.4__tar.gz → 0.1.6__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: kissbt
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: The keep it simple backtesting framework for Python.
5
5
  Author-email: Adrian Hasse <adrian.hasse@finblobs.com>
6
6
  Maintainer-email: Adrian Hasse <adrian.hasse@finblobs.com>
@@ -216,15 +216,16 @@ Description-Content-Type: text/markdown
216
216
  License-File: LICENSE
217
217
  Requires-Dist: numpy
218
218
  Requires-Dist: pandas
219
+ Requires-Dist: scipy
219
220
  Requires-Dist: matplotlib
220
221
  Provides-Extra: dev
221
222
  Requires-Dist: pytest; extra == "dev"
222
223
  Requires-Dist: pytest-mock; extra == "dev"
223
- Requires-Dist: flake8; extra == "dev"
224
- Requires-Dist: black; extra == "dev"
225
- Requires-Dist: isort; extra == "dev"
224
+ Requires-Dist: ruff; extra == "dev"
225
+ Requires-Dist: mypy; extra == "dev"
226
226
  Requires-Dist: yfinance; extra == "dev"
227
227
  Requires-Dist: pyarrow; extra == "dev"
228
+ Dynamic: license-file
228
229
 
229
230
  # kissbt
230
231
 
@@ -264,7 +265,7 @@ pip install kissbt
264
265
  To install `kissbt` via `conda`, run the following command:
265
266
 
266
267
  ```sh
267
- conda install kissbt
268
+ conda install -c conda-forge kissbt
268
269
  ```
269
270
 
270
271
  ## Usage
@@ -36,7 +36,7 @@ pip install kissbt
36
36
  To install `kissbt` via `conda`, run the following command:
37
37
 
38
38
  ```sh
39
- conda install kissbt
39
+ conda install -c conda-forge kissbt
40
40
  ```
41
41
 
42
42
  ## Usage
@@ -2,6 +2,7 @@ from typing import Any, Dict
2
2
 
3
3
  import numpy as np
4
4
  import pandas as pd
5
+ from scipy.stats import linregress
5
6
 
6
7
  from kissbt.broker import Broker
7
8
 
@@ -15,7 +16,13 @@ class Analyzer:
15
16
  key metrics used in financial analysis and portfolio management.
16
17
  """
17
18
 
18
- def __init__(self, broker: Broker, bar_size: str = "1D") -> None:
19
+ def __init__(
20
+ self,
21
+ broker: Broker,
22
+ bar_size: str = "1D",
23
+ trading_hours_per_day: float = 6.5,
24
+ trading_days_per_year: int = 252,
25
+ ) -> None:
19
26
  """
20
27
  Initialize the Analyzer with a Broker instance and the bar size, which is the
21
28
  time interval of each bar in the data.
@@ -23,17 +30,29 @@ class Analyzer:
23
30
  Parameters:
24
31
  broker (Broker): The broker instance containing the trading history.
25
32
  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
33
+ are 'S' for seconds, 'T' for minutes, 'H' for hours and 'D' for days
27
34
  (default is "1D").
35
+ trading_hours_per_day (float): Number of trading hours per day (default is
36
+ 6.5, which assumes US equities market hours; adjust as needed for other
37
+ markets).
38
+ trading_days_per_year (int): Number of trading days per year (default is
39
+ 252, which assumes US equities; adjust as needed for other markets).
28
40
  """
29
41
 
30
42
  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
43
+ bar_unit = bar_size[-1]
44
+ seconds_multiplier = {
45
+ "S": 1,
46
+ "T": 60,
47
+ "H": 3600,
48
+ "D": 3600 * trading_hours_per_day,
49
+ }
50
+ if bar_unit not in seconds_multiplier:
51
+ raise ValueError(f"Unsupported bar size unit: {bar_unit}")
52
+ self.seconds_per_bar = value * seconds_multiplier[bar_unit]
53
+ self.trading_seconds_per_year = (
54
+ trading_days_per_year * trading_hours_per_day * 3600
55
+ )
37
56
 
38
57
  self.broker = broker
39
58
  self.analysis_df = pd.DataFrame(self.broker.history)
@@ -50,6 +69,52 @@ class Analyzer:
50
69
  self.analysis_df["benchmark"].cummax() - self.analysis_df["benchmark"]
51
70
  ) / self.analysis_df["benchmark"].cummax()
52
71
 
72
+ def _equity_curve_stats(
73
+ self,
74
+ value_series: pd.Series,
75
+ *,
76
+ prefix: str = "",
77
+ ) -> Dict[str, float]:
78
+ """
79
+ Calculate statistics of the equity curve based on the log-equity curve.
80
+ This method performs a linear regression on the log-equity curve to estimate
81
+ the slope, standard error, t-statistic, and R² value.
82
+
83
+ - slope: The slope of the log-equity curve, indicating the average return per
84
+ bar.
85
+ - slope_se: The standard error of the slope, indicating the variability of the
86
+ average return.
87
+ - slope_tstat: The t-statistic of the slope, indicating how strongly the data
88
+ supports the presence of a non-zero trend in the log-equity curve.
89
+ - r_squared: The R² value of the regression, indicating the proportion of
90
+ variance explained.
91
+
92
+ Parameters:
93
+ value_series (pd.Series): The series of values to analyze, typically the
94
+ total value of the portfolio or benchmark.
95
+ prefix (str): A prefix to add to the keys in the returned dictionary, useful
96
+ for distinguishing between portfolio and benchmark statistics.
97
+ """
98
+
99
+ if (value_series <= 0).any():
100
+ raise ValueError(
101
+ "Value series contains non-positive values, cannot compute log-based statistics" # noqa: E501
102
+ )
103
+ y = np.log(value_series.to_numpy())
104
+ x = np.arange(y.size, dtype=float)
105
+
106
+ res = linregress(x, y)
107
+ slope, slope_se, r_squared = res.slope, res.stderr, res.rvalue**2
108
+
109
+ slope_tstat = slope / slope_se
110
+
111
+ return {
112
+ f"{prefix}slope": slope,
113
+ f"{prefix}slope_se": slope_se,
114
+ f"{prefix}slope_tstat": slope_tstat,
115
+ f"{prefix}r_squared": r_squared,
116
+ }
117
+
53
118
  def get_performance_metrics(self) -> Dict[str, float]:
54
119
  """
55
120
  Calculate and return key performance metrics of the trading strategy.
@@ -67,9 +132,26 @@ class Analyzer:
67
132
  - profit_factor: The profit factor of the trading strategy, a ratio of gross
68
133
  profits to gross losses.
69
134
 
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.
135
+ Additionally we compute the equity curve statistics for the portfolio's
136
+ total value, including:
137
+ - slope: The slope of the log-equity curve, indicating the average return per
138
+ bar.
139
+ - slope_se: The standard error of the slope, indicating the variability of the
140
+ average return.
141
+ - slope_tstat: The t-statistic of the slope (slope / slope_se), indicating how
142
+ strongly the data supports the presence of a non-zero trend in the
143
+ log-equity curve. A larger absolute value (positive or negative) provides
144
+ stronger evidence against H_0 (β = 0), suggesting that the observed trend is
145
+ unlikely to be due to random fluctuations. For typical backtests the
146
+ t-statistic approximately follows a standard normal distribution. Values
147
+ above +1.96 or below -1.96 are considered statistically significant at the
148
+ 95% confidence level.
149
+ - r_squared: The R² value of the regression, indicating the proportion of
150
+ variance explained by the model.
151
+
152
+ If a benchmark is available in the data, the dictionary also includes the
153
+ total_return, annual_return, slope, slope_se, slope_tstat and r_squared for the
154
+ benchmark, prefixed with "benchmark_".
73
155
 
74
156
  Returns:
75
157
  Dict[str, float]: A dictionary containing the calculated performance
@@ -85,14 +167,21 @@ class Analyzer:
85
167
  "win_rate": self._calculate_win_rate(),
86
168
  "profit_factor": self._calculate_profit_factor(),
87
169
  }
170
+ metrics.update(self._equity_curve_stats(self.analysis_df["total_value"]))
88
171
 
89
172
  if "benchmark" in self.analysis_df.columns:
90
- metrics["total_benchmark_return"] = self._calculate_total_return(
173
+ metrics["benchmark_total_return"] = self._calculate_total_return(
91
174
  "benchmark"
92
175
  )
93
- metrics["annual_benchmark_return"] = self._calculate_annual_return(
176
+ metrics["benchmark_annual_return"] = self._calculate_annual_return(
94
177
  "benchmark"
95
178
  )
179
+ metrics.update(
180
+ self._equity_curve_stats(
181
+ self.analysis_df["benchmark"],
182
+ prefix="benchmark_",
183
+ )
184
+ )
96
185
 
97
186
  return metrics
98
187
 
@@ -280,14 +369,14 @@ class Analyzer:
280
369
  **kwargs(Dict[str, Any]): Additional keyword arguments to pass to the plot
281
370
  function of pandas.
282
371
  """
283
- columns_to_plot = ["date", "drawdown"]
372
+ columns_to_plot = ["timestamp", "drawdown"]
284
373
  if "benchmark_drawdown" in self.analysis_df.columns:
285
374
  columns_to_plot.append("benchmark_drawdown")
286
375
 
287
376
  self.analysis_df.loc[:, columns_to_plot].plot(
288
- x="date",
377
+ x="timestamp",
289
378
  title="Portfolio Drawdown Over Time",
290
- xlabel="Date",
379
+ xlabel="Timestamp",
291
380
  ylabel="Drawdown %",
292
381
  **kwargs,
293
382
  )
@@ -299,7 +388,7 @@ class Analyzer:
299
388
  This method creates a line plot showing the portfolio's cash, total value, and
300
389
  benchmark over time for comparison. If the benchmark is not available, it will
301
390
  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.
391
+ excluded, to focus on the total value and benchmark on logarithmic scale.
303
392
 
304
393
  Parameters:
305
394
  logy (bool): If True, use a logarithmic scale for the y-axis and exclude the
@@ -307,17 +396,52 @@ class Analyzer:
307
396
  **kwargs(Dict[str, Any]): Additional keyword arguments to pass to the plot
308
397
  function of pandas.
309
398
  """
310
- columns_to_plot = ["date", "total_value"]
399
+ columns_to_plot = ["timestamp", "total_value"]
311
400
  if "benchmark" in self.analysis_df.columns:
312
401
  columns_to_plot.append("benchmark")
313
402
  if not logy:
314
403
  columns_to_plot.append("cash")
315
404
 
316
405
  self.analysis_df.loc[:, columns_to_plot].plot(
317
- x="date",
406
+ x="timestamp",
318
407
  title="Portfolio Equity Curve Over Time",
319
- xlabel="Date",
408
+ xlabel="Timestamp",
320
409
  ylabel="Value",
321
410
  logy=logy,
322
411
  **kwargs,
323
412
  )
413
+
414
+ def plot_rolling_returns_distribution(
415
+ self, window_bars: int, include_benchmark: bool = True, **kwargs: Dict[str, Any]
416
+ ) -> None:
417
+ """
418
+ Plot box plots of rolling returns for the portfolio and optionally benchmark.
419
+
420
+ Parameters:
421
+ window_bars (int): Window size as number of bars.
422
+ include_benchmark (bool): Whether to include benchmark if available
423
+ (default True).
424
+ **kwargs (dict): Additional keyword arguments to pass to the pandas
425
+ DataFrame.boxplot function for customizing the appearance and behavior
426
+ of the box plot.
427
+ """
428
+ if window_bars >= len(self.analysis_df):
429
+ raise ValueError(
430
+ f"Window size {window_bars} is too large for the available data {len(self.analysis_df)}." # noqa: E501
431
+ )
432
+
433
+ result = {
434
+ "Portfolio": self.analysis_df["total_value"]
435
+ .pct_change(periods=window_bars)
436
+ .dropna()
437
+ .reset_index(drop=True)
438
+ }
439
+ if include_benchmark and "benchmark" in self.analysis_df.columns:
440
+ result["Benchmark"] = (
441
+ self.analysis_df["benchmark"]
442
+ .pct_change(periods=window_bars)
443
+ .dropna()
444
+ .reset_index(drop=True)
445
+ )
446
+
447
+ pd.DataFrame(result).boxplot(**kwargs)
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from typing import Dict, List, Optional
3
2
 
4
3
  import pandas as pd
@@ -24,7 +23,7 @@ class Broker:
24
23
  Typical usage:
25
24
  broker = Broker(start_capital=100000, fees=0.001)
26
25
  broker.place_order(Order("AAPL", 100, OrderType.OPEN))
27
- broker.update(next_bar, next_datetime)
26
+ broker.update(next_bar, next_timestamp)
28
27
  """
29
28
 
30
29
  def __init__(
@@ -64,9 +63,9 @@ class Broker:
64
63
  self._open_orders: List[Order] = []
65
64
 
66
65
  self._current_bar: pd.DataFrame = pd.DataFrame()
67
- self._current_datetime = None
66
+ self._current_timestamp: Optional[pd.Timestamp] = None
68
67
  self._previous_bar: pd.DataFrame = pd.DataFrame()
69
- self._previous_datetime = None
68
+ self._previous_timestamp: Optional[pd.Timestamp] = None
70
69
 
71
70
  self._long_only = long_only
72
71
  self._short_fee_rate = short_fee_rate
@@ -75,8 +74,8 @@ class Broker:
75
74
  self._benchmark = benchmark
76
75
  self._benchmark_size = 0.0
77
76
 
78
- self._history: Dict[str, List[float]] = {
79
- "date": [],
77
+ self._history: Dict[str, List[float | int | pd.Timestamp]] = {
78
+ "timestamp": [],
80
79
  "cash": [],
81
80
  "long_position_value": [],
82
81
  "short_position_value": [],
@@ -90,7 +89,7 @@ class Broker:
90
89
  """
91
90
  Updates the history dictionary with the current portfolio state.
92
91
  """
93
- self._history["date"].append(self._current_datetime)
92
+ self._history["timestamp"].append(self._current_timestamp)
94
93
  self._history["cash"].append(self._cash)
95
94
  self._history["long_position_value"].append(self.long_position_value)
96
95
  self._history["short_position_value"].append(self.short_position_value)
@@ -164,7 +163,7 @@ class Broker:
164
163
  raise ValueError(f"Unknown order type {order.order_type}")
165
164
 
166
165
  def _update_closed_positions(
167
- self, ticker: str, size: float, price: float, datetime: datetime
166
+ self, ticker: str, size: float, price: float, timestamp: pd.Timestamp
168
167
  ):
169
168
  """
170
169
  Updates the list of closed positions for a given trade.
@@ -178,7 +177,7 @@ class Broker:
178
177
  ticker (str): The ticker symbol of the position
179
178
  size (float): Position size (positive for long, negative for short)
180
179
  price (float): The current closing/reduction price
181
- datetime (datetime): Timestamp of the closing/reduction
180
+ timestamp (timestamp): Timestamp of the closing/reduction
182
181
  """
183
182
  if (
184
183
  ticker in self._open_positions
@@ -191,9 +190,9 @@ class Broker:
191
190
  self._open_positions[ticker].ticker,
192
191
  min(self._open_positions[ticker].size, abs(size)),
193
192
  self._open_positions[ticker].price,
194
- self._open_positions[ticker].datetime,
193
+ self._open_positions[ticker].timestamp,
195
194
  price,
196
- datetime,
195
+ timestamp,
197
196
  ),
198
197
  )
199
198
  # if short position is closed/reduced
@@ -203,20 +202,20 @@ class Broker:
203
202
  self._open_positions[ticker].ticker,
204
203
  max(self._open_positions[ticker].size, -size),
205
204
  price,
206
- datetime,
205
+ timestamp,
207
206
  self._open_positions[ticker].price,
208
- self._open_positions[ticker].datetime,
207
+ self._open_positions[ticker].timestamp,
209
208
  ),
210
209
  )
211
210
 
212
211
  def _update_open_positions(
213
- self, ticker: str, size: float, price: float, datetime: datetime
212
+ self, ticker: str, size: float, price: float, timestamp: pd.Timestamp
214
213
  ):
215
214
  """
216
215
  Updates the open positions for a given ticker.
217
216
 
218
217
  If the ticker already exists in the open positions, it updates the size, price,
219
- and datetime based on the new transaction. If the size of the position becomes
218
+ and timestamp based on the new transaction. If the size of the position becomes
220
219
  zero, the position is removed. If the ticker does not exist, a new open position
221
220
  is created.
222
221
 
@@ -224,7 +223,8 @@ class Broker:
224
223
  ticker (str): The ticker symbol of the asset.
225
224
  size (float): The size of the position.
226
225
  price (float): The price at which the position was opened or updated.
227
- datetime (datetime): The datetime when the position was opened or updated.
226
+ timestamp (Timestamp): The timestamp when the position was opened or
227
+ updated.
228
228
  """
229
229
  if ticker in self._open_positions:
230
230
  if size + self._open_positions[ticker].size == 0.0:
@@ -232,7 +232,7 @@ class Broker:
232
232
  else:
233
233
  open_position_size = self._open_positions[ticker].size + size
234
234
  open_position_price = price
235
- open_position_datetime = datetime
235
+ open_position_timestamp = timestamp
236
236
 
237
237
  if size * self._open_positions[ticker].size > 0.0:
238
238
  open_position_price = (
@@ -240,22 +240,22 @@ class Broker:
240
240
  * self._open_positions[ticker].price
241
241
  + size * price
242
242
  ) / (self._open_positions[ticker].size + size)
243
- open_position_datetime = self._open_positions[ticker].datetime
243
+ open_position_timestamp = self._open_positions[ticker].timestamp
244
244
  elif abs(self._open_positions[ticker].size) > abs(size):
245
- open_position_datetime = self._open_positions[ticker].datetime
245
+ open_position_timestamp = self._open_positions[ticker].timestamp
246
246
  open_position_price = self._open_positions[ticker].price
247
247
  self._open_positions[ticker] = OpenPosition(
248
248
  ticker,
249
249
  open_position_size,
250
250
  open_position_price,
251
- open_position_datetime,
251
+ open_position_timestamp,
252
252
  )
253
253
  else:
254
254
  self._open_positions[ticker] = OpenPosition(
255
255
  ticker,
256
256
  size,
257
257
  price,
258
- datetime,
258
+ timestamp,
259
259
  )
260
260
 
261
261
  def _update_cash(self, order: Order, price: float):
@@ -272,29 +272,29 @@ class Broker:
272
272
  else:
273
273
  self._cash -= order.size * price * (1.0 - self._fees)
274
274
 
275
- def _check_long_only_condition(self, order: Order, datetime: datetime):
275
+ def _check_long_only_condition(self, order: Order, timestamp: pd.Timestamp):
276
276
  size = order.size
277
277
  if order.ticker in self._open_positions:
278
278
  size += self._open_positions[order.ticker].size
279
279
 
280
280
  if size < 0.0:
281
281
  raise ValueError(
282
- f"Short selling is not allowed for {order.ticker} on {datetime}."
282
+ f"Short selling is not allowed for {order.ticker} on {timestamp}."
283
283
  )
284
284
 
285
285
  def _execute_order(
286
286
  self,
287
287
  order: Order,
288
288
  bar: pd.DataFrame,
289
- datetime: datetime,
289
+ timestamp: pd.Timestamp,
290
290
  ) -> bool:
291
291
  """
292
- Executes an order based on the provided bar data and datetime.
292
+ Executes an order based on the provided bar data and timestamp.
293
293
 
294
294
  Args:
295
295
  order (Order): The order to be executed.
296
296
  bar (pd.DataFrame): The bar data containing price information.
297
- datetime (datetime): The datetime at which the order is executed.
297
+ timestamp (Timestamp): The timestamp at which the order is executed.
298
298
 
299
299
  Returns:
300
300
  bool: True if the order was successfully executed, False otherwise.
@@ -308,7 +308,7 @@ class Broker:
308
308
  return False
309
309
 
310
310
  if self._long_only:
311
- self._check_long_only_condition(order, datetime)
311
+ self._check_long_only_condition(order, timestamp)
312
312
 
313
313
  price = self._get_price_for_order(order, bar)
314
314
 
@@ -319,16 +319,16 @@ class Broker:
319
319
  # update cash for long and short positions
320
320
  self._update_cash(order, price)
321
321
 
322
- self._update_closed_positions(ticker, order.size, price, datetime)
322
+ self._update_closed_positions(ticker, order.size, price, timestamp)
323
323
 
324
- self._update_open_positions(ticker, order.size, price, datetime)
324
+ self._update_open_positions(ticker, order.size, price, timestamp)
325
325
 
326
326
  return True
327
327
 
328
328
  def update(
329
329
  self,
330
330
  next_bar: pd.DataFrame,
331
- next_datetime: pd.Timestamp,
331
+ next_timestamp: pd.Timestamp,
332
332
  ):
333
333
  """
334
334
  Updates the broker's state with the next trading bar and executes pending
@@ -344,7 +344,7 @@ class Broker:
344
344
  Args:
345
345
  next_bar (pd.DataFrame): The next trading bar data containing at minimum
346
346
  'close' prices for assets
347
- next_datetime (pd.Timestamp): The timestamp for the next trading bar
347
+ next_timestamp (pd.Timestamp): The timestamp for the next trading bar
348
348
 
349
349
  Notes:
350
350
  - Short fees are calculated using the current bar's closing price
@@ -354,9 +354,9 @@ class Broker:
354
354
  - Good-till-cancel orders that aren't filled are retained for the next bar
355
355
  """
356
356
  self._previous_bar = self._current_bar
357
- self._previous_datetime = self._current_datetime
357
+ self._previous_timestamp = self._current_timestamp
358
358
  self._current_bar = next_bar
359
- self._current_datetime = next_datetime
359
+ self._current_timestamp = next_timestamp
360
360
 
361
361
  # consider short fees
362
362
  if not self._long_only:
@@ -384,7 +384,7 @@ class Broker:
384
384
  self._execute_order(
385
385
  Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
386
386
  self._previous_bar,
387
- self._previous_datetime,
387
+ self._previous_timestamp,
388
388
  )
389
389
 
390
390
  # buy and sell assets
@@ -396,16 +396,18 @@ class Broker:
396
396
  if open_order.ticker in ticker_not_available:
397
397
  if open_order.size > 0:
398
398
  print(
399
- f"{open_order.ticker} could not be bought on {self._current_datetime}." # noqa: E501
399
+ f"{open_order.ticker} could not be bought on {self._current_timestamp}." # noqa: E501
400
400
  )
401
401
  else:
402
402
  print(
403
- f"{open_order.ticker} could not be sold on {self._current_datetime}." # noqa: E501
403
+ f"{open_order.ticker} could not be sold on {self._current_timestamp}." # noqa: E501
404
404
  )
405
405
  continue
406
406
  if (
407
407
  not self._execute_order(
408
- open_order, self._current_bar, self._current_datetime
408
+ open_order,
409
+ self._current_bar,
410
+ self._current_timestamp,
409
411
  )
410
412
  and open_order.good_till_cancel
411
413
  ):
@@ -437,7 +439,7 @@ class Broker:
437
439
  self._execute_order(
438
440
  Order(ticker, -self._open_positions[ticker].size, OrderType.CLOSE),
439
441
  self._current_bar,
440
- self._current_datetime,
442
+ self._current_timestamp,
441
443
  )
442
444
 
443
445
  def place_order(self, order: Order):
@@ -522,7 +524,7 @@ class Broker:
522
524
  - ticker: Financial instrument identifier
523
525
  - size: Position size (positive=long, negative=short)
524
526
  - price: Average entry price
525
- - datetime: Position opening timestamp
527
+ - timestamp: Position opening timestamp
526
528
 
527
529
  Returns:
528
530
  Dict[str, OpenPosition]: Dictionary mapping ticker symbols to positions.
@@ -578,13 +580,14 @@ class Broker:
578
580
  return self._cash
579
581
 
580
582
  @property
581
- def benchmark(self) -> str:
583
+ def benchmark(self) -> Optional[str]:
582
584
  """Gets the benchmark symbol used for performance comparison.
583
585
 
584
586
  The benchmark tracks a reference asset (e.g., market index) to evaluate relative
585
587
  strategy performance. Returns None if no benchmark was specified.
586
588
 
587
589
  Returns:
588
- str: Ticker symbol of the benchmark instrument.
590
+ Optional[str]: Ticker symbol of the benchmark instrument, or None if not
591
+ set.
589
592
  """
590
593
  return self._benchmark
@@ -20,10 +20,10 @@ class Engine:
20
20
  self.strategy = strategy
21
21
 
22
22
  def run(self, data: pd.DataFrame) -> None:
23
- for current_date, current_data in data.groupby("date"):
24
- current_data.index = current_data.index.droplevel("date")
23
+ for current_timestamp, current_data in data.groupby("timestamp"):
24
+ current_data.index = current_data.index.droplevel("timestamp")
25
25
 
26
- self.broker.update(current_data, current_date)
27
- self.strategy(current_data, current_date)
26
+ self.broker.update(current_data, current_timestamp)
27
+ self.strategy.generate_orders(current_data, current_timestamp)
28
28
 
29
29
  self.broker.liquidate_positions()
@@ -39,13 +39,13 @@ class OpenPosition:
39
39
  ticker (str): Financial instrument identifier
40
40
  size (float): Position size (positive for long, negative for short)
41
41
  price (float): Opening price of the position
42
- datetime (datetime): Position opening timestamp
42
+ timestamp (pd.Timestamp): Position opening timestamp
43
43
  """
44
44
 
45
45
  ticker: str
46
46
  size: float
47
47
  price: float
48
- datetime: pd.Timestamp
48
+ timestamp: pd.Timestamp
49
49
 
50
50
 
51
51
  @dataclass(frozen=True)
@@ -57,14 +57,14 @@ class ClosedPosition:
57
57
  ticker (str): Financial instrument identifier
58
58
  size (float): Position size (positive for long, negative for short)
59
59
  purchase_price (float): Entry price of the position
60
- purchase_datetime (pd.Timestamp): Position entry timestamp
60
+ purchase_timestamp (pd.Timestamp): Position entry timestamp
61
61
  selling_price (float): Exit price of the position
62
- selling_datetime (pd.Timestamp): Position exit timestamp
62
+ selling_timestamp (pd.Timestamp): Position exit timestamp
63
63
  """
64
64
 
65
65
  ticker: str
66
66
  size: float
67
67
  purchase_price: float
68
- purchase_datetime: pd.Timestamp
68
+ purchase_timestamp: pd.Timestamp
69
69
  selling_price: float
70
- selling_datetime: pd.Timestamp
70
+ selling_timestamp: pd.Timestamp
@@ -49,7 +49,7 @@ class Strategy(ABC):
49
49
  def generate_orders(
50
50
  self,
51
51
  current_data: pd.DataFrame,
52
- current_datetime: pd.Timestamp,
52
+ current_timestamp: pd.Timestamp,
53
53
  ) -> None:
54
54
  """
55
55
  Generate trading orders based on current market data and indicators.
@@ -71,7 +71,7 @@ class Strategy(ABC):
71
71
  - volume: Trading volume
72
72
  - [custom]: Any additional indicators added during data preparation
73
73
 
74
- current_datetime: Timestamp of the current bar, e.g. used for order timing
74
+ current_timestamp: Timestamp of the current bar, e.g. used for order timing
75
75
 
76
76
  Returns:
77
77
  None
@@ -80,7 +80,7 @@ class Strategy(ABC):
80
80
  - Orders are placed through the _broker instance using methods like:
81
81
  create_market_order(), create_limit_order()
82
82
  - Position and portfolio information can be accessed through the _broker
83
- - Avoid look-ahead bias by only using data available at current_datetime
83
+ - Avoid look-ahead bias by only using data available at current_timestamp
84
84
  - All orders are processed at the next bar's prices (which price depends on
85
85
  the order type)
86
86
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: kissbt
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: The keep it simple backtesting framework for Python.
5
5
  Author-email: Adrian Hasse <adrian.hasse@finblobs.com>
6
6
  Maintainer-email: Adrian Hasse <adrian.hasse@finblobs.com>
@@ -216,15 +216,16 @@ Description-Content-Type: text/markdown
216
216
  License-File: LICENSE
217
217
  Requires-Dist: numpy
218
218
  Requires-Dist: pandas
219
+ Requires-Dist: scipy
219
220
  Requires-Dist: matplotlib
220
221
  Provides-Extra: dev
221
222
  Requires-Dist: pytest; extra == "dev"
222
223
  Requires-Dist: pytest-mock; extra == "dev"
223
- Requires-Dist: flake8; extra == "dev"
224
- Requires-Dist: black; extra == "dev"
225
- Requires-Dist: isort; extra == "dev"
224
+ Requires-Dist: ruff; extra == "dev"
225
+ Requires-Dist: mypy; extra == "dev"
226
226
  Requires-Dist: yfinance; extra == "dev"
227
227
  Requires-Dist: pyarrow; extra == "dev"
228
+ Dynamic: license-file
228
229
 
229
230
  # kissbt
230
231
 
@@ -264,7 +265,7 @@ pip install kissbt
264
265
  To install `kissbt` via `conda`, run the following command:
265
266
 
266
267
  ```sh
267
- conda install kissbt
268
+ conda install -c conda-forge kissbt
268
269
  ```
269
270
 
270
271
  ## Usage
@@ -12,6 +12,7 @@ kissbt.egg-info/SOURCES.txt
12
12
  kissbt.egg-info/dependency_links.txt
13
13
  kissbt.egg-info/requires.txt
14
14
  kissbt.egg-info/top_level.txt
15
+ tests/test_analyzer.py
15
16
  tests/test_broker.py
16
17
  tests/test_entities.py
17
18
  tests/test_integration.py
@@ -1,12 +1,12 @@
1
1
  numpy
2
2
  pandas
3
+ scipy
3
4
  matplotlib
4
5
 
5
6
  [dev]
6
7
  pytest
7
8
  pytest-mock
8
- flake8
9
- black
10
- isort
9
+ ruff
10
+ mypy
11
11
  yfinance
12
12
  pyarrow
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kissbt"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "The keep it simple backtesting framework for Python."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -22,6 +22,7 @@ classifiers = [
22
22
  dependencies = [
23
23
  "numpy",
24
24
  "pandas",
25
+ "scipy",
25
26
  "matplotlib"
26
27
  ]
27
28
  requires-python = ">=3.10"
@@ -30,9 +31,8 @@ requires-python = ">=3.10"
30
31
  dev = [
31
32
  "pytest",
32
33
  "pytest-mock",
33
- "flake8",
34
- "black",
35
- "isort",
34
+ "ruff",
35
+ "mypy",
36
36
  "yfinance",
37
37
  "pyarrow",
38
38
  ]
@@ -41,21 +41,39 @@ dev = [
41
41
  homepage = "https://github.com/FinBlobs/kissbt"
42
42
  bug-tracker = "https://github.com/FinBlobs/kissbt/issues"
43
43
 
44
- [tool.black]
44
+ [tool.ruff]
45
45
  line-length = 88
46
- target-version = ['py310']
47
- include = '\.pyi?$'
46
+ target-version = "py310"
47
+ src = ["kissbt", "tests"]
48
+ exclude = [
49
+ ".git",
50
+ "__pycache__",
51
+ "build",
52
+ "dist",
53
+ "examples",
54
+ ]
55
+
56
+ [tool.ruff.lint]
57
+ # Start with core rules equivalent to the original flake8 setup
58
+ select = [
59
+ "E", # pycodestyle errors
60
+ "W", # pycodestyle warnings
61
+ "F", # pyflakes
62
+ "I", # isort
63
+ ]
64
+ # TODO: Gradually add more rules like N, UP, C4, B
48
65
 
49
- [tool.isort]
50
- profile = "black"
51
- line_length = 88
52
- multi_line_output = 3
53
- include_trailing_comma = true
54
- force_grid_wrap = 0
55
- use_parentheses = true
56
- ensure_newline_before_comments = true
66
+ [tool.ruff.format]
67
+ # Enable Black-compatible formatting
68
+ quote-style = "double"
69
+ indent-style = "space"
70
+ skip-magic-trailing-comma = false
71
+ line-ending = "auto"
57
72
 
58
- [tool.flake8]
59
- max-line-length = 88
60
- extend-ignore = ["E203"]
61
- exclude = [".git", "__pycache__", "build", "dist"]
73
+ [tool.mypy]
74
+ python_version = "3.10"
75
+ # Start with basic type checking, will enable strict mode later
76
+ warn_return_any = false
77
+ warn_unused_configs = true
78
+ show_error_codes = true
79
+ ignore_missing_imports = true
@@ -0,0 +1,61 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import pytest
4
+ from kissbt.analyzer import Analyzer
5
+ from kissbt.broker import Broker
6
+
7
+
8
+ def test_constant_growth_benchmark_stats():
9
+ daily_return = 0.0001
10
+ num_days = 252
11
+ start_value = 100000.0
12
+ values = [start_value * (1 + daily_return) ** i for i in range(num_days)]
13
+
14
+ broker = Broker(benchmark="constant_growth")
15
+ for i, val in enumerate(values):
16
+ ts = pd.Timestamp("2023-01-01") + pd.Timedelta(days=i)
17
+ broker.history["timestamp"].append(ts)
18
+ broker.history["total_value"].append(start_value)
19
+ broker.history["benchmark"].append(val)
20
+ broker.history["cash"].append(0)
21
+ broker.history["long_position_value"].append(0)
22
+ broker.history["short_position_value"].append(0)
23
+ broker.history["positions"].append({})
24
+
25
+ analyzer = Analyzer(broker)
26
+ metrics = analyzer.get_performance_metrics()
27
+
28
+ assert abs(metrics["benchmark_slope"] - np.log(1 + daily_return)) < 1e-10
29
+ assert metrics["benchmark_slope_se"] < 1e-10
30
+ assert metrics["benchmark_slope_tstat"] > 1e5
31
+ assert metrics["benchmark_r_squared"] > 0.9999
32
+
33
+
34
+ def test_portfolio_equity_curve_stats_with_volatility():
35
+ np.random.seed(42)
36
+ num_days = 256 * 3
37
+ start_value = 100000.0
38
+
39
+ # Generate portfolio values with some volatility
40
+ daily_returns = np.random.normal(0.001, 0.02, num_days)
41
+ portfolio_values = [start_value]
42
+ for ret in daily_returns:
43
+ portfolio_values.append(portfolio_values[-1] * (1 + ret))
44
+
45
+ broker = Broker()
46
+ for i, val in enumerate(portfolio_values):
47
+ ts = pd.Timestamp("2023-01-01") + pd.Timedelta(days=i)
48
+ broker.history["timestamp"].append(ts)
49
+ broker.history["total_value"].append(val)
50
+ broker.history["cash"].append(0)
51
+ broker.history["long_position_value"].append(val)
52
+ broker.history["short_position_value"].append(0)
53
+ broker.history["positions"].append({})
54
+
55
+ analyzer = Analyzer(broker)
56
+ metrics = analyzer.get_performance_metrics()
57
+
58
+ assert metrics["slope"] == pytest.approx(0.001, abs=0.0005)
59
+ assert metrics["slope_se"] > 0
60
+ assert metrics["slope_tstat"] > 1.96
61
+ assert 0.5 < metrics["r_squared"] < 0.9
@@ -1,8 +1,5 @@
1
- from datetime import datetime
2
-
3
1
  import pandas as pd
4
2
  import pytest
5
-
6
3
  from kissbt.broker import Broker
7
4
  from kissbt.entities import OpenPosition, Order, OrderType
8
5
 
@@ -14,7 +11,7 @@ def test_initial_broker_state(broker):
14
11
  assert broker.closed_positions == []
15
12
  assert broker.history == {
16
13
  "cash": [],
17
- "date": [],
14
+ "timestamp": [],
18
15
  "long_position_value": [],
19
16
  "positions": [],
20
17
  "short_position_value": [],
@@ -35,7 +32,7 @@ def test_execute_order(broker, mocker):
35
32
  broker._execute_order(
36
33
  order,
37
34
  bar=pd.DataFrame({"open": [150.0], "close": [152.0]}, index=["AAPL"]),
38
- datetime=datetime.now(),
35
+ timestamp=pd.Timestamp.now(),
39
36
  )
40
37
  assert broker.cash == 100000 - (10 * 150 * 1.001)
41
38
  assert len(broker.open_positions) == 1
@@ -53,17 +50,17 @@ def test_update_open_positions(broker):
53
50
  assert broker._execute_order(
54
51
  order,
55
52
  bar=pd.DataFrame({"open": [150.0], "close": [152.0]}, index=["AAPL"]),
56
- datetime=datetime.now(),
53
+ timestamp=pd.Timestamp.now(),
57
54
  )
58
55
  assert len(broker.open_positions) == 1
59
56
  assert "AAPL" in broker.open_positions
60
57
 
61
58
 
62
59
  def test_update_closed_positions(broker):
63
- position = OpenPosition("AAPL", 10, 150.0, datetime.now())
60
+ position = OpenPosition("AAPL", 10, 150.0, pd.Timestamp.now())
64
61
  broker._open_positions["AAPL"] = position
65
62
  broker._update_closed_positions(
66
- position.ticker, -position.size, position.price, position.datetime
63
+ position.ticker, -position.size, position.price, position.timestamp
67
64
  )
68
65
  assert len(broker.closed_positions) == 1
69
66
  assert broker.closed_positions[0].ticker == "AAPL"
@@ -72,14 +69,14 @@ def test_update_closed_positions(broker):
72
69
  def test_liquidate_positions(broker):
73
70
  order = Order("AAPL", 10, OrderType.OPEN)
74
71
  bar = pd.DataFrame({"open": [150.0], "close": [152.0]}, index=["AAPL"])
75
- time = datetime.now()
72
+ time = pd.Timestamp.now()
76
73
  broker._execute_order(
77
74
  order,
78
75
  bar=bar,
79
- datetime=time,
76
+ timestamp=time,
80
77
  )
81
78
  broker._current_bar = bar
82
- broker._current_datetime = time
79
+ broker._current_timestamp = time
83
80
  broker.liquidate_positions()
84
81
 
85
82
  assert broker.open_positions == {}
@@ -87,14 +84,16 @@ def test_liquidate_positions(broker):
87
84
  assert broker.closed_positions[0].ticker == "AAPL"
88
85
  assert broker.closed_positions[0].size == 10
89
86
  assert broker.closed_positions[0].purchase_price == 150.0
90
- assert broker.closed_positions[0].purchase_datetime == time
87
+ assert broker.closed_positions[0].purchase_timestamp == time
91
88
  assert broker.closed_positions[0].selling_price == 152.0
92
- assert broker.closed_positions[0].selling_datetime == time
89
+ assert broker.closed_positions[0].selling_timestamp == time
93
90
 
94
91
 
95
92
  # --- Testing Portfolio Metrics ---
96
93
  def test_long_position_value(broker):
97
- broker._open_positions = {"AAPL": OpenPosition("AAPL", 10, 150.0, datetime.now())}
94
+ broker._open_positions = {
95
+ "AAPL": OpenPosition("AAPL", 10, 150.0, pd.Timestamp.now())
96
+ }
98
97
  broker._current_bar = pd.DataFrame(
99
98
  {"open": [150.0], "close": [152.0]}, index=["AAPL"]
100
99
  )
@@ -104,7 +103,9 @@ def test_long_position_value(broker):
104
103
 
105
104
 
106
105
  def test_short_position_value(broker):
107
- broker._open_positions = {"AAPL": OpenPosition("AAPL", -10, 150.0, datetime.now())}
106
+ broker._open_positions = {
107
+ "AAPL": OpenPosition("AAPL", -10, 150.0, pd.Timestamp.now())
108
+ }
108
109
  broker._current_bar = pd.DataFrame(
109
110
  {"open": [150.0], "close": [152.0]}, index=["AAPL"]
110
111
  )
@@ -118,7 +119,7 @@ def test_execute_order_open(broker, mocker):
118
119
  executed = broker._execute_order(
119
120
  order,
120
121
  bar=pd.DataFrame({"open": [150.0], "close": [152.0]}, index=["AAPL"]),
121
- datetime=datetime.now(),
122
+ timestamp=pd.Timestamp.now(),
122
123
  )
123
124
  assert executed
124
125
  assert broker.cash == 98498.5
@@ -129,12 +130,12 @@ def test_execute_order_open(broker, mocker):
129
130
  def test_execute_order_close(broker):
130
131
  order = Order("AAPL", -10, OrderType.CLOSE)
131
132
  bar = pd.DataFrame({"open": [150.0], "close": [152.0]}, index=["AAPL"])
132
- broker._open_positions["AAPL"] = OpenPosition("AAPL", 10, 150.0, datetime.now())
133
+ broker._open_positions["AAPL"] = OpenPosition("AAPL", 10, 150.0, pd.Timestamp.now())
133
134
  broker._current_bar = bar
134
135
  executed = broker._execute_order(
135
136
  order,
136
137
  bar=bar,
137
- datetime=datetime.now(),
138
+ timestamp=pd.Timestamp.now(),
138
139
  )
139
140
  assert executed
140
141
  assert float(broker.cash) == 100000 + 10 * 152 * 0.999
@@ -154,8 +155,8 @@ def test_place_multiple_orders(broker):
154
155
 
155
156
  def test_portfolio_value_with_positions(broker):
156
157
  broker._open_positions = {
157
- "AAPL": OpenPosition("AAPL", 10, 150.0, datetime.now()),
158
- "GOOG": OpenPosition("GOOG", -5, 1000.0, datetime.now()),
158
+ "AAPL": OpenPosition("AAPL", 10, 150.0, pd.Timestamp.now()),
159
+ "GOOG": OpenPosition("GOOG", -5, 1000.0, pd.Timestamp.now()),
159
160
  }
160
161
  broker._current_bar = pd.DataFrame(
161
162
  {"close": [152.0, 995.0]}, index=["AAPL", "GOOG"]
@@ -164,18 +165,18 @@ def test_portfolio_value_with_positions(broker):
164
165
 
165
166
 
166
167
  def test_update_history(broker):
167
- broker._current_datetime = datetime.now()
168
+ broker._current_timestamp = pd.Timestamp.now()
168
169
  broker._cash = 100000
169
170
  broker._open_positions = {
170
- "AAPL": OpenPosition("AAPL", 10, 150.0, datetime.now()),
171
- "GOOG": OpenPosition("GOOG", -5, 1000.0, datetime.now()),
171
+ "AAPL": OpenPosition("AAPL", 10, 150.0, pd.Timestamp.now()),
172
+ "GOOG": OpenPosition("GOOG", -5, 1000.0, pd.Timestamp.now()),
172
173
  }
173
174
  broker._current_bar = pd.DataFrame(
174
175
  {"close": [152.0, 995.0]}, index=["AAPL", "GOOG"]
175
176
  )
176
177
  broker._update_history()
177
178
  history = broker.history
178
- assert len(history["date"]) == 1
179
+ assert len(history["timestamp"]) == 1
179
180
  assert len(history["cash"]) == 1
180
181
  assert len(history["long_position_value"]) == 1
181
182
  assert len(history["short_position_value"]) == 1
@@ -227,9 +228,9 @@ def test_invalid_order_type(broker):
227
228
 
228
229
  def test_position_update(broker):
229
230
  broker._open_positions["AAPL"] = OpenPosition(
230
- ticker="AAPL", size=5, price=100, datetime=datetime(2024, 1, 1)
231
+ ticker="AAPL", size=5, price=100, timestamp=pd.Timestamp(2024, 1, 1)
231
232
  )
232
- broker._update_open_positions("AAPL", 10, 110, datetime(2024, 1, 2))
233
+ broker._update_open_positions("AAPL", 10, 110, pd.Timestamp(2024, 1, 2))
233
234
  assert broker._open_positions["AAPL"].size == 15
234
235
  assert broker._open_positions["AAPL"].price == pytest.approx(
235
236
  (5 * 100 + 10 * 110) / 15
@@ -240,11 +241,11 @@ def test_short_position_fees(broker, mocker):
240
241
  broker._long_only = False
241
242
  broker._current_bar = pd.DataFrame({"close": [100]}, index=["AAPL"])
242
243
  broker._open_positions["AAPL"] = OpenPosition(
243
- ticker="AAPL", size=-10, price=105, datetime=datetime(2024, 1, 1)
244
+ ticker="AAPL", size=-10, price=105, timestamp=pd.Timestamp(2024, 1, 1)
244
245
  )
245
246
  next_bar = pd.DataFrame({"close": [100]}, index=["AAPL"])
246
- next_datetime = pd.Timestamp("2024-01-02")
247
- broker.update(next_bar, next_datetime)
247
+ next_timestamp = pd.Timestamp("2024-01-02")
248
+ broker.update(next_bar, next_timestamp)
248
249
  assert broker.cash < 100000 # Short fee applied
249
250
 
250
251
 
@@ -291,15 +292,15 @@ def test_check_long_only_blocks_short_orders(broker):
291
292
  broker._long_only = True
292
293
  order = Order("AAPL", -10, OrderType.OPEN)
293
294
  with pytest.raises(ValueError):
294
- broker._check_long_only_condition(order, datetime.now())
295
+ broker._check_long_only_condition(order, pd.Timestamp.now())
295
296
 
296
297
 
297
298
  def test_check_long_only_condition(broker):
298
- time = datetime.now()
299
+ time = pd.Timestamp.now()
299
300
  broker._current_bar = pd.DataFrame(
300
301
  {"open": [150.0], "close": [152.0]}, index=["AAPL"]
301
302
  )
302
- broker._current_datetime = time
303
+ broker._current_timestamp = time
303
304
  order = Order("AAPL", -10, order_type=OrderType.OPEN)
304
305
  with pytest.raises(ValueError):
305
306
  broker._check_long_only_condition(order, time)
@@ -308,11 +309,11 @@ def test_check_long_only_condition(broker):
308
309
  def test_update_with_short_fees(broker):
309
310
  broker._long_only = False
310
311
  broker._open_positions["AAPL"] = OpenPosition(
311
- "AAPL", -10, 100, datetime(2024, 1, 1)
312
+ "AAPL", -10, 100, pd.Timestamp(2024, 1, 1)
312
313
  )
313
314
  next_bar = pd.DataFrame({"close": [100]}, index=["AAPL"])
314
- next_datetime = pd.Timestamp("2024-01-02")
315
- broker.update(next_bar, next_datetime)
315
+ next_timestamp = pd.Timestamp("2024-01-02")
316
+ broker.update(next_bar, next_timestamp)
316
317
  assert broker.cash < 100000 # Short fee applied
317
318
 
318
319
 
@@ -343,8 +344,10 @@ def test_update_cash(broker):
343
344
  def test_update_open_positions_cases(
344
345
  broker, size_change, expected_size, expected_price
345
346
  ):
346
- broker._open_positions["AAPL"] = OpenPosition("AAPL", 10, 100, datetime(2024, 1, 1))
347
- broker._update_open_positions("AAPL", size_change, 110, datetime(2024, 1, 2))
347
+ broker._open_positions["AAPL"] = OpenPosition(
348
+ "AAPL", 10, 100, pd.Timestamp(2024, 1, 1)
349
+ )
350
+ broker._update_open_positions("AAPL", size_change, 110, pd.Timestamp(2024, 1, 2))
348
351
 
349
352
  if expected_size is None:
350
353
  assert "AAPL" not in broker._open_positions
@@ -354,14 +357,18 @@ def test_update_open_positions_cases(
354
357
 
355
358
 
356
359
  def test_update_ticker_out_of_universe(broker):
357
- broker._open_positions["AAPL"] = OpenPosition("AAPL", 10, 100, datetime(2024, 1, 1))
360
+ broker._open_positions["AAPL"] = OpenPosition(
361
+ "AAPL", 10, 100, pd.Timestamp(2024, 1, 1)
362
+ )
358
363
 
359
364
  broker.update(
360
365
  pd.DataFrame({"close": [100, 500]}, index=["GOOG", "AAPL"]),
361
- datetime(2024, 1, 2),
366
+ pd.Timestamp(2024, 1, 2),
362
367
  )
363
368
 
364
- broker.update(pd.DataFrame({"close": [110]}, index=["GOOG"]), datetime(2024, 1, 3))
369
+ broker.update(
370
+ pd.DataFrame({"close": [110]}, index=["GOOG"]), pd.Timestamp(2024, 1, 3)
371
+ )
365
372
 
366
373
  assert len(broker._closed_positions) == 1
367
374
  closed_pos = broker._closed_positions[0]
@@ -369,5 +376,5 @@ def test_update_ticker_out_of_universe(broker):
369
376
  assert closed_pos.size == 10
370
377
  assert closed_pos.purchase_price == 100
371
378
  assert closed_pos.selling_price == 500
372
- assert closed_pos.purchase_datetime == datetime(2024, 1, 1)
373
- assert closed_pos.selling_datetime == datetime(2024, 1, 2)
379
+ assert closed_pos.purchase_timestamp == pd.Timestamp(2024, 1, 1)
380
+ assert closed_pos.selling_timestamp == pd.Timestamp(2024, 1, 2)
@@ -1,7 +1,4 @@
1
- from datetime import datetime
2
-
3
1
  import pandas as pd
4
-
5
2
  from kissbt.entities import OpenPosition, Order, OrderType
6
3
 
7
4
 
@@ -30,10 +27,10 @@ def test_order_defaults():
30
27
 
31
28
 
32
29
  def test_open_position_creation():
33
- entry_time = pd.Timestamp(datetime(2024, 1, 1, 10, 30, 0))
34
- position = OpenPosition(ticker="MSFT", size=50, price=250.0, datetime=entry_time)
30
+ entry_time = pd.Timestamp(2024, 1, 1, 10, 30, 0)
31
+ position = OpenPosition(ticker="MSFT", size=50, price=250.0, timestamp=entry_time)
35
32
 
36
33
  assert position.ticker == "MSFT"
37
34
  assert position.size == 50
38
35
  assert position.price == 250.0
39
- assert position.datetime == entry_time
36
+ assert position.timestamp == entry_time
@@ -1,6 +1,5 @@
1
1
  import pandas as pd
2
2
  import pytest
3
-
4
3
  from kissbt.analyzer import Analyzer
5
4
  from kissbt.broker import Broker
6
5
  from kissbt.engine import Engine
@@ -15,7 +14,7 @@ class GoldenCrossStrategy(Strategy):
15
14
  def generate_orders(
16
15
  self,
17
16
  current_data: pd.DataFrame,
18
- current_date: pd.Timestamp,
17
+ current_timestamp: pd.Timestamp,
19
18
  ) -> None:
20
19
  for ticker in self._broker.open_positions:
21
20
  if (
@@ -66,9 +65,9 @@ def test_analyzer_with_golden_cross(tech_stock_data):
66
65
  assert len(broker.open_positions) == 0, "All positions should be closed"
67
66
  assert pytest.approx(broker.cash, 0.01) == 167534.46, "Final cash manually verified"
68
67
  assert pytest.approx(broker.portfolio_value, 0.01) == 167534.46
69
- assert (
70
- len(broker.closed_positions) == 15
71
- ), "15 trades should have been executed, manually verified"
68
+ assert len(broker.closed_positions) == 15, (
69
+ "15 trades should have been executed, manually verified"
70
+ )
72
71
 
73
72
  # Create the Analyzer
74
73
  analyzer = Analyzer(broker, bar_size="1D")
@@ -99,9 +98,10 @@ def test_analyzer_with_golden_cross(tech_stock_data):
99
98
  assert pytest.approx(metrics["volatility"], abs=0.01) == 0.24
100
99
  assert pytest.approx(metrics["win_rate"], abs=0.01) == 0.47
101
100
  assert pytest.approx(metrics["profit_factor"], abs=0.01) == 3.17
102
- assert pytest.approx(metrics["total_benchmark_return"], abs=0.01) == 0.29
103
- assert pytest.approx(metrics["annual_benchmark_return"], abs=0.01) == 0.09
101
+ assert pytest.approx(metrics["benchmark_total_return"], abs=0.01) == 0.29
102
+ assert pytest.approx(metrics["benchmark_annual_return"], abs=0.01) == 0.09
104
103
 
105
104
  # Ensure running the plot functions does not raise an exception
106
105
  analyzer.plot_equity_curve()
107
106
  analyzer.plot_drawdowns()
107
+ analyzer.plot_rolling_returns_distribution(252)
@@ -1,6 +1,5 @@
1
1
  import pandas as pd
2
2
  import pytest
3
-
4
3
  from kissbt.broker import Broker
5
4
  from kissbt.entities import Order
6
5
  from kissbt.strategy import Strategy
@@ -8,7 +7,7 @@ from kissbt.strategy import Strategy
8
7
 
9
8
  class DummyStrategy(Strategy):
10
9
  def generate_orders(
11
- self, current_data: pd.DataFrame, current_datetime: pd.Timestamp
10
+ self, current_data: pd.DataFrame, current_timestamp: pd.Timestamp
12
11
  ) -> None:
13
12
  for ticker in current_data.index:
14
13
  if current_data.loc[ticker, "close"] > 100:
File without changes
File without changes
File without changes