openseries 1.9.6__py3-none-any.whl → 2.0.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.
@@ -1,11 +1,4 @@
1
- """Defining the portfolio tools for the OpenFrame class.
2
-
3
- Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
4
-
5
- Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
6
- https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
7
- SPDX-License-Identifier: BSD-3-Clause
8
- """
1
+ """Defining the portfolio tools for the OpenFrame class."""
9
2
 
10
3
  from __future__ import annotations
11
4
 
@@ -16,13 +9,13 @@ from typing import TYPE_CHECKING, cast
16
9
  from numpy import (
17
10
  append,
18
11
  array,
12
+ einsum,
19
13
  float64,
20
14
  inf,
21
15
  isnan,
22
16
  linspace,
23
17
  nan,
24
18
  sqrt,
25
- zeros,
26
19
  )
27
20
  from numpy import (
28
21
  sum as npsum,
@@ -48,12 +41,9 @@ from .owntypes import (
48
41
  ValueType,
49
42
  )
50
43
  from .series import OpenTimeSeries
51
-
52
- # noinspection PyProtectedMember
53
44
  from .simulation import _random_generator
54
45
 
55
46
  if TYPE_CHECKING: # pragma: no cover
56
- # noinspection PyUnresolvedReferences
57
47
  from collections.abc import Callable
58
48
 
59
49
  from numpy.typing import NDArray
@@ -87,7 +77,7 @@ def simulate_portfolios(
87
77
  The seed for the random process
88
78
 
89
79
  Returns:
90
- -------
80
+ --------
91
81
  pandas.DataFrame
92
82
  The resulting data
93
83
 
@@ -106,25 +96,16 @@ def simulate_portfolios(
106
96
 
107
97
  log_ret.columns = log_ret.columns.droplevel(level=1)
108
98
 
109
- randomizer = _random_generator(seed=seed)
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)
99
+ cov_matrix = log_ret.cov() * simframe.periods_in_a_year
100
+ mean_returns = log_ret.mean() * simframe.periods_in_a_year
115
101
 
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
120
-
121
- vol_arr[x] = sqrt(
122
- weights.T @ (log_ret.cov() * simframe.periods_in_a_year @ weights),
123
- )
124
-
125
- ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
102
+ randomizer = _random_generator(seed=seed)
103
+ all_weights = randomizer.random((num_ports, simframe.item_count))
104
+ all_weights = all_weights / all_weights.sum(axis=1, keepdims=True)
126
105
 
127
- sharpe_arr[x] = ret_arr[x] / vol_arr[x]
106
+ ret_arr = all_weights @ mean_returns
107
+ vol_arr = sqrt(einsum("ij,jk,ik->i", all_weights, cov_matrix, all_weights))
108
+ sharpe_arr = ret_arr / vol_arr
128
109
 
129
110
  simdf = concat(
130
111
  [
@@ -137,7 +118,6 @@ def simulate_portfolios(
137
118
  return simdf.dropna()
138
119
 
139
120
 
140
- # noinspection PyUnusedLocal
141
121
  def efficient_frontier(
142
122
  eframe: OpenFrame,
143
123
  num_ports: int = 5000,
@@ -168,7 +148,7 @@ def efficient_frontier(
168
148
  cutting the frontier to exclude multiple points with almost the same risk
169
149
 
170
150
  Returns:
171
- -------
151
+ --------
172
152
  tuple[DataFrame, DataFrame, NDArray[float]]
173
153
  The efficient frontier data, simulation data and optimal portfolio
174
154
 
@@ -230,7 +210,7 @@ def efficient_frontier(
230
210
  _get_ret_vol_sr(
231
211
  lg_ret=log_ret,
232
212
  weights=weights,
233
- per_in_yr=eframe.periods_in_a_year,
213
+ per_in_yr=copi.periods_in_a_year,
234
214
  )[2]
235
215
  * -1,
236
216
  )
@@ -243,7 +223,7 @@ def efficient_frontier(
243
223
  _get_ret_vol_sr(
244
224
  lg_ret=log_ret,
245
225
  weights=weights,
246
- per_in_yr=eframe.periods_in_a_year,
226
+ per_in_yr=copi.periods_in_a_year,
247
227
  )[1],
248
228
  )
249
229
 
@@ -263,7 +243,7 @@ def efficient_frontier(
263
243
  optimal = _get_ret_vol_sr(
264
244
  lg_ret=log_ret,
265
245
  weights=opt_results.x,
266
- per_in_yr=eframe.periods_in_a_year,
246
+ per_in_yr=copi.periods_in_a_year,
267
247
  )
268
248
 
269
249
  frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
@@ -280,7 +260,7 @@ def efficient_frontier(
280
260
  "fun": lambda w, poss_return=possible_return: _diff_return(
281
261
  lg_ret=log_ret,
282
262
  weights=w,
283
- per_in_yr=eframe.periods_in_a_year,
263
+ per_in_yr=copi.periods_in_a_year,
284
264
  poss_return=poss_return,
285
265
  ),
286
266
  },
@@ -355,7 +335,7 @@ def constrain_optimized_portfolios(
355
335
  The method passed into the scipy.minimize function
356
336
 
357
337
  Returns:
358
- -------
338
+ --------
359
339
  tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
360
340
  The constrained optimal portfolio data
361
341
 
@@ -375,7 +355,6 @@ def constrain_optimized_portfolios(
375
355
  )
376
356
 
377
357
  condition_least_ret = front_frame.ret > serie.arithmetic_ret
378
- # noinspection PyArgumentList
379
358
  least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
380
359
  least_ret_port: Series[float] = least_ret_frame.iloc[0]
381
360
  least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
@@ -386,7 +365,6 @@ def constrain_optimized_portfolios(
386
365
  resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
387
366
 
388
367
  condition_most_vol = front_frame.stdev < serie.vol
389
- # noinspection PyArgumentList
390
368
  most_vol_frame = front_frame[condition_most_vol].sort_values(
391
369
  by="ret",
392
370
  ascending=False,
@@ -419,7 +397,7 @@ def prepare_plot_data(
419
397
  Data optimized with the efficient_frontier method
420
398
 
421
399
  Returns:
422
- -------
400
+ --------
423
401
  DataFrame
424
402
  The data prepared with mean returns, volatility and weights
425
403
 
@@ -503,7 +481,7 @@ def sharpeplot(
503
481
  Determines whether to open a browser window with the plot
504
482
 
505
483
  Returns:
506
- -------
484
+ --------
507
485
  Figure
508
486
  The scatter plot with simulated and optimized results
509
487
 
openseries/py.typed ADDED
File without changes
openseries/report.py CHANGED
@@ -1,11 +1,4 @@
1
- """Functions related to HTML reports.
2
-
3
- Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
4
-
5
- Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
6
- https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
7
- SPDX-License-Identifier: BSD-3-Clause
8
- """
1
+ """Functions related to HTML reports."""
9
2
 
10
3
  from __future__ import annotations
11
4
 
@@ -61,7 +54,7 @@ def calendar_period_returns(
61
54
  Whether to set new appropriate labels
62
55
 
63
56
  Returns:
64
- -------
57
+ --------
65
58
  pandas.DataFrame
66
59
  The resulting data
67
60
 
@@ -122,7 +115,7 @@ def report_html(
122
115
  Determines whether to vertically align the legend's labels
123
116
 
124
117
  Returns:
125
- -------
118
+ --------
126
119
  tuple[plotly.go.Figure, str]
127
120
  Plotly Figure and a div section or a html filename with location
128
121
 
@@ -264,7 +257,6 @@ def report_html(
264
257
  "{:.2f}",
265
258
  ]
266
259
 
267
- # noinspection PyTypeChecker
268
260
  rpt_df = copied.all_properties(
269
261
  properties=cast("list[LiteralFrameProps]", properties),
270
262
  )
@@ -353,7 +345,9 @@ def report_html(
353
345
 
354
346
  for item, f in zip(rpt_df.index, formats, strict=False):
355
347
  rpt_df.loc[item] = rpt_df.loc[item].apply(
356
- lambda x, fmt=f: x if (isinstance(x, str) or x is None) else fmt.format(x),
348
+ lambda x, fmt=f: str(x)
349
+ if (isinstance(x, str) or x is None)
350
+ else fmt.format(x),
357
351
  )
358
352
 
359
353
  rpt_df.index = Index(labels_init)
openseries/series.py CHANGED
@@ -1,11 +1,4 @@
1
- """Defining the OpenTimeSeries class.
2
-
3
- Copyright (c) Captor Fund Management AB. This file is part of the openseries project.
4
-
5
- Licensed under the BSD 3-Clause License. You may obtain a copy of the License at:
6
- https://github.com/CaptorAB/openseries/blob/master/LICENSE.md
7
- SPDX-License-Identifier: BSD-3-Clause
8
- """
1
+ """The OpenTimeSeries class."""
9
2
 
10
3
  from __future__ import annotations
11
4
 
@@ -15,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypeVar, cast
15
8
 
16
9
  if TYPE_CHECKING: # pragma: no cover
17
10
  import datetime as dt
18
- from collections.abc import Callable
19
11
 
20
12
  from numpy.typing import NDArray
21
13
  from pandas import Timestamp
@@ -41,7 +33,7 @@ from pandas import (
41
33
  )
42
34
  from pydantic import field_validator, model_validator
43
35
 
44
- from ._common_model import _CommonModel
36
+ from ._common_model import _calculate_time_factor, _CommonModel
45
37
  from .datefixer import _do_resample_to_business_period_ends, date_fix
46
38
  from .owntypes import (
47
39
  Countries,
@@ -63,9 +55,6 @@ from .owntypes import (
63
55
  ValueType,
64
56
  )
65
57
 
66
- FieldValidator = cast("Callable[..., Callable[..., Any]]", field_validator)
67
- ModelValidator = cast("Callable[..., Callable[..., Any]]", model_validator)
68
-
69
58
  logger = getLogger(__name__)
70
59
 
71
60
  __all__ = ["OpenTimeSeries", "timeseries_chain"]
@@ -73,7 +62,6 @@ __all__ = ["OpenTimeSeries", "timeseries_chain"]
73
62
  TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
74
63
 
75
64
 
76
- # noinspection PyUnresolvedReferences,PyNestedDecorators
77
65
  class OpenTimeSeries(_CommonModel[float]):
78
66
  """OpenTimeSeries objects are at the core of the openseries package.
79
67
 
@@ -130,21 +118,21 @@ class OpenTimeSeries(_CommonModel[float]):
130
118
  isin: str | None = None
131
119
  label: str | None = None
132
120
 
133
- @FieldValidator("domestic", mode="before")
121
+ @field_validator("domestic", mode="before")
134
122
  @classmethod
135
123
  def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
136
124
  """Pydantic validator to ensure domestic field is validated."""
137
125
  _ = Currency(ccy=value)
138
126
  return value
139
127
 
140
- @FieldValidator("countries", mode="before")
128
+ @field_validator("countries", mode="before")
141
129
  @classmethod
142
130
  def _validate_countries(cls, value: CountriesType) -> CountriesType:
143
131
  """Pydantic validator to ensure countries field is validated."""
144
132
  _ = Countries(countryinput=value)
145
133
  return value
146
134
 
147
- @FieldValidator("markets", mode="before")
135
+ @field_validator("markets", mode="before")
148
136
  @classmethod
149
137
  def _validate_markets(
150
138
  cls,
@@ -164,7 +152,7 @@ class OpenTimeSeries(_CommonModel[float]):
164
152
  raise MarketsNotStringNorListStrError(item_msg)
165
153
  raise MarketsNotStringNorListStrError(msg)
166
154
 
167
- @ModelValidator(mode="after")
155
+ @model_validator(mode="after")
168
156
  def _dates_and_values_validate(self: Self) -> Self:
169
157
  """Pydantic validator to ensure dates and values are validated."""
170
158
  values_list_length = len(self.values)
@@ -220,7 +208,7 @@ class OpenTimeSeries(_CommonModel[float]):
220
208
  Boolean flag indicating if timeseries is in local currency
221
209
 
222
210
  Returns:
223
- -------
211
+ --------
224
212
  OpenTimeSeries
225
213
  An OpenTimeSeries object
226
214
 
@@ -270,7 +258,7 @@ class OpenTimeSeries(_CommonModel[float]):
270
258
  Boolean flag indicating if timeseries is in local currency
271
259
 
272
260
  Returns:
273
- -------
261
+ --------
274
262
  OpenTimeSeries
275
263
  An OpenTimeSeries object
276
264
 
@@ -282,7 +270,7 @@ class OpenTimeSeries(_CommonModel[float]):
282
270
  label, _ = dframe.name
283
271
  else:
284
272
  label = dframe.name
285
- values = cast("list[float]", dframe.to_numpy().tolist())
273
+ values = dframe.to_numpy().tolist()
286
274
  elif isinstance(dframe, DataFrame):
287
275
  values = dframe.iloc[:, column_nmbr].to_list()
288
276
  if isinstance(dframe.columns, MultiIndex):
@@ -301,12 +289,11 @@ class OpenTimeSeries(_CommonModel[float]):
301
289
  msg = f"valuetype missing. Adding: {valuetype.value}"
302
290
  logger.warning(msg=msg)
303
291
  else:
304
- valuetype = cast(
305
- "ValueType",
306
- dframe.columns.get_level_values(1).to_numpy()[column_nmbr],
307
- )
292
+ valuetype = dframe.columns.get_level_values(1).to_numpy()[
293
+ column_nmbr
294
+ ]
308
295
  else:
309
- label = cast("MultiIndex", dframe.columns).to_numpy()[column_nmbr]
296
+ label = dframe.columns.to_numpy()[column_nmbr]
310
297
  else:
311
298
  raise TypeError(msg)
312
299
 
@@ -370,7 +357,7 @@ class OpenTimeSeries(_CommonModel[float]):
370
357
  Boolean flag indicating if timeseries is in local currency
371
358
 
372
359
  Returns:
373
- -------
360
+ --------
374
361
  OpenTimeSeries
375
362
  An OpenTimeSeries object
376
363
 
@@ -409,7 +396,7 @@ class OpenTimeSeries(_CommonModel[float]):
409
396
  """Create copy of OpenTimeSeries object.
410
397
 
411
398
  Returns:
412
- -------
399
+ --------
413
400
  OpenTimeSeries
414
401
  An OpenTimeSeries object
415
402
 
@@ -420,7 +407,7 @@ class OpenTimeSeries(_CommonModel[float]):
420
407
  """Populate .tsdf Pandas DataFrame from the .dates and .values lists.
421
408
 
422
409
  Returns:
423
- -------
410
+ --------
424
411
  OpenTimeSeries
425
412
  An OpenTimeSeries object
426
413
 
@@ -447,7 +434,7 @@ class OpenTimeSeries(_CommonModel[float]):
447
434
  The properties to calculate. Defaults to calculating all available.
448
435
 
449
436
  Returns:
450
- -------
437
+ --------
451
438
  pandas.DataFrame
452
439
  Properties of the OpenTimeSeries
453
440
 
@@ -467,12 +454,11 @@ class OpenTimeSeries(_CommonModel[float]):
467
454
  """Convert series of values into series of returns.
468
455
 
469
456
  Returns:
470
- -------
457
+ --------
471
458
  OpenTimeSeries
472
459
  The returns of the values in the series
473
460
 
474
461
  """
475
- # noinspection PyCallingNonCallable
476
462
  returns = self.tsdf.ffill().pct_change()
477
463
  returns.iloc[0] = 0
478
464
  self.valuetype = ValueType.RTRN
@@ -493,7 +479,7 @@ class OpenTimeSeries(_CommonModel[float]):
493
479
  is calculated
494
480
 
495
481
  Returns:
496
- -------
482
+ --------
497
483
  OpenTimeSeries
498
484
  An OpenTimeSeries object
499
485
 
@@ -513,7 +499,7 @@ class OpenTimeSeries(_CommonModel[float]):
513
499
  """Convert series of returns into cumulative series of values.
514
500
 
515
501
  Returns:
516
- -------
502
+ --------
517
503
  OpenTimeSeries
518
504
  An OpenTimeSeries object
519
505
 
@@ -548,7 +534,7 @@ class OpenTimeSeries(_CommonModel[float]):
548
534
  Convenience divider for when the 1-day rate is not scaled correctly
549
535
 
550
536
  Returns:
551
- -------
537
+ --------
552
538
  OpenTimeSeries
553
539
  An OpenTimeSeries object
554
540
 
@@ -589,7 +575,7 @@ class OpenTimeSeries(_CommonModel[float]):
589
575
  The date offset string that sets the resampled frequency
590
576
 
591
577
  Returns:
592
- -------
578
+ --------
593
579
  OpenTimeSeries
594
580
  An OpenTimeSeries object
595
581
 
@@ -619,7 +605,7 @@ class OpenTimeSeries(_CommonModel[float]):
619
605
  Controls the method used to align values across columns
620
606
 
621
607
  Returns:
622
- -------
608
+ --------
623
609
  OpenTimeSeries
624
610
  An OpenTimeSeries object
625
611
 
@@ -674,7 +660,7 @@ class OpenTimeSeries(_CommonModel[float]):
674
660
  Allows locking the periods-in-a-year to simplify test cases and comparisons
675
661
 
676
662
  Returns:
677
- -------
663
+ --------
678
664
  Pandas.Series[float]
679
665
  Series EWMA volatility
680
666
 
@@ -684,16 +670,14 @@ class OpenTimeSeries(_CommonModel[float]):
684
670
  from_dt=from_date,
685
671
  to_dt=to_date,
686
672
  )
687
- if periods_in_a_year_fixed:
688
- time_factor = float(periods_in_a_year_fixed)
689
- else:
690
- how_many = (
691
- self.tsdf.loc[cast("Timestamp", earlier) : cast("Timestamp", later)]
692
- .count()
693
- .iloc[0]
694
- )
695
- fraction = (later - earlier).days / 365.25
696
- time_factor = how_many / fraction
673
+ time_factor = _calculate_time_factor(
674
+ data=self.tsdf.loc[
675
+ cast("Timestamp", earlier) : cast("Timestamp", later)
676
+ ].iloc[:, 0],
677
+ earlier=earlier,
678
+ later=later,
679
+ periods_in_a_year_fixed=periods_in_a_year_fixed,
680
+ )
697
681
 
698
682
  data = self.tsdf.loc[
699
683
  cast("Timestamp", earlier) : cast("Timestamp", later)
@@ -741,7 +725,7 @@ class OpenTimeSeries(_CommonModel[float]):
741
725
  assumed number of days in a calendar year
742
726
 
743
727
  Returns:
744
- -------
728
+ --------
745
729
  OpenTimeSeries
746
730
  An OpenTimeSeries object
747
731
 
@@ -757,7 +741,6 @@ class OpenTimeSeries(_CommonModel[float]):
757
741
  ra_df = ra_df.dropna()
758
742
 
759
743
  prev = self.first_idx
760
- # noinspection PyTypeChecker
761
744
  dates: list[dt.date] = [prev]
762
745
 
763
746
  for idx, row in ra_df.iterrows():
@@ -803,7 +786,7 @@ class OpenTimeSeries(_CommonModel[float]):
803
786
  If True the level one label is deleted
804
787
 
805
788
  Returns:
806
- -------
789
+ --------
807
790
  OpenTimeSeries
808
791
  An OpenTimeSeries object
809
792
 
@@ -845,7 +828,7 @@ def timeseries_chain(
845
828
  Fee to apply to earlier series
846
829
 
847
830
  Returns:
848
- -------
831
+ --------
849
832
  TypeOpenTimeSeries
850
833
  An OpenTimeSeries object or a subclass thereof
851
834
 
@@ -878,7 +861,6 @@ def timeseries_chain(
878
861
 
879
862
  dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
880
863
 
881
- # noinspection PyTypeChecker
882
864
  return back.__class__(
883
865
  timeseries_id=new.timeseries_id,
884
866
  instrument_id=new.instrument_id,
@@ -907,7 +889,7 @@ def _check_if_none(item: Any) -> bool: # noqa: ANN401
907
889
  variable to be checked
908
890
 
909
891
  Returns:
910
- -------
892
+ --------
911
893
  bool
912
894
  Answer to whether the variable is None or equivalent
913
895