openseries 1.9.6__py3-none-any.whl → 1.9.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openseries/_common_model.py +319 -161
- openseries/frame.py +41 -74
- openseries/owntypes.py +1 -1
- openseries/portfoliotools.py +13 -28
- openseries/py.typed +0 -0
- openseries/report.py +3 -2
- openseries/series.py +18 -29
- openseries/simulation.py +94 -22
- {openseries-1.9.6.dist-info → openseries-1.9.7.dist-info}/METADATA +14 -18
- openseries-1.9.7.dist-info/RECORD +18 -0
- {openseries-1.9.6.dist-info → openseries-1.9.7.dist-info}/WHEEL +1 -1
- openseries-1.9.6.dist-info/RECORD +0 -17
- {openseries-1.9.6.dist-info → openseries-1.9.7.dist-info}/licenses/LICENSE.md +0 -0
openseries/frame.py
CHANGED
@@ -16,10 +16,12 @@ from typing import TYPE_CHECKING, Any, cast
|
|
16
16
|
|
17
17
|
from numpy import (
|
18
18
|
array,
|
19
|
+
asarray,
|
19
20
|
concatenate,
|
20
21
|
cov,
|
21
22
|
diff,
|
22
23
|
divide,
|
24
|
+
float64,
|
23
25
|
isinf,
|
24
26
|
log,
|
25
27
|
nan,
|
@@ -39,6 +41,7 @@ from pandas import (
|
|
39
41
|
if TYPE_CHECKING: # pragma: no cover
|
40
42
|
import datetime as dt
|
41
43
|
|
44
|
+
from numpy.typing import NDArray
|
42
45
|
from pandas import Series as _Series
|
43
46
|
from pandas import Timestamp
|
44
47
|
|
@@ -49,7 +52,7 @@ else:
|
|
49
52
|
from pydantic import field_validator
|
50
53
|
from sklearn.linear_model import LinearRegression # type: ignore[import-untyped]
|
51
54
|
|
52
|
-
from ._common_model import _CommonModel
|
55
|
+
from ._common_model import _calculate_time_factor, _CommonModel, _get_base_column_data
|
53
56
|
from .datefixer import _do_resample_to_business_period_ends
|
54
57
|
from .owntypes import (
|
55
58
|
DaysInYearType,
|
@@ -77,7 +80,6 @@ logger = getLogger(__name__)
|
|
77
80
|
__all__ = ["OpenFrame"]
|
78
81
|
|
79
82
|
|
80
|
-
# noinspection PyUnresolvedReferences,PyTypeChecker
|
81
83
|
class OpenFrame(_CommonModel[SeriesFloat]):
|
82
84
|
"""OpenFrame objects hold OpenTimeSeries in the list constituents.
|
83
85
|
|
@@ -101,7 +103,6 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
101
103
|
tsdf: DataFrame = DataFrame(dtype="float64")
|
102
104
|
weights: list[float] | None = None
|
103
105
|
|
104
|
-
# noinspection PyMethodParameters
|
105
106
|
@field_validator("constituents")
|
106
107
|
def _check_labels_unique(
|
107
108
|
cls: type[OpenFrame], # noqa: N805
|
@@ -150,10 +151,12 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
150
151
|
def _set_tsdf(self: Self) -> None:
|
151
152
|
"""Set the tsdf DataFrame."""
|
152
153
|
if self.constituents is not None and len(self.constituents) != 0:
|
153
|
-
self.
|
154
|
-
|
155
|
-
|
156
|
-
|
154
|
+
if len(self.constituents) == 1:
|
155
|
+
self.tsdf = self.constituents[0].tsdf.copy()
|
156
|
+
else:
|
157
|
+
self.tsdf = concat(
|
158
|
+
[x.tsdf for x in self.constituents], axis="columns", sort=True
|
159
|
+
)
|
157
160
|
else:
|
158
161
|
logger.warning("OpenFrame() was passed an empty list.")
|
159
162
|
|
@@ -610,13 +613,12 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
610
613
|
.std(ddof=dlta_degr_freedms)
|
611
614
|
* sqrt(time_factor),
|
612
615
|
]
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
]
|
616
|
+
rm = data[(cols[0], ValueType.RTRN)].iloc[1:day_chunk]
|
617
|
+
m: NDArray[float64] = asarray(rm, dtype=float64)
|
618
|
+
ry = data[(cols[1], ValueType.RTRN)].iloc[1:day_chunk]
|
619
|
+
y: NDArray[float64] = asarray(ry, dtype=float64)
|
620
|
+
|
621
|
+
raw_cov = [cov(m=m, y=y, ddof=dlta_degr_freedms)[0][1]]
|
620
622
|
|
621
623
|
r1 = data[(cols[0], ValueType.RTRN)]
|
622
624
|
r2 = data[(cols[1], ValueType.RTRN)]
|
@@ -855,36 +857,20 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
855
857
|
from_dt=from_date,
|
856
858
|
to_dt=to_date,
|
857
859
|
)
|
858
|
-
fraction: float = (later - earlier).days / 365.25
|
859
860
|
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
short_label = cast(
|
867
|
-
"tuple[str, ValueType]",
|
868
|
-
self.tsdf[base_column].name,
|
869
|
-
)[0]
|
870
|
-
elif isinstance(base_column, int):
|
871
|
-
shortdf = self.tsdf.loc[
|
872
|
-
cast("Timestamp", earlier) : cast("Timestamp", later)
|
873
|
-
].iloc[:, base_column]
|
874
|
-
short_item = cast(
|
875
|
-
"tuple[str, ValueType]",
|
876
|
-
self.tsdf.iloc[:, base_column].name,
|
877
|
-
)
|
878
|
-
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
879
|
-
0
|
880
|
-
]
|
881
|
-
else:
|
882
|
-
raise TypeError(msg)
|
861
|
+
shortdf, short_item, short_label = _get_base_column_data(
|
862
|
+
self=self,
|
863
|
+
base_column=base_column,
|
864
|
+
earlier=earlier,
|
865
|
+
later=later,
|
866
|
+
)
|
883
867
|
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
868
|
+
time_factor = _calculate_time_factor(
|
869
|
+
data=shortdf,
|
870
|
+
earlier=earlier,
|
871
|
+
later=later,
|
872
|
+
periods_in_a_year_fixed=periods_in_a_year_fixed,
|
873
|
+
)
|
888
874
|
|
889
875
|
terrors = []
|
890
876
|
for item in self.tsdf:
|
@@ -946,39 +932,20 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
946
932
|
from_dt=from_date,
|
947
933
|
to_dt=to_date,
|
948
934
|
)
|
949
|
-
fraction: float = (later - earlier).days / 365.25
|
950
935
|
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
short_label = cast(
|
958
|
-
"tuple[str, str]",
|
959
|
-
self.tsdf[base_column].name,
|
960
|
-
)[0]
|
961
|
-
elif isinstance(base_column, int):
|
962
|
-
shortdf = self.tsdf.loc[
|
963
|
-
cast("Timestamp", earlier) : cast("Timestamp", later)
|
964
|
-
].iloc[:, base_column]
|
965
|
-
short_item = cast(
|
966
|
-
"tuple[str, ValueType]",
|
967
|
-
self.tsdf.iloc[
|
968
|
-
:,
|
969
|
-
base_column,
|
970
|
-
].name,
|
971
|
-
)
|
972
|
-
short_label = cast("tuple[str, str]", self.tsdf.iloc[:, base_column].name)[
|
973
|
-
0
|
974
|
-
]
|
975
|
-
else:
|
976
|
-
raise TypeError(msg)
|
936
|
+
shortdf, short_item, short_label = _get_base_column_data(
|
937
|
+
self=self,
|
938
|
+
base_column=base_column,
|
939
|
+
earlier=earlier,
|
940
|
+
later=later,
|
941
|
+
)
|
977
942
|
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
943
|
+
time_factor = _calculate_time_factor(
|
944
|
+
data=shortdf,
|
945
|
+
earlier=earlier,
|
946
|
+
later=later,
|
947
|
+
periods_in_a_year_fixed=periods_in_a_year_fixed,
|
948
|
+
)
|
982
949
|
|
983
950
|
ratios = []
|
984
951
|
for item in self.tsdf:
|
@@ -1451,7 +1418,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
|
|
1451
1418
|
returns = self.tsdf.ffill().pct_change()
|
1452
1419
|
returns.iloc[0] = 0
|
1453
1420
|
elif all(vtypes):
|
1454
|
-
returns = self.tsdf
|
1421
|
+
returns = self.tsdf
|
1455
1422
|
else:
|
1456
1423
|
msg = "Mix of series types will give inconsistent results"
|
1457
1424
|
raise MixedValuetypesError(msg)
|
openseries/owntypes.py
CHANGED
@@ -35,7 +35,7 @@ except ImportError: # pragma: no cover
|
|
35
35
|
__all__ = ["Self", "ValueType"]
|
36
36
|
|
37
37
|
|
38
|
-
|
38
|
+
SeriesOrFloat_co = TypeVar("SeriesOrFloat_co", float, SeriesFloat, covariant=True)
|
39
39
|
|
40
40
|
|
41
41
|
CountryStringType = Annotated[
|
openseries/portfoliotools.py
CHANGED
@@ -16,13 +16,13 @@ from typing import TYPE_CHECKING, cast
|
|
16
16
|
from numpy import (
|
17
17
|
append,
|
18
18
|
array,
|
19
|
+
einsum,
|
19
20
|
float64,
|
20
21
|
inf,
|
21
22
|
isnan,
|
22
23
|
linspace,
|
23
24
|
nan,
|
24
25
|
sqrt,
|
25
|
-
zeros,
|
26
26
|
)
|
27
27
|
from numpy import (
|
28
28
|
sum as npsum,
|
@@ -48,12 +48,9 @@ from .owntypes import (
|
|
48
48
|
ValueType,
|
49
49
|
)
|
50
50
|
from .series import OpenTimeSeries
|
51
|
-
|
52
|
-
# noinspection PyProtectedMember
|
53
51
|
from .simulation import _random_generator
|
54
52
|
|
55
53
|
if TYPE_CHECKING: # pragma: no cover
|
56
|
-
# noinspection PyUnresolvedReferences
|
57
54
|
from collections.abc import Callable
|
58
55
|
|
59
56
|
from numpy.typing import NDArray
|
@@ -106,25 +103,16 @@ def simulate_portfolios(
|
|
106
103
|
|
107
104
|
log_ret.columns = log_ret.columns.droplevel(level=1)
|
108
105
|
|
109
|
-
|
110
|
-
|
111
|
-
all_weights = zeros((num_ports, simframe.item_count))
|
112
|
-
ret_arr = zeros(num_ports)
|
113
|
-
vol_arr = zeros(num_ports)
|
114
|
-
sharpe_arr = zeros(num_ports)
|
115
|
-
|
116
|
-
for x in range(num_ports):
|
117
|
-
weights = array(randomizer.random(simframe.item_count))
|
118
|
-
weights = weights / npsum(weights)
|
119
|
-
all_weights[x, :] = weights
|
106
|
+
cov_matrix = log_ret.cov() * simframe.periods_in_a_year
|
107
|
+
mean_returns = log_ret.mean() * simframe.periods_in_a_year
|
120
108
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
|
109
|
+
randomizer = _random_generator(seed=seed)
|
110
|
+
all_weights = randomizer.random((num_ports, simframe.item_count))
|
111
|
+
all_weights = all_weights / all_weights.sum(axis=1, keepdims=True)
|
126
112
|
|
127
|
-
|
113
|
+
ret_arr = all_weights @ mean_returns
|
114
|
+
vol_arr = sqrt(einsum("ij,jk,ik->i", all_weights, cov_matrix, all_weights))
|
115
|
+
sharpe_arr = ret_arr / vol_arr
|
128
116
|
|
129
117
|
simdf = concat(
|
130
118
|
[
|
@@ -137,7 +125,6 @@ def simulate_portfolios(
|
|
137
125
|
return simdf.dropna()
|
138
126
|
|
139
127
|
|
140
|
-
# noinspection PyUnusedLocal
|
141
128
|
def efficient_frontier(
|
142
129
|
eframe: OpenFrame,
|
143
130
|
num_ports: int = 5000,
|
@@ -230,7 +217,7 @@ def efficient_frontier(
|
|
230
217
|
_get_ret_vol_sr(
|
231
218
|
lg_ret=log_ret,
|
232
219
|
weights=weights,
|
233
|
-
per_in_yr=
|
220
|
+
per_in_yr=copi.periods_in_a_year,
|
234
221
|
)[2]
|
235
222
|
* -1,
|
236
223
|
)
|
@@ -243,7 +230,7 @@ def efficient_frontier(
|
|
243
230
|
_get_ret_vol_sr(
|
244
231
|
lg_ret=log_ret,
|
245
232
|
weights=weights,
|
246
|
-
per_in_yr=
|
233
|
+
per_in_yr=copi.periods_in_a_year,
|
247
234
|
)[1],
|
248
235
|
)
|
249
236
|
|
@@ -263,7 +250,7 @@ def efficient_frontier(
|
|
263
250
|
optimal = _get_ret_vol_sr(
|
264
251
|
lg_ret=log_ret,
|
265
252
|
weights=opt_results.x,
|
266
|
-
per_in_yr=
|
253
|
+
per_in_yr=copi.periods_in_a_year,
|
267
254
|
)
|
268
255
|
|
269
256
|
frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
|
@@ -280,7 +267,7 @@ def efficient_frontier(
|
|
280
267
|
"fun": lambda w, poss_return=possible_return: _diff_return(
|
281
268
|
lg_ret=log_ret,
|
282
269
|
weights=w,
|
283
|
-
per_in_yr=
|
270
|
+
per_in_yr=copi.periods_in_a_year,
|
284
271
|
poss_return=poss_return,
|
285
272
|
),
|
286
273
|
},
|
@@ -375,7 +362,6 @@ def constrain_optimized_portfolios(
|
|
375
362
|
)
|
376
363
|
|
377
364
|
condition_least_ret = front_frame.ret > serie.arithmetic_ret
|
378
|
-
# noinspection PyArgumentList
|
379
365
|
least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
|
380
366
|
least_ret_port: Series[float] = least_ret_frame.iloc[0]
|
381
367
|
least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
|
@@ -386,7 +372,6 @@ def constrain_optimized_portfolios(
|
|
386
372
|
resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
|
387
373
|
|
388
374
|
condition_most_vol = front_frame.stdev < serie.vol
|
389
|
-
# noinspection PyArgumentList
|
390
375
|
most_vol_frame = front_frame[condition_most_vol].sort_values(
|
391
376
|
by="ret",
|
392
377
|
ascending=False,
|
openseries/py.typed
ADDED
File without changes
|
openseries/report.py
CHANGED
@@ -264,7 +264,6 @@ def report_html(
|
|
264
264
|
"{:.2f}",
|
265
265
|
]
|
266
266
|
|
267
|
-
# noinspection PyTypeChecker
|
268
267
|
rpt_df = copied.all_properties(
|
269
268
|
properties=cast("list[LiteralFrameProps]", properties),
|
270
269
|
)
|
@@ -353,7 +352,9 @@ def report_html(
|
|
353
352
|
|
354
353
|
for item, f in zip(rpt_df.index, formats, strict=False):
|
355
354
|
rpt_df.loc[item] = rpt_df.loc[item].apply(
|
356
|
-
lambda x, fmt=f:
|
355
|
+
lambda x, fmt=f: str(x)
|
356
|
+
if (isinstance(x, str) or x is None)
|
357
|
+
else fmt.format(x),
|
357
358
|
)
|
358
359
|
|
359
360
|
rpt_df.index = Index(labels_init)
|
openseries/series.py
CHANGED
@@ -15,7 +15,6 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
15
15
|
|
16
16
|
if TYPE_CHECKING: # pragma: no cover
|
17
17
|
import datetime as dt
|
18
|
-
from collections.abc import Callable
|
19
18
|
|
20
19
|
from numpy.typing import NDArray
|
21
20
|
from pandas import Timestamp
|
@@ -41,7 +40,7 @@ from pandas import (
|
|
41
40
|
)
|
42
41
|
from pydantic import field_validator, model_validator
|
43
42
|
|
44
|
-
from ._common_model import _CommonModel
|
43
|
+
from ._common_model import _calculate_time_factor, _CommonModel
|
45
44
|
from .datefixer import _do_resample_to_business_period_ends, date_fix
|
46
45
|
from .owntypes import (
|
47
46
|
Countries,
|
@@ -63,9 +62,6 @@ from .owntypes import (
|
|
63
62
|
ValueType,
|
64
63
|
)
|
65
64
|
|
66
|
-
FieldValidator = cast("Callable[..., Callable[..., Any]]", field_validator)
|
67
|
-
ModelValidator = cast("Callable[..., Callable[..., Any]]", model_validator)
|
68
|
-
|
69
65
|
logger = getLogger(__name__)
|
70
66
|
|
71
67
|
__all__ = ["OpenTimeSeries", "timeseries_chain"]
|
@@ -73,7 +69,6 @@ __all__ = ["OpenTimeSeries", "timeseries_chain"]
|
|
73
69
|
TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
|
74
70
|
|
75
71
|
|
76
|
-
# noinspection PyUnresolvedReferences,PyNestedDecorators
|
77
72
|
class OpenTimeSeries(_CommonModel[float]):
|
78
73
|
"""OpenTimeSeries objects are at the core of the openseries package.
|
79
74
|
|
@@ -130,21 +125,21 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
130
125
|
isin: str | None = None
|
131
126
|
label: str | None = None
|
132
127
|
|
133
|
-
@
|
128
|
+
@field_validator("domestic", mode="before")
|
134
129
|
@classmethod
|
135
130
|
def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
|
136
131
|
"""Pydantic validator to ensure domestic field is validated."""
|
137
132
|
_ = Currency(ccy=value)
|
138
133
|
return value
|
139
134
|
|
140
|
-
@
|
135
|
+
@field_validator("countries", mode="before")
|
141
136
|
@classmethod
|
142
137
|
def _validate_countries(cls, value: CountriesType) -> CountriesType:
|
143
138
|
"""Pydantic validator to ensure countries field is validated."""
|
144
139
|
_ = Countries(countryinput=value)
|
145
140
|
return value
|
146
141
|
|
147
|
-
@
|
142
|
+
@field_validator("markets", mode="before")
|
148
143
|
@classmethod
|
149
144
|
def _validate_markets(
|
150
145
|
cls,
|
@@ -164,7 +159,7 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
164
159
|
raise MarketsNotStringNorListStrError(item_msg)
|
165
160
|
raise MarketsNotStringNorListStrError(msg)
|
166
161
|
|
167
|
-
@
|
162
|
+
@model_validator(mode="after")
|
168
163
|
def _dates_and_values_validate(self: Self) -> Self:
|
169
164
|
"""Pydantic validator to ensure dates and values are validated."""
|
170
165
|
values_list_length = len(self.values)
|
@@ -282,7 +277,7 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
282
277
|
label, _ = dframe.name
|
283
278
|
else:
|
284
279
|
label = dframe.name
|
285
|
-
values =
|
280
|
+
values = dframe.to_numpy().tolist()
|
286
281
|
elif isinstance(dframe, DataFrame):
|
287
282
|
values = dframe.iloc[:, column_nmbr].to_list()
|
288
283
|
if isinstance(dframe.columns, MultiIndex):
|
@@ -301,12 +296,11 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
301
296
|
msg = f"valuetype missing. Adding: {valuetype.value}"
|
302
297
|
logger.warning(msg=msg)
|
303
298
|
else:
|
304
|
-
valuetype =
|
305
|
-
|
306
|
-
|
307
|
-
)
|
299
|
+
valuetype = dframe.columns.get_level_values(1).to_numpy()[
|
300
|
+
column_nmbr
|
301
|
+
]
|
308
302
|
else:
|
309
|
-
label =
|
303
|
+
label = dframe.columns.to_numpy()[column_nmbr]
|
310
304
|
else:
|
311
305
|
raise TypeError(msg)
|
312
306
|
|
@@ -472,7 +466,6 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
472
466
|
The returns of the values in the series
|
473
467
|
|
474
468
|
"""
|
475
|
-
# noinspection PyCallingNonCallable
|
476
469
|
returns = self.tsdf.ffill().pct_change()
|
477
470
|
returns.iloc[0] = 0
|
478
471
|
self.valuetype = ValueType.RTRN
|
@@ -684,16 +677,14 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
684
677
|
from_dt=from_date,
|
685
678
|
to_dt=to_date,
|
686
679
|
)
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
fraction = (later - earlier).days / 365.25
|
696
|
-
time_factor = how_many / fraction
|
680
|
+
time_factor = _calculate_time_factor(
|
681
|
+
data=self.tsdf.loc[
|
682
|
+
cast("Timestamp", earlier) : cast("Timestamp", later)
|
683
|
+
].iloc[:, 0],
|
684
|
+
earlier=earlier,
|
685
|
+
later=later,
|
686
|
+
periods_in_a_year_fixed=periods_in_a_year_fixed,
|
687
|
+
)
|
697
688
|
|
698
689
|
data = self.tsdf.loc[
|
699
690
|
cast("Timestamp", earlier) : cast("Timestamp", later)
|
@@ -757,7 +748,6 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
757
748
|
ra_df = ra_df.dropna()
|
758
749
|
|
759
750
|
prev = self.first_idx
|
760
|
-
# noinspection PyTypeChecker
|
761
751
|
dates: list[dt.date] = [prev]
|
762
752
|
|
763
753
|
for idx, row in ra_df.iterrows():
|
@@ -878,7 +868,6 @@ def timeseries_chain(
|
|
878
868
|
|
879
869
|
dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
|
880
870
|
|
881
|
-
# noinspection PyTypeChecker
|
882
871
|
return back.__class__(
|
883
872
|
timeseries_id=new.timeseries_id,
|
884
873
|
instrument_id=new.instrument_id,
|
openseries/simulation.py
CHANGED
@@ -9,7 +9,13 @@ SPDX-License-Identifier: BSD-3-Clause
|
|
9
9
|
|
10
10
|
from __future__ import annotations
|
11
11
|
|
12
|
-
from
|
12
|
+
from functools import cached_property
|
13
|
+
from typing import TYPE_CHECKING, TypedDict, cast
|
14
|
+
|
15
|
+
try:
|
16
|
+
from typing import Unpack
|
17
|
+
except ImportError:
|
18
|
+
from typing_extensions import Unpack
|
13
19
|
|
14
20
|
if TYPE_CHECKING:
|
15
21
|
import datetime as dt # pragma: no cover
|
@@ -41,6 +47,14 @@ from .owntypes import (
|
|
41
47
|
__all__ = ["ReturnSimulation"]
|
42
48
|
|
43
49
|
|
50
|
+
class _JumpParams(TypedDict, total=False):
|
51
|
+
"""TypedDict for jump diffusion parameters."""
|
52
|
+
|
53
|
+
jumps_lamda: NonNegativeFloat
|
54
|
+
jumps_sigma: NonNegativeFloat
|
55
|
+
jumps_mu: float
|
56
|
+
|
57
|
+
|
44
58
|
def _random_generator(seed: int | None) -> Generator:
|
45
59
|
"""Make a Numpy Random Generator object.
|
46
60
|
|
@@ -60,6 +74,58 @@ def _random_generator(seed: int | None) -> Generator:
|
|
60
74
|
return Generator(bit_generator=bg)
|
61
75
|
|
62
76
|
|
77
|
+
def _create_base_simulation(
|
78
|
+
cls: type[ReturnSimulation],
|
79
|
+
returns: DataFrame,
|
80
|
+
number_of_sims: PositiveInt,
|
81
|
+
trading_days: PositiveInt,
|
82
|
+
trading_days_in_year: DaysInYearType,
|
83
|
+
mean_annual_return: float,
|
84
|
+
mean_annual_vol: PositiveFloat,
|
85
|
+
seed: int | None = None,
|
86
|
+
**kwargs: Unpack[_JumpParams],
|
87
|
+
) -> ReturnSimulation:
|
88
|
+
"""Common logic for creating simulations.
|
89
|
+
|
90
|
+
Parameters
|
91
|
+
----------
|
92
|
+
cls: type[ReturnSimulation]
|
93
|
+
The ReturnSimulation class
|
94
|
+
returns: pandas.DataFrame
|
95
|
+
The calculated returns data
|
96
|
+
number_of_sims: PositiveInt
|
97
|
+
Number of simulations to generate
|
98
|
+
trading_days: PositiveInt
|
99
|
+
Number of trading days to simulate
|
100
|
+
trading_days_in_year: DaysInYearType
|
101
|
+
Number of trading days used to annualize
|
102
|
+
mean_annual_return: float
|
103
|
+
Mean annual return
|
104
|
+
mean_annual_vol: PositiveFloat
|
105
|
+
Mean annual volatility
|
106
|
+
seed: int, optional
|
107
|
+
Seed for random process initiation
|
108
|
+
**kwargs
|
109
|
+
Additional keyword arguments for jump parameters
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
-------
|
113
|
+
ReturnSimulation
|
114
|
+
A ReturnSimulation instance
|
115
|
+
|
116
|
+
"""
|
117
|
+
return cls(
|
118
|
+
number_of_sims=number_of_sims,
|
119
|
+
trading_days=trading_days,
|
120
|
+
trading_days_in_year=trading_days_in_year,
|
121
|
+
mean_annual_return=mean_annual_return,
|
122
|
+
mean_annual_vol=mean_annual_vol,
|
123
|
+
dframe=returns,
|
124
|
+
seed=seed,
|
125
|
+
**kwargs,
|
126
|
+
)
|
127
|
+
|
128
|
+
|
63
129
|
class ReturnSimulation(BaseModel):
|
64
130
|
"""The class ReturnSimulation allows for simulating financial timeseries.
|
65
131
|
|
@@ -105,7 +171,7 @@ class ReturnSimulation(BaseModel):
|
|
105
171
|
revalidate_instances="always",
|
106
172
|
)
|
107
173
|
|
108
|
-
@
|
174
|
+
@cached_property
|
109
175
|
def results(self: Self) -> DataFrame:
|
110
176
|
"""Simulation data.
|
111
177
|
|
@@ -197,13 +263,14 @@ class ReturnSimulation(BaseModel):
|
|
197
263
|
size=(number_of_sims, trading_days),
|
198
264
|
)
|
199
265
|
|
200
|
-
return
|
266
|
+
return _create_base_simulation(
|
267
|
+
cls=cls,
|
268
|
+
returns=DataFrame(data=returns, dtype="float64"),
|
201
269
|
number_of_sims=number_of_sims,
|
202
270
|
trading_days=trading_days,
|
203
271
|
trading_days_in_year=trading_days_in_year,
|
204
272
|
mean_annual_return=mean_annual_return,
|
205
273
|
mean_annual_vol=mean_annual_vol,
|
206
|
-
dframe=DataFrame(data=returns, dtype="float64"),
|
207
274
|
seed=seed,
|
208
275
|
)
|
209
276
|
|
@@ -255,13 +322,14 @@ class ReturnSimulation(BaseModel):
|
|
255
322
|
- 1
|
256
323
|
)
|
257
324
|
|
258
|
-
return
|
325
|
+
return _create_base_simulation(
|
326
|
+
cls=cls,
|
327
|
+
returns=DataFrame(data=returns, dtype="float64"),
|
259
328
|
number_of_sims=number_of_sims,
|
260
329
|
trading_days=trading_days,
|
261
330
|
trading_days_in_year=trading_days_in_year,
|
262
331
|
mean_annual_return=mean_annual_return,
|
263
332
|
mean_annual_vol=mean_annual_vol,
|
264
|
-
dframe=DataFrame(data=returns, dtype="float64"),
|
265
333
|
seed=seed,
|
266
334
|
)
|
267
335
|
|
@@ -317,13 +385,14 @@ class ReturnSimulation(BaseModel):
|
|
317
385
|
|
318
386
|
returns = drift + wiener
|
319
387
|
|
320
|
-
return
|
388
|
+
return _create_base_simulation(
|
389
|
+
cls=cls,
|
390
|
+
returns=DataFrame(data=returns, dtype="float64"),
|
321
391
|
number_of_sims=number_of_sims,
|
322
392
|
trading_days=trading_days,
|
323
393
|
trading_days_in_year=trading_days_in_year,
|
324
394
|
mean_annual_return=mean_annual_return,
|
325
395
|
mean_annual_vol=mean_annual_vol,
|
326
|
-
dframe=DataFrame(data=returns, dtype="float64"),
|
327
396
|
seed=seed,
|
328
397
|
)
|
329
398
|
|
@@ -404,17 +473,18 @@ class ReturnSimulation(BaseModel):
|
|
404
473
|
|
405
474
|
returns[:, 0] = 0.0
|
406
475
|
|
407
|
-
return
|
476
|
+
return _create_base_simulation(
|
477
|
+
cls=cls,
|
478
|
+
returns=DataFrame(data=returns, dtype="float64"),
|
408
479
|
number_of_sims=number_of_sims,
|
409
480
|
trading_days=trading_days,
|
410
481
|
trading_days_in_year=trading_days_in_year,
|
411
482
|
mean_annual_return=mean_annual_return,
|
412
483
|
mean_annual_vol=mean_annual_vol,
|
484
|
+
seed=seed,
|
413
485
|
jumps_lamda=jumps_lamda,
|
414
486
|
jumps_sigma=jumps_sigma,
|
415
487
|
jumps_mu=jumps_mu,
|
416
|
-
dframe=DataFrame(data=returns, dtype="float64"),
|
417
|
-
seed=seed,
|
418
488
|
)
|
419
489
|
|
420
490
|
def to_dataframe(
|
@@ -465,15 +535,17 @@ class ReturnSimulation(BaseModel):
|
|
465
535
|
)
|
466
536
|
return sdf
|
467
537
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
538
|
+
df_list = [
|
539
|
+
DataFrame(
|
540
|
+
data=self.dframe.iloc[item].values,
|
541
|
+
index=Index(d_range),
|
542
|
+
columns=MultiIndex.from_arrays(
|
543
|
+
[
|
544
|
+
[f"{name}_{item}"],
|
545
|
+
[ValueType.RTRN],
|
546
|
+
],
|
547
|
+
),
|
477
548
|
)
|
478
|
-
|
479
|
-
|
549
|
+
for item in range(self.number_of_sims)
|
550
|
+
]
|
551
|
+
return concat(df_list, axis="columns", sort=True)
|