openseries 1.5.6__py3-none-any.whl → 1.6.0__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.
@@ -0,0 +1,613 @@
1
+ """Defining the portfolio tools for the OpenFrame class."""
2
+
3
+ # mypy: disable-error-code="index,assignment"
4
+ from __future__ import annotations
5
+
6
+ from inspect import stack
7
+ from pathlib import Path
8
+ from typing import Callable, Optional, Union, cast
9
+
10
+ from numpy import (
11
+ append,
12
+ array,
13
+ dot,
14
+ float64,
15
+ inf,
16
+ linspace,
17
+ nan,
18
+ sqrt,
19
+ zeros,
20
+ )
21
+ from numpy import (
22
+ sum as npsum,
23
+ )
24
+ from numpy.typing import NDArray
25
+ from pandas import (
26
+ DataFrame,
27
+ Series,
28
+ concat,
29
+ )
30
+ from plotly.graph_objs import Figure # type: ignore[import-untyped,unused-ignore]
31
+ from plotly.io import to_html # type: ignore[import-untyped,unused-ignore]
32
+ from plotly.offline import plot # type: ignore[import-untyped,unused-ignore]
33
+ from pydantic import DirectoryPath
34
+ from scipy.optimize import minimize # type: ignore[import-untyped,unused-ignore]
35
+
36
+ # noinspection PyProtectedMember
37
+ from openseries.frame import OpenFrame
38
+ from openseries.load_plotly import load_plotly_dict
39
+ from openseries.series import OpenTimeSeries
40
+ from openseries.simulation import _random_generator
41
+ from openseries.types import (
42
+ LiteralLinePlotMode,
43
+ LiteralMinimizeMethods,
44
+ LiteralPlotlyJSlib,
45
+ LiteralPlotlyOutput,
46
+ ValueType,
47
+ )
48
+
49
+ __all__ = [
50
+ "constrain_optimized_portfolios",
51
+ "efficient_frontier",
52
+ "prepare_plot_data",
53
+ "sharpeplot",
54
+ "simulate_portfolios",
55
+ ]
56
+
57
+
58
+ def simulate_portfolios(
59
+ simframe: OpenFrame,
60
+ num_ports: int,
61
+ seed: int,
62
+ ) -> DataFrame:
63
+ """
64
+ Generate random weights for simulated portfolios.
65
+
66
+ Parameters
67
+ ----------
68
+ simframe: OpenFrame
69
+ Return data for portfolio constituents
70
+ num_ports: int
71
+ Number of possible portfolios to simulate
72
+ seed: int
73
+ The seed for the random process
74
+
75
+ Returns
76
+ -------
77
+ pandas.DataFrame
78
+ The resulting data
79
+
80
+ """
81
+ copi = simframe.from_deepcopy()
82
+
83
+ if any(
84
+ x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
85
+ ):
86
+ copi.value_to_ret()
87
+ log_ret = copi.tsdf.copy()[1:]
88
+ else:
89
+ log_ret = copi.tsdf.copy()
90
+
91
+ log_ret.columns = log_ret.columns.droplevel(level=1)
92
+
93
+ randomizer = _random_generator(seed=seed)
94
+
95
+ all_weights = zeros((num_ports, simframe.item_count))
96
+ ret_arr = zeros(num_ports)
97
+ vol_arr = zeros(num_ports)
98
+ sharpe_arr = zeros(num_ports)
99
+
100
+ for x in range(num_ports):
101
+ weights = array(randomizer.random(simframe.item_count))
102
+ weights = weights / npsum(weights)
103
+ all_weights[x, :] = weights
104
+
105
+ vol_arr[x] = sqrt(
106
+ dot(
107
+ weights.T,
108
+ dot(log_ret.cov() * simframe.periods_in_a_year, weights),
109
+ ),
110
+ )
111
+
112
+ ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
113
+
114
+ sharpe_arr[x] = ret_arr[x] / vol_arr[x]
115
+
116
+ # noinspection PyUnreachableCode
117
+ simdf = concat(
118
+ [
119
+ DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
120
+ DataFrame(all_weights, columns=simframe.columns_lvl_zero),
121
+ ],
122
+ axis="columns",
123
+ )
124
+ simdf = simdf.replace([inf, -inf], nan)
125
+ return simdf.dropna()
126
+
127
+
128
+ # noinspection PyUnusedLocal
129
+ def efficient_frontier( # noqa: C901
130
+ eframe: OpenFrame,
131
+ num_ports: int = 5000,
132
+ seed: int = 71,
133
+ bounds: Optional[tuple[tuple[float]]] = None,
134
+ frontier_points: int = 200,
135
+ minimize_method: LiteralMinimizeMethods = "SLSQP",
136
+ *,
137
+ tweak: bool = True,
138
+ ) -> tuple[DataFrame, DataFrame, NDArray[float64]]:
139
+ """
140
+ Identify an efficient frontier.
141
+
142
+ Parameters
143
+ ----------
144
+ eframe: OpenFrame
145
+ Portfolio data
146
+ num_ports: int, default: 5000
147
+ Number of possible portfolios to simulate
148
+ seed: int, default: 71
149
+ The seed for the random process
150
+ bounds: tuple[tuple[float]], optional
151
+ The range of minumum and maximum allowed allocations for each asset
152
+ frontier_points: int, default: 200
153
+ number of points along frontier to optimize
154
+ minimize_method: LiteralMinimizeMethods, default: SLSQP
155
+ The method passed into the scipy.minimize function
156
+ tweak: bool, default: True
157
+ cutting the frontier to exclude multiple points with almost the same risk
158
+
159
+ Returns
160
+ -------
161
+ tuple[DataFrame, DataFrame, NDArray[float]]
162
+ The efficient frontier data, simulation data and optimal portfolio
163
+
164
+ """
165
+ if eframe.weights is None:
166
+ eframe.weights = [1.0 / eframe.item_count] * eframe.item_count
167
+
168
+ copi = eframe.from_deepcopy()
169
+
170
+ if any(
171
+ x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
172
+ ):
173
+ copi.value_to_ret()
174
+ log_ret = copi.tsdf.copy()[1:]
175
+ else:
176
+ log_ret = copi.tsdf.copy()
177
+
178
+ log_ret.columns = log_ret.columns.droplevel(level=1)
179
+
180
+ simulated = simulate_portfolios(simframe=copi, num_ports=num_ports, seed=seed)
181
+
182
+ frontier_min = simulated.loc[simulated["stdev"].idxmin()]["ret"]
183
+ arithmetic_mean = log_ret.mean() * copi.periods_in_a_year
184
+ frontier_max = 0.0
185
+ if isinstance(arithmetic_mean, Series):
186
+ frontier_max = arithmetic_mean.max()
187
+
188
+ def _check_sum(weights: NDArray[float64]) -> float64:
189
+ return cast(float64, npsum(weights) - 1)
190
+
191
+ def _get_ret_vol_sr(
192
+ lg_ret: DataFrame,
193
+ weights: NDArray[float64],
194
+ per_in_yr: float,
195
+ ) -> NDArray[float64]:
196
+ ret = npsum(lg_ret.mean() * weights) * per_in_yr
197
+ volatility = sqrt(dot(weights.T, dot(lg_ret.cov() * per_in_yr, weights)))
198
+ sr = ret / volatility
199
+ return cast(NDArray[float64], array([ret, volatility, sr]))
200
+
201
+ def _diff_return(
202
+ lg_ret: DataFrame,
203
+ weights: NDArray[float64],
204
+ per_in_yr: float,
205
+ poss_return: float,
206
+ ) -> float64:
207
+ return cast(
208
+ float64,
209
+ _get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
210
+ - poss_return,
211
+ )
212
+
213
+ def _neg_sharpe(weights: NDArray[float64]) -> float64:
214
+ return cast(
215
+ float64,
216
+ _get_ret_vol_sr(
217
+ lg_ret=log_ret,
218
+ weights=weights,
219
+ per_in_yr=eframe.periods_in_a_year,
220
+ )[2]
221
+ * -1,
222
+ )
223
+
224
+ def _minimize_volatility(
225
+ weights: NDArray[float64],
226
+ ) -> float64:
227
+ return cast(
228
+ float64,
229
+ _get_ret_vol_sr(
230
+ lg_ret=log_ret,
231
+ weights=weights,
232
+ per_in_yr=eframe.periods_in_a_year,
233
+ )[1],
234
+ )
235
+
236
+ constraints = {"type": "eq", "fun": _check_sum}
237
+ if not bounds:
238
+ bounds = tuple((0.0, 1.0) for _ in range(eframe.item_count))
239
+ init_guess = array(eframe.weights)
240
+
241
+ opt_results = minimize(
242
+ fun=_neg_sharpe,
243
+ x0=init_guess,
244
+ method=minimize_method,
245
+ bounds=bounds,
246
+ constraints=constraints,
247
+ )
248
+
249
+ optimal = _get_ret_vol_sr(
250
+ lg_ret=log_ret,
251
+ weights=opt_results.x,
252
+ per_in_yr=eframe.periods_in_a_year,
253
+ )
254
+
255
+ frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
256
+ frontier_x = []
257
+ frontier_weights = []
258
+
259
+ for possible_return in frontier_y:
260
+ cons = cast(
261
+ dict[str, Union[str, Callable[[float, NDArray[float64]], float64]]],
262
+ (
263
+ {"type": "eq", "fun": _check_sum},
264
+ {
265
+ "type": "eq",
266
+ "fun": lambda w, poss_return=possible_return: _diff_return(
267
+ lg_ret=log_ret,
268
+ weights=w,
269
+ per_in_yr=eframe.periods_in_a_year,
270
+ poss_return=poss_return,
271
+ ),
272
+ },
273
+ ),
274
+ )
275
+
276
+ result = minimize(
277
+ fun=_minimize_volatility,
278
+ x0=init_guess,
279
+ method=minimize_method,
280
+ bounds=bounds,
281
+ constraints=cons,
282
+ )
283
+
284
+ frontier_x.append(result["fun"])
285
+ frontier_weights.append(result["x"])
286
+
287
+ # noinspection PyUnreachableCode
288
+ line_df = concat(
289
+ [
290
+ DataFrame(data=frontier_weights, columns=eframe.columns_lvl_zero),
291
+ DataFrame({"stdev": frontier_x, "ret": frontier_y}),
292
+ ],
293
+ axis="columns",
294
+ )
295
+ line_df["sharpe"] = line_df.ret / line_df.stdev
296
+
297
+ limit_small = 0.0001
298
+ line_df = line_df.mask(line_df.abs() < limit_small, 0.0)
299
+ line_df["text"] = line_df.apply(
300
+ lambda c: "<br><br>Weights:<br>"
301
+ + "<br>".join(
302
+ [f"{c[nm]:.1%} {nm}" for nm in eframe.columns_lvl_zero],
303
+ ),
304
+ axis="columns",
305
+ )
306
+
307
+ if tweak:
308
+ limit_tweak = 0.001
309
+ line_df["stdev_diff"] = line_df.stdev.pct_change()
310
+ line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
311
+ line_df = line_df.drop(columns="stdev_diff")
312
+
313
+ return line_df, simulated, append(optimal, opt_results.x)
314
+
315
+
316
+ def constrain_optimized_portfolios(
317
+ data: OpenFrame,
318
+ serie: OpenTimeSeries,
319
+ portfolioname: str = "Current Portfolio",
320
+ simulations: int = 10000,
321
+ curve_points: int = 200,
322
+ bounds: Optional[tuple[tuple[float]]] = None,
323
+ minimize_method: LiteralMinimizeMethods = "SLSQP",
324
+ ) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
325
+ """
326
+ Constrain optimized portfolios to those that improve on the current one.
327
+
328
+ Parameters
329
+ ----------
330
+ data: OpenFrame
331
+ Portfolio data
332
+ serie: OpenTimeSeries
333
+ A
334
+ portfolioname: str, default: "Current Portfolio"
335
+ Name of the portfolio
336
+ simulations: int, default: 10000
337
+ Number of possible portfolios to simulate
338
+ curve_points: int, default: 200
339
+ Number of optimal portfolios on the efficient frontier
340
+ bounds: tuple[tuple[float]], optional
341
+ The range of minumum and maximum allowed allocations for each asset
342
+ minimize_method: LiteralMinimizeMethods, default: SLSQP
343
+ The method passed into the scipy.minimize function
344
+
345
+ Returns
346
+ -------
347
+ tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
348
+ The constrained optimal portfolio data
349
+
350
+ """
351
+ lr_frame = data.from_deepcopy()
352
+ mv_frame = data.from_deepcopy()
353
+
354
+ if not bounds:
355
+ bounds = tuple((0.0, 1.0) for _ in range(data.item_count))
356
+
357
+ front_frame, sim_frame, optimal = efficient_frontier(
358
+ eframe=data,
359
+ num_ports=simulations,
360
+ frontier_points=curve_points,
361
+ bounds=bounds,
362
+ minimize_method=minimize_method,
363
+ )
364
+
365
+ condition_least_ret = front_frame.ret > serie.arithmetic_ret
366
+ # noinspection PyArgumentList
367
+ least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
368
+ least_ret_port = least_ret_frame.iloc[0]
369
+ least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
370
+ least_ret_weights = [least_ret_port[c] for c in lr_frame.columns_lvl_zero]
371
+ lr_frame.weights = least_ret_weights
372
+ resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
373
+
374
+ condition_most_vol = front_frame.stdev < serie.vol
375
+ # noinspection PyArgumentList
376
+ most_vol_frame = front_frame[condition_most_vol].sort_values(
377
+ by="ret",
378
+ ascending=False,
379
+ )
380
+ most_vol_port = most_vol_frame.iloc[0]
381
+ most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
382
+ most_vol_weights = [most_vol_port[c] for c in mv_frame.columns_lvl_zero]
383
+ mv_frame.weights = most_vol_weights
384
+ resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
385
+
386
+ return lr_frame, resleast, mv_frame, resmost
387
+
388
+
389
+ def prepare_plot_data(
390
+ assets: OpenFrame,
391
+ current: OpenTimeSeries,
392
+ optimized: NDArray[float64],
393
+ ) -> DataFrame:
394
+ """
395
+ Prepare date to be used as point_frame in the sharpeplot function.
396
+
397
+ Parameters
398
+ ----------
399
+ assets: OpenFrame
400
+ Portfolio data with individual assets and a weighted portfolio
401
+ current: OpenTimeSeries
402
+ The current or initial portfolio based on given weights
403
+ optimized: DataFrame
404
+ Data optimized with the efficient_frontier method
405
+
406
+ Returns
407
+ -------
408
+ DataFrame
409
+ The data prepared with mean returns, volatility and weights
410
+
411
+ """
412
+ txt = "<br><br>Weights:<br>" + "<br>".join(
413
+ [
414
+ f"{wgt:.1%} {nm}"
415
+ for wgt, nm in zip(
416
+ cast(list[float], assets.weights),
417
+ assets.columns_lvl_zero,
418
+ )
419
+ ],
420
+ )
421
+
422
+ opt_text_list = [
423
+ f"{wgt:.1%} {nm}" for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero)
424
+ ]
425
+ opt_text = "<br><br>Weights:<br>" + "<br>".join(opt_text_list)
426
+ vol: Series[float] = assets.vol
427
+ plotframe = DataFrame(
428
+ data=[
429
+ assets.arithmetic_ret,
430
+ vol,
431
+ Series(
432
+ data=[""] * assets.item_count,
433
+ index=vol.index,
434
+ ),
435
+ ],
436
+ index=["ret", "stdev", "text"],
437
+ )
438
+ plotframe.columns = plotframe.columns.droplevel(level=1)
439
+ plotframe["Max Sharpe Portfolio"] = [optimized[0], optimized[1], opt_text]
440
+ plotframe[current.label] = [current.arithmetic_ret, current.vol, txt]
441
+
442
+ return plotframe
443
+
444
+
445
+ def sharpeplot( # noqa: C901
446
+ sim_frame: Optional[DataFrame] = None,
447
+ line_frame: Optional[DataFrame] = None,
448
+ point_frame: Optional[DataFrame] = None,
449
+ point_frame_mode: LiteralLinePlotMode = "markers",
450
+ filename: Optional[str] = None,
451
+ directory: Optional[DirectoryPath] = None,
452
+ titletext: Optional[str] = None,
453
+ output_type: LiteralPlotlyOutput = "file",
454
+ include_plotlyjs: LiteralPlotlyJSlib = "cdn",
455
+ *,
456
+ title: bool = True,
457
+ add_logo: bool = True,
458
+ auto_open: bool = True,
459
+ ) -> tuple[Figure, str]:
460
+ """
461
+ Create scatter plot coloured by Sharpe Ratio.
462
+
463
+ Parameters
464
+ ----------
465
+ sim_frame: DataFrame, optional
466
+ Data from the simulate_portfolios method.
467
+ line_frame: DataFrame, optional
468
+ Data from the efficient_frontier method.
469
+ point_frame: DataFrame, optional
470
+ Data to highlight current and efficient portfolios.
471
+ point_frame_mode: LiteralLinePlotMode, default: markers
472
+ Which type of scatter to use.
473
+ filename: str, optional
474
+ Name of the Plotly html file
475
+ directory: DirectoryPath, optional
476
+ Directory where Plotly html file is saved
477
+ titletext: str, optional
478
+ Text for the plot title
479
+ output_type: LiteralPlotlyOutput, default: "file"
480
+ Determines output type
481
+ include_plotlyjs: LiteralPlotlyJSlib, default: "cdn"
482
+ Determines how the plotly.js library is included in the output
483
+ title: bool, default: True
484
+ Whether to add standard plot title
485
+ add_logo: bool, default: True
486
+ Whether to add Captor logo
487
+ auto_open: bool, default: True
488
+ Determines whether to open a browser window with the plot
489
+
490
+ Returns
491
+ -------
492
+ Figure
493
+ The scatter plot with simulated and optimized results
494
+
495
+ """
496
+ returns = []
497
+ risk = []
498
+
499
+ if directory:
500
+ dirpath = Path(directory).resolve()
501
+ elif Path.home().joinpath("Documents").exists():
502
+ dirpath = Path.home().joinpath("Documents")
503
+ else:
504
+ dirpath = Path(stack()[1].filename).parent
505
+
506
+ if not filename:
507
+ filename = "sharpeplot.html"
508
+ plotfile = dirpath.joinpath(filename)
509
+
510
+ fig, logo = load_plotly_dict()
511
+ figure = Figure(fig)
512
+
513
+ if sim_frame is not None:
514
+ returns.extend(list(sim_frame.loc[:, "ret"]))
515
+ risk.extend(list(sim_frame.loc[:, "stdev"]))
516
+ figure.add_scatter(
517
+ x=sim_frame.loc[:, "stdev"],
518
+ y=sim_frame.loc[:, "ret"],
519
+ hoverinfo="skip",
520
+ marker={
521
+ "size": 10,
522
+ "opacity": 0.5,
523
+ "color": sim_frame.loc[:, "sharpe"],
524
+ "colorscale": "Jet",
525
+ "reversescale": True,
526
+ "colorbar": {"thickness": 20, "title": "Ratio<br>ret / vol"},
527
+ },
528
+ mode="markers",
529
+ name="simulated portfolios",
530
+ )
531
+ if line_frame is not None:
532
+ returns.extend(list(line_frame.loc[:, "ret"]))
533
+ risk.extend(list(line_frame.loc[:, "stdev"]))
534
+ figure.add_scatter(
535
+ x=line_frame.loc[:, "stdev"],
536
+ y=line_frame.loc[:, "ret"],
537
+ text=line_frame.loc[:, "text"],
538
+ xhoverformat=".2%",
539
+ yhoverformat=".2%",
540
+ hovertemplate="Return %{y}<br>Vol %{x}%{text}",
541
+ hoverlabel_align="right",
542
+ line={"width": 2.5, "dash": "solid"},
543
+ mode="lines",
544
+ name="Efficient frontier",
545
+ )
546
+
547
+ colorway = fig["layout"].get("colorway")[ # type: ignore[union-attr]
548
+ : len(cast(DataFrame, point_frame).columns)
549
+ ]
550
+
551
+ if point_frame is not None:
552
+ for col, clr in zip(point_frame.columns, colorway):
553
+ returns.extend([point_frame.loc["ret", col]])
554
+ risk.extend([point_frame.loc["stdev", col]])
555
+ figure.add_scatter(
556
+ x=[point_frame.loc["stdev", col]],
557
+ y=[point_frame.loc["ret", col]],
558
+ xhoverformat=".2%",
559
+ yhoverformat=".2%",
560
+ hovertext=[point_frame.loc["text", col]],
561
+ hovertemplate="Return %{y}<br>Vol %{x}%{hovertext}",
562
+ hoverlabel_align="right",
563
+ marker={"size": 20, "color": clr},
564
+ mode=point_frame_mode,
565
+ name=col,
566
+ text=col,
567
+ textfont={"size": 14},
568
+ textposition="bottom center",
569
+ )
570
+
571
+ figure.update_layout(
572
+ xaxis={"tickformat": ".1%"},
573
+ xaxis_title="volatility",
574
+ yaxis={
575
+ "tickformat": ".1%",
576
+ "scaleanchor": "x",
577
+ "scaleratio": 1,
578
+ },
579
+ yaxis_title="annual return",
580
+ showlegend=False,
581
+ )
582
+ if title:
583
+ if titletext is None:
584
+ titletext = "<b>Risk and Return</b><br>"
585
+ figure.update_layout(title={"text": titletext, "font": {"size": 32}})
586
+
587
+ if add_logo:
588
+ figure.add_layout_image(logo)
589
+
590
+ if output_type == "file":
591
+ plot(
592
+ figure_or_data=figure,
593
+ filename=str(plotfile),
594
+ auto_open=auto_open,
595
+ auto_play=False,
596
+ link_text="",
597
+ include_plotlyjs=cast(bool, include_plotlyjs),
598
+ config=fig["config"],
599
+ output_type=output_type,
600
+ )
601
+ string_output = str(plotfile)
602
+ else:
603
+ div_id = filename.split(sep=".")[0]
604
+ string_output = to_html(
605
+ fig=figure,
606
+ config=fig["config"],
607
+ auto_play=False,
608
+ include_plotlyjs=cast(bool, include_plotlyjs),
609
+ full_html=False,
610
+ div_id=div_id,
611
+ )
612
+
613
+ return figure, string_output
openseries/series.py CHANGED
@@ -15,6 +15,7 @@ from numpy import (
15
15
  isnan,
16
16
  log,
17
17
  sqrt,
18
+ square,
18
19
  )
