openseries 2.1.6__tar.gz → 2.1.7__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
1
  Metadata-Version: 2.4
2
2
  Name: openseries
3
- Version: 2.1.6
3
+ Version: 2.1.7
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE.md
@@ -31,6 +31,7 @@ Requires-Dist: python-dateutil (>=2.8.2)
31
31
  Requires-Dist: requests (>=2.20.0)
32
32
  Requires-Dist: scikit-learn (>=1.4.0)
33
33
  Requires-Dist: scipy (>=1.14.1)
34
+ Requires-Dist: tzdata (>=2025.3)
34
35
  Project-URL: Documentation, https://openseries.readthedocs.io/
35
36
  Project-URL: Homepage, https://captorab.github.io/openseries/
36
37
  Project-URL: Issue Tracker, https://github.com/CaptorAB/openseries/issues
@@ -162,6 +162,30 @@ def _get_base_column_data(
162
162
  return data, item, label
163
163
 
164
164
 
165
+ def _demeaned_returns_for_autocorr(
166
+ series: Series[float], valuetype: ValueType, *, squared: bool = False
167
+ ) -> Series[float]:
168
+ """Return demeaned return series for autocorrelation analysis.
169
+
170
+ Args:
171
+ series: Input series (prices or returns).
172
+ valuetype: ValueType.PRICE for price data (pct_change applied),
173
+ else use as returns.
174
+ squared: If True, square the demeaned returns.
175
+
176
+ Returns:
177
+ Demeaned return series (optionally squared).
178
+ """
179
+ if valuetype == ValueType.PRICE:
180
+ rets = series.ffill().pct_change().dropna()
181
+ else:
182
+ rets = series.ffill().dropna()
183
+ rets = rets - rets.mean()
184
+ if squared:
185
+ rets = rets**2
186
+ return rets
187
+
188
+
165
189
  def _calculate_time_factor(
166
190
  data: Series[float],
167
191
  earlier: dt.date,
@@ -354,6 +378,23 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
354
378
  """
355
379
  return self.vol_func()
356
380
 
381
+ @property
382
+ def autocorr(self: Self) -> SeriesOrFloat_co:
383
+ """Autocorrelation at lag 1.
384
+
385
+ Shorthand for ``autocorr_func(lag=1)``. Returns the lag-1 autocorrelation
386
+ of demeaned returns. For price series, returns are computed via
387
+ ``pct_change``; for return series, raw values are used after demeaning.
388
+
389
+ Returns:
390
+ --------
391
+ SeriesOrFloat_co
392
+ Autocorrelation at lag 1.
393
+ Returns float for OpenTimeSeries, Series[float] for OpenFrame.
394
+
395
+ """
396
+ return self.autocorr_func()
397
+
357
398
  @property
358
399
  def downside_deviation(self: Self) -> SeriesOrFloat_co:
359
400
  """Downside Deviation.
@@ -824,7 +865,7 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
824
865
  else None,
825
866
  )
826
867
  ]
827
- self.tsdf = self.tsdf.reindex(labels=d_range, method=method, copy=False)
868
+ self.tsdf = self.tsdf.reindex(labels=d_range, method=method)
828
869
 
829
870
  return self
830
871
 
@@ -1482,6 +1523,50 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
1482
1523
 
1483
1524
  return self._coerce_result(result=result, name="Volatility")
1484
1525
 
