openseries 1.9.3__py3-none-any.whl → 1.9.5__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/owntypes.py CHANGED
@@ -376,3 +376,7 @@ class IncorrectArgumentComboError(Exception):
376
376
 
377
377
  class PropertiesInputValidationError(Exception):
378
378
  """Raised when duplicate strings are provided."""
379
+
380
+
381
+ class ResampleDataLossError(Exception):
382
+ """Raised when user attempts to run resample_to_business_period_ends on returns."""
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "responsive": true,
18
18
  "toImageButtonOptions": {
19
- "filename": "captor_plot_image",
19
+ "filename": "openseries_plot",
20
20
  "format": "png"
21
21
  }
22
22
  },
@@ -7,7 +7,6 @@ https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
7
7
  SPDX-License-Identifier: BSD-3-Clause
8
8
  """
9
9
 
10
- # mypy: disable-error-code="assignment"
11
10
  from __future__ import annotations
12
11
 
13
12
  from inspect import stack
@@ -36,7 +35,7 @@ from pandas import (
36
35
  from plotly.graph_objs import Figure # type: ignore[import-untyped]
37
36
  from plotly.io import to_html # type: ignore[import-untyped]
38
37
  from plotly.offline import plot # type: ignore[import-untyped]
39
- from scipy.optimize import minimize # type: ignore[import-untyped]
38
+ from scipy.optimize import minimize
40
39
 
41
40
  from .load_plotly import load_plotly_dict
42
41
  from .owntypes import (
@@ -143,7 +142,7 @@ def efficient_frontier(
143
142
  eframe: OpenFrame,
144
143
  num_ports: int = 5000,
145
144
  seed: int = 71,
146
- bounds: tuple[tuple[float]] | None = None,
145
+ bounds: tuple[tuple[float, float], ...] | None = None,
147
146
  frontier_points: int = 200,
148
147
  minimize_method: LiteralMinimizeMethods = "SLSQP",
149
148
  *,
@@ -159,7 +158,7 @@ def efficient_frontier(
159
158
  Number of possible portfolios to simulate
160
159
  seed: int, default: 71
161
160
  The seed for the random process
162
- bounds: tuple[tuple[float]], optional
161
+ bounds: tuple[tuple[float, float], ...], optional
163
162
  The range of minumum and maximum allowed allocations for each asset
164
163
  frontier_points: int, default: 200
165
164
  number of points along frontier to optimize
@@ -253,7 +252,7 @@ def efficient_frontier(
253
252
  bounds = tuple((0.0, 1.0) for _ in range(eframe.item_count))
254
253
  init_guess = array(eframe.weights)
255
254
 
256
- opt_results = minimize(
255
+ opt_results = minimize( # type: ignore[call-overload]
257
256
  fun=_neg_sharpe,
258
257
  x0=init_guess,
259
258
  method=minimize_method,
@@ -288,7 +287,7 @@ def efficient_frontier(
288
287
  ),
289
288
  )
290
289
 
291
- result = minimize(
290
+ result = minimize( # type: ignore[call-overload]
292
291
  fun=_minimize_volatility,
293
292
  x0=init_guess,
294
293
  method=minimize_method,
@@ -320,7 +319,7 @@ def efficient_frontier(
320
319
 
321
320
  if tweak:
322
321
  limit_tweak = 0.001
323
- line_df["stdev_diff"] = line_df.stdev.pct_change()
322
+ line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
324
323
  line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
325
324
  line_df = line_df.drop(columns="stdev_diff")
326
325
 
@@ -333,7 +332,7 @@ def constrain_optimized_portfolios(
333
332
  portfolioname: str = "Current Portfolio",
334
333
  simulations: int = 10000,
335
334
  curve_points: int = 200,
336
- bounds: tuple[tuple[float]] | None = None,
335
+ bounds: tuple[tuple[float, float], ...] | None = None,
337
336
  minimize_method: LiteralMinimizeMethods = "SLSQP",
338
337
  ) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
339
338
  """Constrain optimized portfolios to those that improve on the current one.