19
20
  from pandas import (
20
21
  DataFrame,
@@ -28,9 +29,6 @@ from pydantic import model_validator
28
29
  from typing_extensions import Self
29
30
 
30
31
  from openseries._common_model import _CommonModel
31
- from openseries._risk import (
32
- _ewma_calc,
33
- )
34
32
  from openseries.datefixer import date_fix, do_resample_to_business_period_ends
35
33
  from openseries.types import (
36
34
  Countries,
@@ -48,6 +46,8 @@ from openseries.types import (
48
46
  ValueType,
49
47
  )
50
48
 
49
+ __all__ = ["OpenTimeSeries", "timeseries_chain"]
50
+
51
51
  TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
52
52
 
53
53
 
@@ -675,13 +675,10 @@ class OpenTimeSeries(_CommonModel):
675
675
  ]
676
676
 
677
677
  for item in data.loc[:, cast(int, (self.label, ValueType.RTRN))].iloc[1:]:
678
- previous = rawdata[-1]
678
+ prev = rawdata[-1]
679
679
  rawdata.append(
680
- _ewma_calc(
681
- reeturn=cast(float, item),
682
- prev_ewma=previous,
683
- time_factor=time_factor,
684
- lmbda=lmbda,
680
+ sqrt(
681
+ square(item) * time_factor * (1 - lmbda) + square(prev) * lmbda,
685
682
  ),
686
683
  )
687
684
 
openseries/simulation.py CHANGED
@@ -29,8 +29,10 @@ from openseries.types import (
29
29
  ValueType,
30
30
  )
31
31
 
32
+ __all__ = ["ReturnSimulation"]
32
33
 
33
- def random_generator(seed: Optional[int]) -> Generator:
34
+
35
+ def _random_generator(seed: Optional[int]) -> Generator:
34
36
  """
35
37
  Make a Numpy Random Generator object.
36
38
 
@@ -187,7 +189,7 @@ class ReturnSimulation(BaseModel):
187
189
 
188
190
  """
189
191
  if not randomizer:
190
- randomizer = random_generator(seed=seed)
192
+ randomizer = _random_generator(seed=seed)
191
193
 
192
194
  returns = randomizer.normal(
193
195
  loc=mean_annual_return / trading_days_in_year,
@@ -244,7 +246,7 @@ class ReturnSimulation(BaseModel):
244
246
 
245
247
  """
246
248
  if not randomizer:
247
- randomizer = random_generator(seed=seed)
249
+ randomizer = _random_generator(seed=seed)
248
250
 
249
251
  returns = (
250
252
  randomizer.lognormal(
@@ -303,7 +305,7 @@ class ReturnSimulation(BaseModel):
303
305
 
304
306
  """
305
307
  if not randomizer:
306
- randomizer = random_generator(seed=seed)
308
+ randomizer = _random_generator(seed=seed)
307
309
 
308
310
  drift = (mean_annual_return - 0.5 * mean_annual_vol**2.0) * (
309
311
  1.0 / trading_days_in_year
@@ -375,7 +377,7 @@ class ReturnSimulation(BaseModel):
375
377
 
376
378
  """
377
379
  if not randomizer:
378
- randomizer = random_generator(seed=seed)
380
+ randomizer = _random_generator(seed=seed)
379
381
 
380
382
  normal_mean = 0.0
381
383
  wiener = randomizer.normal(