1526
+ def autocorr_func(
1527
+ self: Self,
1528
+ lag: int = 1,
1529
+ *,
1530
+ squared: bool = False,
1531
+ ) -> SeriesOrFloat_co:
1532
+ """Calculate autocorrelation at a given lag.
1533
+
1534
+ Computes the autocorrelation of demeaned returns at the specified lag.
1535
+ For price series (ValueType.PRICE), returns are derived via ``pct_change``;
1536
+ for return series (ValueType.RTRN), raw values are demeaned. Use
1537
+ ``squared=True`` for squared-return autocorrelation (e.g. volatility
1538
+ clustering). Returns ``nan`` when the series has too few observations.
1539
+
1540
+ Args:
1541
+ lag: The lag at which to compute autocorrelation. Defaults to 1.
1542
+ squared: If True, compute autocorrelation of squared returns.
1543
+ Defaults to False.
1544
+
1545
+ Returns:
1546
+ Autocorrelation at the specified lag. Float for OpenTimeSeries,
1547
+ ``Series[float]`` for OpenFrame.
1548
+ """
1549
+ values: list[float] = []
1550
+ vtypes = self.tsdf.columns.get_level_values(1)
1551
+ for col_idx, col in enumerate(self.tsdf.columns):
1552
+ valuetype = cast("ValueType", vtypes[col_idx])
1553
+ rets = _demeaned_returns_for_autocorr(
1554
+ series=self.tsdf[col],
1555
+ valuetype=valuetype,
1556
+ squared=squared,
1557
+ )
1558
+ if len(rets) > lag:
1559
+ values.append(float(rets.autocorr(lag=lag)))
1560
+ else:
1561
+ values.append(float("nan"))
1562
+ result = Series(
1563
+ data=values,
1564
+ index=self.tsdf.columns,
1565
+ name="Autocorrelation",
1566
+ dtype="float64",
1567
+ )
1568
+ return self._coerce_result(result=result, name="Autocorrelation")
1569
+
1485
1570
  def vol_from_var_func(
1486
1571
  self: Self,
1487
1572
  level: float = 0.95,
@@ -663,13 +663,12 @@ class OpenFrame(_CommonModel[SeriesFloat]):
663
663
  if not end_cut and where in ["after", "both"]:
664
664
  end_cut = self.last_indices.min()
665
665
  self.tsdf = self.tsdf.sort_index()
666
- self.tsdf = self.tsdf.truncate(before=start_cut, after=end_cut, copy=False)
666
+ self.tsdf = self.tsdf.truncate(before=start_cut, after=end_cut)
667
667
 
668
668
  for xerie in self.constituents:
669
669
  xerie.tsdf = xerie.tsdf.truncate(
670
670
  before=start_cut,
671
671
  after=end_cut,
672
- copy=False,
673
672
  )
674
673
  if len(set(self.first_indices)) != 1:
675
674
  msg = (
@@ -184,11 +184,14 @@ LiteralSeriesProps = Literal[
184
184
  "span_of_days",
185
185
  "yearfrac",
186
186
  "periods_in_a_year",
187
+ "autocorr",
188
+ "partial_autocorr",
187
189
  ]
188
190
  LiteralFrameProps = Literal[
189
191
  "value_ret",
190
192
  "geo_ret",
191
193
  "arithmetic_ret",
194
+ "autocorr",
192
195
  "vol",
193
196
  "downside_deviation",
194
197
  "ret_vol_ratio",
@@ -273,6 +276,8 @@ class OpenTimeSeriesPropertiesList(PropertiesList):
273
276
  "span_of_days",
274
277
  "yearfrac",
275
278
  "periods_in_a_year",
279
+ "autocorr",
280
+ "partial_autocorr",
276
281
  }
277
282
 
278
283
  def __init__(
@@ -288,6 +293,7 @@ class OpenFramePropertiesList(PropertiesList):
288
293
  """Allowed property arguments for the OpenFrame class."""
289
294
 
290
295
  allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
296
+ "autocorr",
291
297
  "first_indices",
292
298
  "last_indices",
293
299
  "lengths_of_items",
@@ -32,9 +32,13 @@ from pandas import (
32
32
  date_range,
33
33
  )
34
34
  from pydantic import field_validator, model_validator
35
- from scipy.stats import norm
35
+ from scipy.stats import chi2, norm
36
36
 
37
- from ._common_model import _calculate_time_factor, _CommonModel
37
+ from ._common_model import (
38
+ _calculate_time_factor,
39
+ _CommonModel,
40
+ _demeaned_returns_for_autocorr,
41
+ )
38
42
  from .datefixer import _do_resample_to_business_period_ends, date_fix
39
43
  from .owntypes import (
40
44
  Countries,
@@ -416,7 +420,18 @@ class OpenTimeSeries(_CommonModel[float]):
416
420
  )
417
421
 
418
422
  props = OpenTimeSeriesPropertiesList(*properties)
419
- pdf = DataFrame.from_dict({x: getattr(self, x) for x in props}, orient="index")
423
+
424
+ def _prop_value(name: str) -> float | int | dt.date | Series[float]:
425
+ attr = getattr(self, name)
426
+ return cast(
427
+ "float | int | dt.date | Series[float]",
428
+ attr() if callable(attr) else attr,
429
+ )
430
+
431
+ pdf = DataFrame.from_dict(
432
+ {x: _prop_value(x) for x in props},
433
+ orient="index",
434
+ )
420
435
  pdf.columns = self.tsdf.columns
421
436
  return pdf
422
437
 
@@ -824,6 +839,147 @@ class OpenTimeSeries(_CommonModel[float]):
824
839
  self.tsdf.columns = self.tsdf.columns.droplevel(level=1)
825
840
  return self
826
841
 
842
+ def _returns_series(self: Self, *, squared: bool = False) -> Series[float]:
843
+ """Return demeaned return series for autocorrelation analysis."""
844
+ data: Series[float] = self.tsdf.iloc[:, 0]
845
+ return _demeaned_returns_for_autocorr(
846
+ series=data, valuetype=self.valuetype, squared=squared
847
+ )
848
+
849
+ def acf(
850
+ self: Self,
851
+ lags: int | list[int],
852
+ *,
853
+ squared: bool = False,
854
+ ) -> Series[float]:
855
+ """Calculate autocorrelation function for specified lags.
856
+
857
+ Args:
858
+ lags: If int, compute ACF from lag 0 to this value (inclusive).
859
+ If list, compute ACF at lag 0 plus each lag in the list.
860
+ squared: If True, compute ACF of squared returns. Defaults to False.
861
+
862
+ Returns:
863
+ Series of autocorrelations indexed by lag.
864
+ """
865
+ rets = self._returns_series(squared=squared)
866
+ if isinstance(lags, int):
867
+ lag_list = list(range(lags + 1))
868
+ else:
869
+ lag_list = sorted({0} | set(lags))
870
+ values: list[float] = []
871
+ for lag in lag_list:
872
+ if lag == 0:
873
+ values.append(1.0)
874
+ else:
875
+ values.append(float(rets.autocorr(lag=lag)))
876
+ return Series(
877
+ data=values,
878
+ index=lag_list,
879
+ name="ACF",
880
+ dtype="float64",
881
+ )
882
+
883
+ def partial_autocorr(self: Self, lag: int = 1, *, squared: bool = False) -> float:
884
+ """Calculate partial autocorrelation at a given lag.
885
+
886
+ Args:
887
+ lag: The lag at which to compute partial autocorrelation. Defaults to 1.
888
+ squared: If True, compute partial autocorrelation of squared returns.
889
+ Defaults to False.
890
+
891
+ Returns:
892
+ Partial autocorrelation at the specified lag.
893
+ """
894
+ pacf_series = self.pacf(lags=lag, squared=squared)
895
+ return float(pacf_series.loc[lag])
896
+
897
+ def pacf(
898
+ self: Self,
899
+ lags: int | list[int],
900
+ *,
901
+ squared: bool = False,
902
+ ) -> Series[float]:
903
+ """Calculate partial autocorrelation function for specified lags.
904
+
905
+ Args:
906
+ lags: If int, compute PACF from lag 0 to this value (inclusive).
907
+ If list, compute PACF at lag 0 plus each lag in the list.
908
+ squared: If True, compute PACF of squared returns. Defaults to False.
909
+
910
+ Returns:
911
+ Series of partial autocorrelations indexed by lag.
912
+ """
913
+ if isinstance(lags, int):
914
+ lag_list = list(range(lags + 1))
915
+ else:
916
+ lag_list = sorted({0} | set(lags))
917
+ max_lag = max(lag_list) if lag_list else 0
918
+ acf_vals = self.acf(lags=max_lag, squared=squared)
919
+ acf_arr = array([acf_vals.loc[k] for k in range(max_lag + 1)])
920
+ pacf_values: list[float] = [1.0]
921
+ phi: list[list[float]] = []
922
+ for k in range(1, max_lag + 1):
923
+ if k == 1:
924
+ phi_kk = acf_arr[1]
925
+ else:
926
+ numer = acf_arr[k]
927
+ denom = 1.0
928
+ for j in range(k - 1):
929
+ numer -= phi[k - 2][j] * acf_arr[k - 1 - j]
930
+ denom -= phi[k - 2][j] * acf_arr[j + 1]
931
+ phi_kk = numer / denom
932
+ phi_row = [0.0] * k
933
+ for j in range(k - 1):
934
+ phi_row[j] = phi[k - 2][j] - phi_kk * phi[k - 2][k - 2 - j]
935
+ phi_row[k - 1] = phi_kk
936
+ phi.append(phi_row)
937
+ pacf_values.append(phi_kk)
938
+ result = {lag: pacf_values[lag] for lag in lag_list}
939
+ return Series(
940
+ data=[result[lag] for lag in lag_list],
941
+ index=lag_list,
942
+ name="PACF",
943
+ dtype="float64",
944
+ )
945
+
946
+ def ljung_box(
947
+ self: Self,
948
+ lags: int | list[int],
949
+ *,
950
+ squared: bool = False,
951
+ ) -> tuple[float, float, list[int]]:
952
+ """Compute Ljung-Box test for autocorrelation.
953
+
954
+ Args:
955
+ lags: If int, use lags 1 through this value. If list, use the given
956
+ lags (lag 0 excluded from test).
957
+ squared: If True, test autocorrelation of squared returns.
958
+ Defaults to False.
959
+
960
+ Returns:
961
+ Tuple of (statistic, pvalue, lags) where statistic is the Ljung-Box
962
+ Q statistic, pvalue is the chi-squared p-value, and lags is the
963
+ list of lags used.
964
+ """
965
+ rets = self._returns_series(squared=squared)
966
+ n = len(rets)
967
+ if isinstance(lags, int):
968
+ lag_list = list(range(1, lags + 1))
969
+ else:
970
+ lag_list = sorted({k for k in lags if k > 0})
971
+ if not lag_list:
972
+ return 0.0, 1.0, []
973
+ r_k_sq_sum = 0.0
974
+ for k in lag_list:
975
+ if k < n:
976
+ r_k = float(rets.autocorr(lag=k))
977
+ r_k_sq_sum += r_k**2 / (n - k)
978
+ q_stat = n * (n + 2) * r_k_sq_sum
979
+ df = len(lag_list)
980
+ pval = float(1.0 - chi2.cdf(q_stat, df))
981
+ return q_stat, pval, lag_list
982
+
827
983
 
828
984
  def timeseries_chain(
829
985
  front: TypeOpenTimeSeries,
@@ -47,6 +47,35 @@ class _JumpParams(TypedDict, total=False):
47
47
  jumps_mu: float
48
48
 
49
49
 
50
+ def _validate_ar1_coef(ar1_coef: float) -> None:
51
+ """Validate ar1_coef is in (-1, 1) for stationarity."""
52
+ if not -1.0 < ar1_coef < 1.0:
53
+ msg = f"ar1_coef must be in (-1, 1) for stationarity, got {ar1_coef}"
54
+ raise ValueError(msg)
55
+
56
+
57
+ def _apply_ar1_filter(returns: DataFrame, ar1_coef: float) -> DataFrame:
58
+ """Apply AR(1) filter to returns to introduce lag-1 autocorrelation.
59
+
60
+ r_t = ar1_coef * r_{t-1} + sqrt(1 - ar1_coef**2) * innovation_t
61
+ Preserves mean and variance of the base process.
62
+
63
+ Args:
64
+ returns: DataFrame of shape (number_of_sims, trading_days).
65
+ ar1_coef: Lag-1 autocorrelation coefficient in (-1, 1).
66
+
67
+ Returns:
68
+ Filtered returns.
69
+ """
70
+ if ar1_coef == 0.0:
71
+ return returns
72
+ arr = returns.to_numpy(copy=True)
73
+ scale = sqrt(1.0 - ar1_coef * ar1_coef)
74
+ for t in range(1, arr.shape[1]):
75
+ arr[:, t] = ar1_coef * arr[:, t - 1] + scale * arr[:, t]
76
+ return DataFrame(data=arr, dtype="float64")
77
+
78
+
50
79
  def _random_generator(seed: int | None) -> Generator:
51
80
  """Make a Numpy Random Generator object.
52
81
 
@@ -183,6 +212,7 @@ class ReturnSimulation(BaseModel):
183
212
  trading_days_in_year: DaysInYearType = 252,
184
213
  seed: int | None = None,
185
214
  randomizer: Generator | None = None,
215
+ ar1_coef: float = 0.0,
186
216
  ) -> ReturnSimulation:
187
217
  """Create a Normal distribution simulation.
188
218
 
@@ -195,22 +225,29 @@ class ReturnSimulation(BaseModel):
195
225
  Defaults to 252.
196
226
  seed: Seed for random process initiation.
197
227
  randomizer: Random process generator.
228
+ ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
229
+ autocorrelation. Defaults to 0.0 (i.i.d. returns).
198
230
 
199
231
  Returns:
200
232
  Normal distribution simulation.
201
233
  """
234
+ _validate_ar1_coef(ar1_coef)
202
235
  if not randomizer:
203
236
  randomizer = _random_generator(seed=seed)
204
237
 
205
- returns = randomizer.normal(
206
- loc=mean_annual_return / trading_days_in_year,
207
- scale=mean_annual_vol / sqrt(trading_days_in_year),
208
- size=(number_of_sims, trading_days),
238
+ returns_df = DataFrame(
239
+ data=randomizer.normal(
240
+ loc=mean_annual_return / trading_days_in_year,
241
+ scale=mean_annual_vol / sqrt(trading_days_in_year),
242
+ size=(number_of_sims, trading_days),
243
+ ),
244
+ dtype="float64",
209
245
  )
246
+ returns = _apply_ar1_filter(returns_df, ar1_coef)
210
247
 
211
248
  return _create_base_simulation(
212
249
  cls=cls,
213
- returns=DataFrame(data=returns, dtype="float64"),
250
+ returns=returns,
214
251
  number_of_sims=number_of_sims,
215
252
  trading_days=trading_days,
216
253
  trading_days_in_year=trading_days_in_year,
@@ -229,6 +266,7 @@ class ReturnSimulation(BaseModel):
229
266
  trading_days_in_year: DaysInYearType = 252,
230
267
  seed: int | None = None,
231
268
  randomizer: Generator | None = None,
269
+ ar1_coef: float = 0.0,
232
270
  ) -> ReturnSimulation:
233
271
  """Create a Lognormal distribution simulation.
234
272
 
@@ -241,25 +279,32 @@ class ReturnSimulation(BaseModel):
241
279
  Defaults to 252.
242
280
  seed: Seed for random process initiation.
243
281
  randomizer: Random process generator.
282
+ ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
283
+ autocorrelation. Defaults to 0.0 (i.i.d. returns).
244
284
 
245
285
  Returns:
246
286
  Lognormal distribution simulation.
247
287
  """
288
+ _validate_ar1_coef(ar1_coef)
248
289
  if not randomizer:
249
290
  randomizer = _random_generator(seed=seed)
250
291
 
251
- returns = (
252
- randomizer.lognormal(
253
- mean=mean_annual_return / trading_days_in_year,
254
- sigma=mean_annual_vol / sqrt(trading_days_in_year),
255
- size=(number_of_sims, trading_days),
256
- )
257
- - 1
292
+ returns_df = DataFrame(
293
+ data=(
294
+ randomizer.lognormal(
295
+ mean=mean_annual_return / trading_days_in_year,
296
+ sigma=mean_annual_vol / sqrt(trading_days_in_year),
297
+ size=(number_of_sims, trading_days),
298
+ )
299
+ - 1
300
+ ),
301
+ dtype="float64",
258
302
  )
303
+ returns = _apply_ar1_filter(returns_df, ar1_coef)
259
304
 
260
305
  return _create_base_simulation(
261
306
  cls=cls,
262
- returns=DataFrame(data=returns, dtype="float64"),
307
+ returns=returns,
263
308
  number_of_sims=number_of_sims,
264
309
  trading_days=trading_days,
265
310
  trading_days_in_year=trading_days_in_year,
@@ -278,6 +323,7 @@ class ReturnSimulation(BaseModel):
278
323
  trading_days_in_year: DaysInYearType = 252,
279
324
  seed: int | None = None,
280
325
  randomizer: Generator | None = None,
326
+ ar1_coef: float = 0.0,
281
327
  ) -> ReturnSimulation:
282
328
  """Create a Geometric Brownian Motion simulation.
283
329
 
@@ -290,10 +336,13 @@ class ReturnSimulation(BaseModel):
290
336
  Defaults to 252.
291
337
  seed: Seed for random process initiation.
292
338
  randomizer: Random process generator.
339
+ ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
340
+ autocorrelation. Defaults to 0.0 (i.i.d. returns).
293
341
 
294
342
  Returns:
295
343
  Geometric Brownian Motion simulation.
296
344
  """
345
+ _validate_ar1_coef(ar1_coef)
297
346
  if not randomizer:
298
347
  randomizer = _random_generator(seed=seed)
299
348
 
@@ -308,11 +357,12 @@ class ReturnSimulation(BaseModel):
308
357
  size=(number_of_sims, trading_days),
309
358
  )
310
359
 
311
- returns = drift + wiener
360
+ returns_df = DataFrame(data=drift + wiener, dtype="float64")
361
+ returns = _apply_ar1_filter(returns_df, ar1_coef)
312
362
 
313
363
  return _create_base_simulation(
314
364
  cls=cls,
315
- returns=DataFrame(data=returns, dtype="float64"),
365
+ returns=returns,
316
366
  number_of_sims=number_of_sims,
317
367
  trading_days=trading_days,
318
368
  trading_days_in_year=trading_days_in_year,
@@ -334,6 +384,7 @@ class ReturnSimulation(BaseModel):
334
384
  trading_days_in_year: DaysInYearType = 252,
335
385
  seed: int | None = None,
336
386
  randomizer: Generator | None = None,
387
+ ar1_coef: float = 0.0,
337
388
  ) -> ReturnSimulation:
338
389
  """Create a Merton Jump-Diffusion model simulation.
339
390
 
@@ -350,10 +401,13 @@ class ReturnSimulation(BaseModel):
350
401
  Defaults to 252.
351
402
  seed: Seed for random process initiation.
352
403
  randomizer: Random process generator.
404
+ ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
405
+ autocorrelation. Defaults to 0.0 (i.i.d. returns).
353
406
 
354
407
  Returns:
355
408
  Merton Jump-Diffusion model simulation.
356
409
  """
410
+ _validate_ar1_coef(ar1_coef)
357
411
  if not randomizer:
358
412
  randomizer = _random_generator(seed=seed)
359
413
 
@@ -382,13 +436,15 @@ class ReturnSimulation(BaseModel):
382
436
  - jumps_lamda * (jumps_mu + jumps_sigma**2.0)
383
437
  ) * (1.0 / trading_days_in_year)
384
438
 
385
- returns = poisson_jumps + drift + wiener
439
+ raw_returns = poisson_jumps + drift + wiener
440
+ raw_returns[:, 0] = 0.0
386
441
 
387
- returns[:, 0] = 0.0
442
+ returns_df = DataFrame(data=raw_returns, dtype="float64")
443
+ returns = _apply_ar1_filter(returns_df, ar1_coef)
388
444
 
389
445
  return _create_base_simulation(
390
446
  cls=cls,
391
- returns=DataFrame(data=returns, dtype="float64"),
447
+ returns=returns,
392
448
  number_of_sims=number_of_sims,
393
449
  trading_days=trading_days,
394
450
  trading_days_in_year=trading_days_in_year,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openseries"
3
- version = "2.1.6"
3
+ version = "2.1.7"
4
4
  description = "Tools for analyzing financial timeseries."
5
5
  authors = [
6
6
  { name = "Martin Karrin", email = "martin.karrin@captor.se" },
@@ -50,7 +50,8 @@ dependencies = [
50
50
  "python-dateutil>=2.8.2",
51
51
  "requests>=2.20.0",
52
52
  "scipy>=1.14.1",
53
- "scikit-learn>=1.4.0"
53
+ "scikit-learn>=1.4.0",
54
+ "tzdata (>=2025.3)"
54
55
  ]
55
56
 
56
57
  [project.urls]
@@ -67,7 +68,7 @@ pre-commit = ">=4.5.1"
67
68
  pytest = ">=9.0.2"
68
69
  pytest-cov = ">=7.0.0"
69
70
  pytest-xdist = ">=3.8.0"
70
- ruff = "0.15.0"
71
+ ruff = "0.15.6"
71
72
  types-openpyxl = ">=3.1.2"
72
73
  scipy-stubs = ">=1.14.1.0"
73
74
  types-python-dateutil = ">=2.8.2"
File without changes
File without changes