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.
- {openseries-2.1.6 → openseries-2.1.7}/PKG-INFO +2 -1
- {openseries-2.1.6 → openseries-2.1.7}/openseries/_common_model.py +86 -1
- {openseries-2.1.6 → openseries-2.1.7}/openseries/frame.py +1 -2
- {openseries-2.1.6 → openseries-2.1.7}/openseries/owntypes.py +6 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/series.py +159 -3
- {openseries-2.1.6 → openseries-2.1.7}/openseries/simulation.py +74 -18
- {openseries-2.1.6 → openseries-2.1.7}/pyproject.toml +4 -3
- {openseries-2.1.6 → openseries-2.1.7}/LICENSE.md +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/README.md +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/__init__.py +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/_risk.py +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/datefixer.py +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/html_utils.py +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/load_plotly.py +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/plotly_captor_logo.json +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/plotly_layouts.json +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/portfoliotools.py +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/py.typed +0 -0
- {openseries-2.1.6 → openseries-2.1.7}/openseries/report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openseries
|
|
3
|
-
Version: 2.1.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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=
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
439
|
+
raw_returns = poisson_jumps + drift + wiener
|
|
440
|
+
raw_returns[:, 0] = 0.0
|
|
386
441
|
|
|
387
|
-
|
|
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=
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|