@@ -350,7 +349,7 @@ def constrain_optimized_portfolios(
350
349
  Number of possible portfolios to simulate
351
350
  curve_points: int, default: 200
352
351
  Number of optimal portfolios on the efficient frontier
353
- bounds: tuple[tuple[float]], optional
352
+ bounds: tuple[tuple[float, float], ...], optional
354
353
  The range of minumum and maximum allowed allocations for each asset
355
354
  minimize_method: LiteralMinimizeMethods, default: SLSQP
356
355
  The method passed into the scipy.minimize function
@@ -441,7 +440,7 @@ def prepare_plot_data(
441
440
  for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero, strict=True)
442
441
  ]
443
442
  opt_text = "<br><br>Weights:<br>" + "<br>".join(opt_text_list)
444
- vol: Series[float] = assets.vol
443
+ vol = cast("Series[float]", assets.vol)
445
444
  plotframe = DataFrame(
446
445
  data=[
447
446
  assets.arithmetic_ret,
openseries/report.py CHANGED
@@ -7,7 +7,6 @@ https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
7
7
  SPDX-License-Identifier: BSD-3-Clause
8
8
  """
9
9
 
10
- # mypy: disable-error-code="assignment"
11
10
  from __future__ import annotations
12
11
 
13
12
  from inspect import stack
@@ -20,20 +19,21 @@ from warnings import catch_warnings, simplefilter
20
19
 
21
20
  if TYPE_CHECKING: # pragma: no cover
22
21
  from pandas import Series
23
- from plotly.graph_objs import Figure
22
+ from plotly.graph_objs import Figure # type: ignore[import-untyped]
24
23
 
25
24
  from .frame import OpenFrame
26
25
  from .owntypes import LiteralPlotlyJSlib, LiteralPlotlyOutput
27
26
 
28
27
 
29
- from pandas import DataFrame, Series, Timestamp, concat
30
- from plotly.io import to_html
31
- from plotly.offline import plot
32
- from plotly.subplots import make_subplots
28
+ from pandas import DataFrame, Index, Series, Timestamp, concat
29
+ from plotly.io import to_html # type: ignore[import-untyped]
30
+ from plotly.offline import plot # type: ignore[import-untyped]
31
+ from plotly.subplots import make_subplots # type: ignore[import-untyped]
33
32
 
34
33
  from .load_plotly import load_plotly_dict
35
34
  from .owntypes import (
36
35
  LiteralBizDayFreq,
36
+ LiteralFrameProps,
37
37
  ValueType,
38
38
  )
39
39
 
@@ -68,21 +68,19 @@ def calendar_period_returns(
68
68
  """
69
69
  copied = data.from_deepcopy()
70
70
  copied.resample_to_business_period_ends(freq=freq)
71
- vtypes = [x == ValueType.RTRN for x in copied.tsdf.columns.get_level_values(1)]
72
- if not any(vtypes):
73
- copied.value_to_ret()
71
+ copied.value_to_ret()
74
72
  cldr = copied.tsdf.iloc[1:].copy()
75
73
  if relabel:
76
74
  if freq.upper() == "BYE":
77
- cldr.index = [d.year for d in cldr.index]
75
+ cldr.index = Index([d.year for d in cldr.index])
78
76
  elif freq.upper() == "BQE":
79
- cldr.index = [
80
- Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index
81
- ]
77
+ cldr.index = Index(
78
+ [Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index]
79
+ )
82
80
  else:
83
- cldr.index = [d.strftime("%b %y") for d in cldr.index]
81
+ cldr.index = Index([d.strftime("%b %y") for d in cldr.index])
84
82
 
85
- return cldr # type: ignore[no-any-return]
83
+ return cldr
86
84
 
87
85
 
88
86
  def report_html(
@@ -129,9 +127,10 @@ def report_html(
129
127
  Plotly Figure and a div section or a html filename with location
130
128
 
131
129
  """
132
- data.trunc_frame().value_nan_handle().to_cumret()
130
+ copied = data.from_deepcopy()
131
+ copied.trunc_frame().value_nan_handle().to_cumret()
133
132
 
134
- if data.yearfrac > 1.0:
133
+ if copied.yearfrac > 1.0:
135
134
  properties = [
136
135
  "geo_ret",
137
136
  "vol",
@@ -219,10 +218,10 @@ def report_html(
219
218
  ],
220
219
  )
221
220
 
222
- for item, lbl in enumerate(data.columns_lvl_zero):
221
+ for item, lbl in enumerate(copied.columns_lvl_zero):
223
222
  figure.add_scatter(
224
- x=data.tsdf.index,
225
- y=data.tsdf.iloc[:, item],
223
+ x=copied.tsdf.index,
224
+ y=copied.tsdf.iloc[:, item],
226
225
  hovertemplate="%{y:.2%}<br>%{x|%Y-%m-%d}",
227
226
  line={"width": 2.5, "dash": "solid"},
228
227
  mode="lines",
@@ -233,18 +232,19 @@ def report_html(
233
232
  )
234
233
 
235
234
  quarter_of_year = 0.25
236
- if data.yearfrac < quarter_of_year:
237
- tmp = data.from_deepcopy()
235
+ if copied.yearfrac < quarter_of_year:
236
+ tmp = copied.from_deepcopy()
238
237
  bdf = tmp.value_to_ret().tsdf.iloc[1:]
239
238
  else:
240
- bdf = calendar_period_returns(data, freq=bar_freq)
239
+ bdf = calendar_period_returns(data=copied, freq=bar_freq)
241
240
 
242
- for item in range(data.item_count):
241
+ for item in range(copied.item_count):
242
+ col_name = cast("tuple[str, ValueType]", bdf.iloc[:, item].name)
243
243
  figure.add_bar(
244
244
  x=bdf.index,
245
245
  y=bdf.iloc[:, item],
246
246
  hovertemplate="%{y:.2%}<br>%{x}",
247
- name=bdf.iloc[:, item].name[0], # type: ignore[index]
247
+ name=col_name[0],
248
248
  showlegend=False,
249
249
  row=2,
250
250
  col=1,
@@ -265,8 +265,10 @@ def report_html(
265
265
  ]
266
266
 
267
267
  # noinspection PyTypeChecker
268
- rpt_df = data.all_properties(properties=properties) # type: ignore[arg-type]
269
- alpha_frame = data.from_deepcopy()
268
+ rpt_df = copied.all_properties(
269
+ properties=cast("list[LiteralFrameProps]", properties)
270
+ )
271
+ alpha_frame = copied.from_deepcopy()
270
272
  alpha_frame.to_cumret()
271
273
  with catch_warnings():
272
274
  simplefilter("ignore")
@@ -279,14 +281,16 @@ def report_html(
279
281
  for aname in alpha_frame.columns_lvl_zero[:-1]
280
282
  ]
281
283
  alphas.append("")
282
- ar = DataFrame(data=alphas, index=data.tsdf.columns, columns=["Jensen's Alpha"]).T
284
+ ar = DataFrame(
285
+ data=alphas, index=copied.tsdf.columns, columns=["Jensen's Alpha"]
286
+ ).T
283
287
  rpt_df = concat([rpt_df, ar])
284
- ir = data.info_ratio_func()
288
+ ir = copied.info_ratio_func()
285
289
  ir.name = "Information Ratio"
286
290
  ir.iloc[-1] = None
287
- ir = ir.to_frame().T
288
- rpt_df = concat([rpt_df, ir])
289
- te_frame = data.from_deepcopy()
291
+ ir_df = ir.to_frame().T
292
+ rpt_df = concat([rpt_df, ir_df])
293
+ te_frame = copied.from_deepcopy()
290
294
  te_frame.resample("7D")
291
295
  with catch_warnings():
292
296
  simplefilter("ignore")
@@ -300,11 +304,11 @@ def report_html(
300
304
  else:
301
305
  te.iloc[-1] = None
302
306
  te.name = "Tracking Error (weekly)"
303
- te = te.to_frame().T
304
- rpt_df = concat([rpt_df, te])
307
+ te_df = te.to_frame().T
308
+ rpt_df = concat([rpt_df, te_df])
305
309
 
306
- if data.yearfrac > 1.0:
307
- crm = data.from_deepcopy()
310
+ if copied.yearfrac > 1.0:
311
+ crm = copied.from_deepcopy()
308
312
  crm.resample("ME")
309
313
  cru_save = Series(
310
314
  data=[""] * crm.item_count,
@@ -324,10 +328,10 @@ def report_html(
324
328
  else:
325
329
  cru.iloc[-1] = None
326
330
  cru.name = "Capture Ratio (monthly)"
327
- cru = cru.to_frame().T
328
- rpt_df = concat([rpt_df, cru])
331
+ cru_df = cru.to_frame().T
332
+ rpt_df = concat([rpt_df, cru_df])
329
333
  formats.append("{:.2f}")
330
- beta_frame = data.from_deepcopy()
334
+ beta_frame = copied.from_deepcopy()
331
335
  beta_frame.resample("7D").value_nan_handle("drop")
332
336
  beta_frame.to_cumret()
333
337
  betas: list[str | float] = [
@@ -337,51 +341,50 @@ def report_html(
337
341
  )
338
342
  for bname in beta_frame.columns_lvl_zero[:-1]
339
343
  ]
340
- # noinspection PyTypeChecker
341
344
  betas.append("")
342
345
  br = DataFrame(
343
346
  data=betas,
344
- index=data.tsdf.columns,
347
+ index=copied.tsdf.columns,
345
348
  columns=["Index Beta (weekly)"],
346
349
  ).T
347
350
  rpt_df = concat([rpt_df, br])
348
351
 
349
352
  for item, f in zip(rpt_df.index, formats, strict=False):
350
353
  rpt_df.loc[item] = rpt_df.loc[item].apply(
351
- lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x), # type: ignore[return-value]
354
+ lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
352
355
  )
353
356
 
354
- rpt_df.index = labels_init
357
+ rpt_df.index = Index(labels_init)
355
358
 
356
- this_year = data.last_idx.year
357
- this_month = data.last_idx.month
358
- ytd = cast("Series[float]", data.value_ret_calendar_period(year=this_year)).map(
359
+ this_year = copied.last_idx.year
360
+ this_month = copied.last_idx.month
361
+ ytd = cast("Series[float]", copied.value_ret_calendar_period(year=this_year)).map(
359
362
  "{:.2%}".format
360
363
  )
361
364
  ytd.name = "Year-to-Date"
362
365
  mtd = cast(
363
366
  "Series[float]",
364
- data.value_ret_calendar_period(year=this_year, month=this_month),
367
+ copied.value_ret_calendar_period(year=this_year, month=this_month),
365
368
  ).map(
366
369
  "{:.2%}".format,
367
370
  )
368
371
  mtd.name = "Month-to-Date"
369
- ytd = ytd.to_frame().T
370
- mtd = mtd.to_frame().T
371
- rpt_df = concat([rpt_df, ytd])
372
- rpt_df = concat([rpt_df, mtd])
372
+ ytd_df = ytd.to_frame().T
373
+ mtd_df = mtd.to_frame().T
374
+ rpt_df = concat([rpt_df, ytd_df])
375
+ rpt_df = concat([rpt_df, mtd_df])
373
376
  rpt_df = rpt_df.reindex(labels_final)
374
377
 
375
- rpt_df.index = [f"<b>{x}</b>" for x in rpt_df.index]
378
+ rpt_df.index = Index([f"<b>{x}</b>" for x in rpt_df.index])
376
379
  rpt_df = rpt_df.reset_index()
377
380
 
378
- colmns = ["", *data.columns_lvl_zero]
381
+ colmns = ["", *copied.columns_lvl_zero]
379
382
  columns = [f"<b>{x}</b>" for x in colmns]
380
383
  aligning = ["left"] + ["center"] * (len(columns) - 1)
381
384
 
382
385
  col_even_color = "lightgrey"
383
386
  col_odd_color = "white"
384
- color_lst = ["grey"] + [col_odd_color] * (data.item_count - 1) + [col_even_color]
387
+ color_lst = ["grey"] + [col_odd_color] * (copied.item_count - 1) + [col_even_color]
385
388
 
386
389
  tablevalues = rpt_df.transpose().to_numpy().tolist()
387
390
  cleanedtablevalues = list(tablevalues)[:-1]
@@ -426,7 +429,9 @@ def report_html(
426
429
  figure.add_layout_image(logo)
427
430
 
428
431
  figure.update_layout(fig.get("layout"))
429
- colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get("colorway")
432
+ colorway: list[str] = cast("dict[str, list[str]]", fig["layout"]).get(
433
+ "colorway", []
434
+ )
430
435
 
431
436
  if vertical_legend:
432
437
  legend = {
@@ -447,7 +452,7 @@ def report_html(
447
452
 
448
453
  figure.update_layout(
449
454
  legend=legend,
450
- colorway=colorway[: data.item_count],
455
+ colorway=colorway[: copied.item_count],
451
456
  )
452
457
  figure.update_xaxes(gridcolor="#EEEEEE", automargin=True, tickangle=-45)
453
458
  figure.update_yaxes(tickformat=".2%", gridcolor="#EEEEEE", automargin=True)
openseries/series.py CHANGED
@@ -9,13 +9,13 @@ SPDX-License-Identifier: BSD-3-Clause
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from collections.abc import Iterable
13
12
  from copy import deepcopy
14
13
  from logging import getLogger
15
14
  from typing import TYPE_CHECKING, Any, TypeVar, cast
16
15
 
17
16
  if TYPE_CHECKING: # pragma: no cover
18
17
  import datetime as dt
18
+ from collections.abc import Callable
19
19
 
20
20
  from numpy import (
21
21
  append,
@@ -53,11 +53,15 @@ from .owntypes import (
53
53
  LiteralSeriesProps,
54
54
  MarketsNotStringNorListStrError,
55
55
  OpenTimeSeriesPropertiesList,
56
+ ResampleDataLossError,
56
57
  Self,
57
58
  ValueListType,
58
59
  ValueType,
59
60
  )
60
61
 
62
+ FieldValidator = cast("Callable[..., Callable[..., Any]]", field_validator)
63
+ ModelValidator = cast("Callable[..., Callable[..., Any]]", model_validator)
64
+
61
65
  logger = getLogger(__name__)
62
66
 
63
67
  __all__ = ["OpenTimeSeries", "timeseries_chain"]
@@ -122,21 +126,21 @@ class OpenTimeSeries(_CommonModel):
122
126
  isin: str | None = None
123
127
  label: str | None = None
124
128
 
125
- @field_validator("domestic", mode="before") # type: ignore[misc]
129
+ @FieldValidator("domestic", mode="before")
126
130
  @classmethod
127
131
  def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
128
132
  """Pydantic validator to ensure domestic field is validated."""
129
133
  _ = Currency(ccy=value)
130
134
  return value
131
135
 
132
- @field_validator("countries", mode="before") # type: ignore[misc]
136
+ @FieldValidator("countries", mode="before")
133
137
  @classmethod
134
138
  def _validate_countries(cls, value: CountriesType) -> CountriesType:
135
139
  """Pydantic validator to ensure countries field is validated."""
136
140
  _ = Countries(countryinput=value)
137
141
  return value
138
142
 
139
- @field_validator("markets", mode="before") # type: ignore[misc]
143
+ @FieldValidator("markets", mode="before")
140
144
  @classmethod
141
145
  def _validate_markets(
142
146
  cls, value: list[str] | str | None
@@ -155,7 +159,7 @@ class OpenTimeSeries(_CommonModel):
155
159
  raise MarketsNotStringNorListStrError(item_msg)
156
160
  raise MarketsNotStringNorListStrError(msg)
157
161
 
158
- @model_validator(mode="after") # type: ignore[misc,unused-ignore]
162
+ @ModelValidator(mode="after")
159
163
  def _dates_and_values_validate(self: Self) -> Self:
160
164
  """Pydantic validator to ensure dates and values are validated."""
161
165
  values_list_length = len(self.values)
@@ -369,19 +373,17 @@ class OpenTimeSeries(_CommonModel):
369
373
  An OpenTimeSeries object
370
374
 
371
375
  """
372
- if not isinstance(d_range, Iterable) and all([days, end_dt]):
373
- d_range = DatetimeIndex(
374
- [d.date() for d in date_range(periods=days, end=end_dt, freq="D")],
375
- )
376
- elif not isinstance(d_range, Iterable) and not all([days, end_dt]):
377
- msg = "If d_range is not provided both days and end_dt must be."
378
- raise IncorrectArgumentComboError(msg)
379
-
380
- deltas = array(
381
- [i.days for i in DatetimeIndex(d_range)[1:] - DatetimeIndex(d_range)[:-1]], # type: ignore[arg-type]
382
- )
376
+ if d_range is None:
377
+ if days is not None and end_dt is not None:
378
+ d_range = DatetimeIndex(
379
+ [d.date() for d in date_range(periods=days, end=end_dt, freq="D")],
380
+ )
381
+ else:
382
+ msg = "If d_range is not provided both days and end_dt must be."
383
+ raise IncorrectArgumentComboError(msg)
384
+ deltas = array([i.days for i in d_range[1:] - d_range[:-1]])
383
385
  arr: list[float] = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
384
- dates = [d.strftime("%Y-%m-%d") for d in DatetimeIndex(d_range)] # type: ignore[arg-type]
386
+ dates = [d.strftime("%Y-%m-%d") for d in d_range]
385
387
 
386
388
  return cls(
387
389
  timeseries_id="",
@@ -468,7 +470,7 @@ class OpenTimeSeries(_CommonModel):
468
470
  The returns of the values in the series
469
471
 
470
472
  """
471
- returns = self.tsdf.pct_change()
473
+ returns = self.tsdf.ffill().pct_change()
472
474
  returns.iloc[0] = 0
473
475
  self.valuetype = ValueType.RTRN
474
476
  arrays = [[self.label], [self.valuetype]]
@@ -551,8 +553,7 @@ class OpenTimeSeries(_CommonModel):
551
553
  arr = array(self.values) / divider
552
554
 
553
555
  deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
554
- # noinspection PyTypeChecker
555
- arr = cumprod( # type: ignore[assignment]
556
+ arr = cumprod(
556
557
  a=insert(arr=1.0 + deltas * arr[:-1] / days_in_year, obj=0, values=1.0)
557
558
  )
558
559
 
@@ -586,7 +587,10 @@ class OpenTimeSeries(_CommonModel):
586
587
 
587
588
  """
588
589
  self.tsdf.index = DatetimeIndex(self.tsdf.index)
589
- self.tsdf = self.tsdf.resample(freq).last()
590
+ if self.valuetype == ValueType.RTRN:
591
+ self.tsdf = self.tsdf.resample(freq).sum()
592
+ else:
593
+ self.tsdf = self.tsdf.resample(freq).last()
590
594
  self.tsdf.index = Index(d.date() for d in DatetimeIndex(self.tsdf.index))
591
595
  return self
592
596
 
@@ -612,6 +616,14 @@ class OpenTimeSeries(_CommonModel):
612
616
  An OpenTimeSeries object
613
617
 
614
618
  """
619
+ if self.valuetype == ValueType.RTRN:
620
+ msg = (
621
+ "Do not run resample_to_business_period_ends on return series. "
622
+ "The operation will pick the last data point in the sparser series. "
623
+ "It will not sum returns and therefore data will be lost."
624
+ )
625
+ raise ResampleDataLossError(msg)
626
+
615
627
  dates = _do_resample_to_business_period_ends(
616
628
  data=self.tsdf,
617
629
  freq=freq,
@@ -659,7 +671,9 @@ class OpenTimeSeries(_CommonModel):
659
671
  Series EWMA volatility
660
672
 
661
673
  """
662
- earlier, later = self.calc_range(months_from_last, from_date, to_date)
674
+ earlier, later = self.calc_range(
675
+ months_offset=months_from_last, from_dt=from_date, to_dt=to_date
676
+ )
663
677
  if periods_in_a_year_fixed:
664
678
  time_factor = float(periods_in_a_year_fixed)
665
679
  else:
@@ -725,7 +739,7 @@ class OpenTimeSeries(_CommonModel):
725
739
  returns_input = True
726
740
  else:
727
741
  values = [cast("float", self.tsdf.iloc[0, 0])]
728
- ra_df = self.tsdf.pct_change()
742
+ ra_df = self.tsdf.ffill().pct_change()
729
743
  returns_input = False
730
744
  ra_df = ra_df.dropna()
731
745
 
openseries/simulation.py CHANGED
@@ -115,7 +115,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
115
115
  Simulation data
116
116
 
117
117
  """
118
- return self.dframe.add(1.0).cumprod(axis="columns").T # type: ignore[no-any-return]
118
+ return self.dframe.add(1.0).cumprod(axis="columns").T
119
119
 
120
120
  @property
121
121
  def realized_mean_return(self: Self) -> float:
@@ -129,7 +129,9 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
129
129
  """
130
130
  return cast(
131
131
  "float",
132
- (self.results.pct_change().mean() * self.trading_days_in_year).iloc[0],
132
+ (
133
+ self.results.ffill().pct_change().mean() * self.trading_days_in_year
134
+ ).iloc[0],
133
135
  )
134
136
 
135
137
  @property
@@ -144,9 +146,10 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
144
146
  """
145
147
  return cast(
146
148
  "float",
147
- (self.results.pct_change().std() * sqrt(self.trading_days_in_year)).iloc[
148
- 0
149
- ],
149
+ (
150
+ self.results.ffill().pct_change().std()
151
+ * sqrt(self.trading_days_in_year)
152
+ ).iloc[0],
150
153
  )
151
154
 
152
155
  @classmethod
@@ -460,7 +463,7 @@ class ReturnSimulation(BaseModel): # type: ignore[misc]
460
463
  [ValueType.RTRN],
461
464
  ],
462
465
  )
463
- return sdf # type: ignore[no-any-return]
466
+ return sdf
464
467
 
465
468
  fdf = DataFrame()
466
469
  for item in range(self.number_of_sims):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openseries
3
- Version: 1.9.3
3
+ Version: 1.9.5
4
4
  Summary: Tools for analyzing financial timeseries.
5
5
  License: # BSD 3-Clause License
6
6
 
@@ -0,0 +1,17 @@
1
+ openseries/__init__.py,sha256=WAh79oE-ceGG_yl4nBukkp3UPvmLk4u_GySL2xOKbxE,1375
2
+ openseries/_common_model.py,sha256=Nug9DIp54q7tt0yHFEQKAZPrG9c1Oy6VpyqoRWKOC4I,85499
3
+ openseries/_risk.py,sha256=8XKZWWXrECo0Vd9r2kbcn4dzyPuo93DAEO8eSkv4w20,2357
4
+ openseries/datefixer.py,sha256=Z3AKLvULzy9MPQOndKhay0nGx2EgYcjVFNjT9qReoHk,15727
5
+ openseries/frame.py,sha256=a3TPLdvapnvHU_wbhPO0G95UHaHlJLXnsSmm-Ti_2sw,58579
6
+ openseries/load_plotly.py,sha256=C6iQyabfi5ubSONuis3yRHb3bUktBtTDlovsDIaeHNQ,2266
7
+ openseries/owntypes.py,sha256=P9CKoLtjUaFiktLb_axihrlVR5bJfdDbSFJC72kQG2o,9584
8
+ openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
9
+ openseries/plotly_layouts.json,sha256=MvDEQuiqIhMBXBelXb1sedTOlTPheizv6NZRLeE9YS4,1431
10
+ openseries/portfoliotools.py,sha256=NMSp-dYjPRjBJpZ9W20IKDrQmBDk7e4qkTPT4QFx6io,19721
11
+ openseries/report.py,sha256=iWe68o883EIU9B_t-61fl41wzTY2e6p_ZHgST2uoH3g,14393
12
+ openseries/series.py,sha256=HT2U_gUhiZMhvt7hzpUPakKBEXI64Ur2WqoaUhXV11k,28925
13
+ openseries/simulation.py,sha256=t2LFlAT9lcfPqqGEXOUoEgIG2gDEuGps3Qd3IgN_GLk,14359
14
+ openseries-1.9.5.dist-info/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
15
+ openseries-1.9.5.dist-info/METADATA,sha256=XYz8k9ujPY_MuwGjyHEyxyCgjKppszQJfmFq3tZ67U4,48301
16
+ openseries-1.9.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ openseries-1.9.5.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- openseries/__init__.py,sha256=WAh79oE-ceGG_yl4nBukkp3UPvmLk4u_GySL2xOKbxE,1375
2
- openseries/_common_model.py,sha256=ydS3CNqxtd5GPa9R-28ApcgjKe1d9vXPyaF2aECkv1A,84720
3
- openseries/_risk.py,sha256=8XKZWWXrECo0Vd9r2kbcn4dzyPuo93DAEO8eSkv4w20,2357
4
- openseries/datefixer.py,sha256=FBe0zEcCDbrMPatN9OvaKqXJd9EQOqXzknQQ1N3Ji0s,15774
5
- openseries/frame.py,sha256=AAIHHvs0f1HoRtlXQZmF8PPFPNa134KUYaKnSVad-xY,57752
6
- openseries/load_plotly.py,sha256=C6iQyabfi5ubSONuis3yRHb3bUktBtTDlovsDIaeHNQ,2266
7
- openseries/owntypes.py,sha256=VrOTslL0H8hM0BL-E57inlgfV_JEto9WMH9RCE2G0Ug,9454
8
- openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
9
- openseries/plotly_layouts.json,sha256=9tKAeittrjwJWhBMV8SnCDAWdhgbVnUqXcN6P_J_bos,1433
10
- openseries/portfoliotools.py,sha256=X0byXAfLI8AOc0kWIa1edJujgTRhSP4BNLQgbE28YPk,19667
11
- openseries/report.py,sha256=s81bPGlgyzBdWR_MZTpb9twj2N9uxtnZZrV7FRpFyNU,14238
12
- openseries/series.py,sha256=sk6ZeFEsM9sET52y8Zzgoo7zBZvEhcYkddyIMa5Kp4s,28506
13
- openseries/simulation.py,sha256=fc0zonz9aogRIT7p06CBOUm6-mArVIg9UJF3bdgq3CE,14359
14
- openseries-1.9.3.dist-info/LICENSE.md,sha256=wNupG-KLsG0aTncb_SMNDh1ExtrKXlpxSJ6RC-g-SWs,1516
15
- openseries-1.9.3.dist-info/METADATA,sha256=jQA3_jxZVvwOcCxBrDXrD4ZpnqpZkO9UyFbdPr791Z0,48301
16
- openseries-1.9.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- openseries-1.9.3.dist-info/RECORD,,