openseries 1.4.12__py3-none-any.whl → 1.5.1__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.
@@ -1507,7 +1507,7 @@ class _CommonModel(BaseModel):
1507
1507
  Compounded Annual Growth Rate (CAGR)
1508
1508
 
1509
1509
  """
1510
- zero: float = 0.0
1510
+ zero = 0.0
1511
1511
  earlier, later = self.calc_range(
1512
1512
  months_offset=months_from_last,
1513
1513
  from_dt=from_date,
openseries/_risk.py CHANGED
@@ -124,9 +124,6 @@ def _calc_inv_vol_weights(returns: DataFrame) -> NDArray[float64]:
124
124
  """
125
125
  Calculate weights proportional to inverse volatility.
126
126
 
127
- Source: https://github.com/pmorissette/ffn.
128
- Function copied here because of FutureWarning from pandas ^2.1.0
129
-
130
127
  Parameters
131
128
  ----------
132
129
  returns: pandas.DataFrame
openseries/frame.py CHANGED
@@ -1,19 +1,35 @@
1
1
  """Defining the OpenFrame class."""
2
+
2
3
  # mypy: disable-error-code="index,assignment"
3
4
  from __future__ import annotations
4
5
 
5
6
  import datetime as dt
6
7
  from copy import deepcopy
7
8
  from functools import reduce
9
+ from inspect import stack
8
10
  from logging import warning
9
- from typing import Optional, Union, cast
11
+ from pathlib import Path
12
+ from typing import Callable, Optional, Union, cast
10
13
 
11
14
  import statsmodels.api as sm # type: ignore[import-untyped,unused-ignore]
12
- from ffn.core import ( # type: ignore[import-untyped,unused-ignore]
13
- calc_erc_weights,
14
- calc_mean_var_weights,
15
+ from numpy import (
16
+ append,
17
+ array,
18
+ cov,
19
+ cumprod,
20
+ dot,
21
+ float64,
22
+ inf,
23
+ linspace,
24
+ log,
25
+ nan,
26
+ sqrt,
27
+ zeros,
28
+ )
29
+ from numpy import (
30
+ sum as npsum,
15
31
  )
16
- from numpy import array, cov, cumprod, log, sqrt
32
+ from numpy.typing import NDArray
17
33
  from pandas import (
18
34
  DataFrame,
19
35
  DatetimeIndex,
@@ -24,7 +40,11 @@ from pandas import (
24
40
  concat,
25
41
  merge,
26
42
  )
27
- from pydantic import field_validator
43
+ from plotly.graph_objs import Figure # type: ignore[import-untyped,unused-ignore]
44
+ from plotly.io import to_html # type: ignore[import-untyped,unused-ignore]
45
+ from plotly.offline import plot # type: ignore[import-untyped,unused-ignore]
46
+ from pydantic import DirectoryPath, field_validator
47
+ from scipy.optimize import minimize # type: ignore[import-untyped,unused-ignore]
28
48
 
29
49
  # noinspection PyProtectedMember
30
50
  from statsmodels.regression.linear_model import ( # type: ignore[import-untyped,unused-ignore]
@@ -38,26 +58,30 @@ from openseries._risk import (
38
58
  _ewma_calc,
39
59
  )
40
60
  from openseries.datefixer import do_resample_to_business_period_ends
61
+ from openseries.load_plotly import load_plotly_dict
41
62
  from openseries.series import OpenTimeSeries
63
+ from openseries.simulation import random_generator
42
64
  from openseries.types import (
43
65
  CountriesType,
44
66
  DaysInYearType,
45
67
  LiteralBizDayFreq,
46
68
  LiteralCaptureRatio,
47
- LiteralCovMethod,
48
69
  LiteralFrameProps,
49
70
  LiteralHowMerge,
71
+ LiteralLinePlotMode,
50
72
  LiteralOlsFitCovType,
51
73
  LiteralOlsFitMethod,
52
74
  LiteralPandasReindexMethod,
75
+ LiteralPlotlyJSlib,
76
+ LiteralPlotlyOutput,
53
77
  LiteralPortfolioWeightings,
54
- LiteralRiskParityMethod,
55
78
  LiteralTrunc,
56
79
  OpenFramePropertiesList,
57
80
  ValueType,
58
81
  )
59
82
 
60
83
 
84
+ # noinspection PyUnresolvedReferences
61
85
  class OpenFrame(_CommonModel):
62
86
 
63
87
  """
@@ -220,7 +244,7 @@ class OpenFrame(_CommonModel):
220
244
  prop_list = [
221
245
  getattr(self, x) for x in OpenFramePropertiesList.allowed_strings
222
246
  ]
223
- return concat(prop_list, axis="columns").T
247
+ return cast(DataFrame, concat(prop_list, axis="columns").T)
224
248
 
225
249
  @property
226
250
  def lengths_of_items(self: Self) -> Series[int]:
@@ -458,8 +482,8 @@ class OpenFrame(_CommonModel):
458
482
  tail = self.tsdf.loc[self.last_indices.min()].copy()
459
483
  dates = do_resample_to_business_period_ends(
460
484
  data=self.tsdf,
461
- head=head, # type: ignore[arg-type]
462
- tail=tail, # type: ignore[arg-type]
485
+ head=head, # type: ignore[arg-type,unused-ignore]
486
+ tail=tail, # type: ignore[arg-type,unused-ignore]
463
487
  freq=freq,
464
488
  countries=countries,
465
489
  )
@@ -637,6 +661,7 @@ class OpenFrame(_CommonModel):
637
661
 
638
662
  """
639
663
  self.constituents += [new_series]
664
+ # noinspection PyUnreachableCode
640
665
  self.tsdf = concat([self.tsdf, new_series.tsdf], axis="columns", sort=True)
641
666
  return self
642
667
 
@@ -841,6 +866,7 @@ class OpenFrame(_CommonModel):
841
866
  :,
842
867
  item,
843
868
  ]
869
+ # noinspection PyTypeChecker
844
870
  relative = 1.0 + longdf - shortdf
845
871
  vol = float(
846
872
  relative.pct_change(fill_method=cast(str, None)).std()
@@ -935,6 +961,7 @@ class OpenFrame(_CommonModel):
935
961
  :,
936
962
  item,
937
963
  ]
964
+ # noinspection PyTypeChecker
938
965
  relative = 1.0 + longdf - shortdf
939
966
  ret = float(
940
967
  relative.pct_change(fill_method=cast(str, None)).mean()
@@ -1445,15 +1472,6 @@ class OpenFrame(_CommonModel):
1445
1472
  self: Self,
1446
1473
  name: str,
1447
1474
  weight_strat: Optional[LiteralPortfolioWeightings] = None,
1448
- initial_weights: Optional[list[float]] = None,
1449
- risk_weights: Optional[list[float]] = None,
1450
- risk_parity_method: LiteralRiskParityMethod = "ccd",
1451
- maximum_iterations: int = 100,
1452
- tolerance: float = 1e-8,
1453
- weight_bounds: tuple[float, float] = (0.0, 1.0),
1454
- riskfree: float = 0.0,
1455
- covar_method: LiteralCovMethod = "ledoit-wolf",
1456
- options: Optional[dict[str, int]] = None,
1457
1475
  ) -> DataFrame:
1458
1476
  """
1459
1477
  Calculate a basket timeseries based on the supplied weights.
@@ -1463,25 +1481,7 @@ class OpenFrame(_CommonModel):
1463
1481
  name: str
1464
1482
  Name of the basket timeseries
1465
1483
  weight_strat: LiteralPortfolioWeightings, optional
1466
- weight calculation from https://github.com/pmorissette/ffn
1467
- initial_weights: list[float], optional
1468
- Starting asset weights, default inverse volatility
1469
- risk_weights: list[float], optional
1470
- Risk target weights, default equal weight
1471
- risk_parity_method: LiteralRiskParityMethod, default: ccd
1472
- Risk parity estimation method
1473
- maximum_iterations: int, default: 100
1474
- Maximum iterations in iterative solutions
1475
- tolerance: float, default: 1e-8
1476
- Tolerance level in iterative solutions
1477
- weight_bounds: tuple[float, float], default: (0.0, 1.0)
1478
- Weigh limits for optimization
1479
- riskfree: float, default: 0.0
1480
- Risk-free rate used in utility calculation
1481
- covar_method: LiteralCovMethod, default: ledoit-wolf
1482
- Covariance matrix estimation method
1483
- options: dict, optional
1484
- options for minimizing, e.g. {'maxiter': 10000 }
1484
+ weight calculation strategies
1485
1485
 
1486
1486
  Returns
1487
1487
  -------
@@ -1507,32 +1507,9 @@ class OpenFrame(_CommonModel):
1507
1507
  if weight_strat:
1508
1508
  if weight_strat == "eq_weights":
1509
1509
  self.weights = [1.0 / self.item_count] * self.item_count
1510
- elif weight_strat == "eq_risk":
1511
- weight_calc = list(
1512
- calc_erc_weights(
1513
- returns=dframe,
1514
- initial_weights=initial_weights,
1515
- risk_weights=risk_weights,
1516
- risk_parity_method=risk_parity_method,
1517
- maximum_iterations=maximum_iterations,
1518
- tolerance=tolerance,
1519
- ),
1520
- )
1521
- self.weights = weight_calc
1522
1510
  elif weight_strat == "inv_vol":
1523
1511
  weight_calc = list(_calc_inv_vol_weights(returns=dframe))
1524
1512
  self.weights = weight_calc
1525
- elif weight_strat == "mean_var":
1526
- weight_calc = list(
1527
- calc_mean_var_weights(
1528
- returns=dframe,
1529
- weight_bounds=weight_bounds,
1530
- rf=riskfree,
1531
- covar_method=covar_method,
1532
- options=options,
1533
- ),
1534
- )
1535
- self.weights = weight_calc
1536
1513
  else:
1537
1514
  msg = "Weight strategy not implemented"
1538
1515
  raise NotImplementedError(msg)
@@ -1718,3 +1695,546 @@ class OpenFrame(_CommonModel):
1718
1695
  )
1719
1696
 
1720
1697
  return DataFrame(corrdf)
1698
+
1699
+
1700
+ def simulate_portfolios(
1701
+ simframe: OpenFrame,
1702
+ num_ports: int,
1703
+ seed: int,
1704
+ ) -> DataFrame:
1705
+ """
1706
+ Generate random weights for simulated portfolios.
1707
+
1708
+ Parameters
1709
+ ----------
1710
+ simframe: OpenFrame
1711
+ Return data for portfolio constituents
1712
+ num_ports: int
1713
+ Number of possible portfolios to simulate
1714
+ seed: int
1715
+ The seed for the random process
1716
+
1717
+ Returns
1718
+ -------
1719
+ pandas.DataFrame
1720
+ The resulting data
1721
+
1722
+ """
1723
+ copi = simframe.from_deepcopy()
1724
+
1725
+ if any(
1726
+ x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
1727
+ ):
1728
+ copi.value_to_ret()
1729
+ log_ret = copi.tsdf.copy()[1:]
1730
+ else:
1731
+ log_ret = copi.tsdf.copy()
1732
+
1733
+ log_ret.columns = log_ret.columns.droplevel(level=1)
1734
+
1735
+ randomizer = random_generator(seed=seed)
1736
+
1737
+ all_weights = zeros((num_ports, simframe.item_count))
1738
+ ret_arr = zeros(num_ports)
1739
+ vol_arr = zeros(num_ports)
1740
+ sharpe_arr = zeros(num_ports)
1741
+
1742
+ for x in range(num_ports):
1743
+ weights = array(randomizer.random(simframe.item_count))
1744
+ weights = weights / npsum(weights)
1745
+ all_weights[x, :] = weights
1746
+
1747
+ vol_arr[x] = sqrt(
1748
+ dot(
1749
+ weights.T,
1750
+ dot(log_ret.cov() * simframe.periods_in_a_year, weights),
1751
+ ),
1752
+ )
1753
+
1754
+ ret_arr[x] = npsum(log_ret.mean() * weights * simframe.periods_in_a_year)
1755
+
1756
+ sharpe_arr[x] = ret_arr[x] / vol_arr[x]
1757
+
1758
+ # noinspection PyUnreachableCode
1759
+ simdf = concat(
1760
+ [
1761
+ DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
1762
+ DataFrame(all_weights, columns=simframe.columns_lvl_zero),
1763
+ ],
1764
+ axis="columns",
1765
+ )
1766
+ simdf = simdf.replace([inf, -inf], nan)
1767
+ return simdf.dropna()
1768
+
1769
+
1770
+ def efficient_frontier( # noqa: C901
1771
+ eframe: OpenFrame,
1772
+ num_ports: int = 5000,
1773
+ seed: int = 71,
1774
+ upperbounds: float = 1.0,
1775
+ frontier_points: int = 200,
1776
+ *,
1777
+ tweak: bool = True,
1778
+ ) -> tuple[DataFrame, DataFrame, NDArray[float64]]:
1779
+ """
1780
+ Identify an efficient frontier.
1781
+
1782
+ Parameters
1783
+ ----------
1784
+ eframe: OpenFrame
1785
+ Portfolio data
1786
+ num_ports: int, default: 5000
1787
+ Number of possible portfolios to simulate
1788
+ seed: int, default: 71
1789
+ The seed for the random process
1790
+ upperbounds: float, default: 1.0
1791
+ The largest allowed allocation to a single asset
1792
+ frontier_points: int, default: 200
1793
+ number of points along frontier to optimize
1794
+ tweak: bool, default: True
1795
+ cutting the frontier to exclude multiple points with almost the same risk
1796
+
1797
+ Returns
1798
+ -------
1799
+ tuple[DataFrame, DataFrame, NDArray[float]]
1800
+ The efficient frontier data, simulation data and optimal portfolio
1801
+
1802
+ """
1803
+ if eframe.weights is None:
1804
+ eframe.weights = [1.0 / eframe.item_count] * eframe.item_count
1805
+
1806
+ copi = eframe.from_deepcopy()
1807
+
1808
+ if any(
1809
+ x == ValueType.PRICE for x in copi.tsdf.columns.get_level_values(1).to_numpy()
1810
+ ):
1811
+ copi.value_to_ret()
1812
+ log_ret = copi.tsdf.copy()[1:]
1813
+ else:
1814
+ log_ret = copi.tsdf.copy()
1815
+
1816
+ log_ret.columns = log_ret.columns.droplevel(level=1)
1817
+
1818
+ simulated = simulate_portfolios(simframe=copi, num_ports=num_ports, seed=seed)
1819
+
1820
+ frontier_min = simulated.loc[simulated["stdev"].idxmin()]["ret"]
1821
+ arithmetic_mean = log_ret.mean() * copi.periods_in_a_year
1822
+ frontier_max = 0.0
1823
+ if isinstance(arithmetic_mean, Series):
1824
+ frontier_max = arithmetic_mean.max()
1825
+
1826
+ def _check_sum(weights: NDArray[float64]) -> float64:
1827
+ return cast(float64, npsum(weights) - 1)
1828
+
1829
+ def _get_ret_vol_sr(
1830
+ lg_ret: DataFrame,
1831
+ weights: NDArray[float64],
1832
+ per_in_yr: float,
1833
+ ) -> NDArray[float64]:
1834
+ ret = npsum(lg_ret.mean() * weights) * per_in_yr
1835
+ volatility = sqrt(dot(weights.T, dot(lg_ret.cov() * per_in_yr, weights)))
1836
+ sr = ret / volatility
1837
+ return cast(NDArray[float64], array([ret, volatility, sr]))
1838
+
1839
+ def _diff_return(
1840
+ lg_ret: DataFrame,
1841
+ weights: NDArray[float64],
1842
+ per_in_yr: float,
1843
+ poss_return: float,
1844
+ ) -> float64:
1845
+ return cast(
1846
+ float64,
1847
+ _get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
1848
+ - poss_return,
1849
+ )
1850
+
1851
+ def _neg_sharpe(weights: NDArray[float64]) -> float64:
1852
+ return cast(
1853
+ float64,
1854
+ _get_ret_vol_sr(
1855
+ lg_ret=log_ret,
1856
+ weights=weights,
1857
+ per_in_yr=eframe.periods_in_a_year,
1858
+ )[2]
1859
+ * -1,
1860
+ )
1861
+
1862
+ def _minimize_volatility(
1863
+ weights: NDArray[float64],
1864
+ ) -> float64:
1865
+ return cast(
1866
+ float64,
1867
+ _get_ret_vol_sr(
1868
+ lg_ret=log_ret,
1869
+ weights=weights,
1870
+ per_in_yr=eframe.periods_in_a_year,
1871
+ )[1],
1872
+ )
1873
+
1874
+ constraints = {"type": "eq", "fun": _check_sum}
1875
+ bounds = tuple((0, upperbounds) for _ in range(eframe.item_count))
1876
+ init_guess = array(eframe.weights)
1877
+
1878
+ opt_results = minimize(
1879
+ fun=_neg_sharpe,
1880
+ x0=init_guess,
1881
+ method="SLSQP",
1882
+ bounds=bounds,
1883
+ constraints=constraints,
1884
+ )
1885
+
1886
+ optimal = _get_ret_vol_sr(
1887
+ lg_ret=log_ret,
1888
+ weights=opt_results.x,
1889
+ per_in_yr=eframe.periods_in_a_year,
1890
+ )
1891
+
1892
+ frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
1893
+ frontier_x = []
1894
+ frontier_weights = []
1895
+
1896
+ for possible_return in frontier_y:
1897
+ cons = cast(
1898
+ dict[str, Union[str, Callable[[float, NDArray[float64]], float64]]],
1899
+ (
1900
+ {"type": "eq", "fun": _check_sum},
1901
+ {
1902
+ "type": "eq",
1903
+ "fun": lambda w, poss_return=possible_return: _diff_return(
1904
+ lg_ret=log_ret,
1905
+ weights=w,
1906
+ per_in_yr=eframe.periods_in_a_year,
1907
+ poss_return=poss_return,
1908
+ ),
1909
+ },
1910
+ ),
1911
+ )
1912
+
1913
+ result = minimize(
1914
+ fun=_minimize_volatility,
1915
+ x0=init_guess,
1916
+ method="SLSQP",
1917
+ bounds=bounds,
1918
+ constraints=cons,
1919
+ )
1920
+
1921
+ frontier_x.append(result["fun"])
1922
+ frontier_weights.append(result["x"])
1923
+
1924
+ # noinspection PyUnreachableCode
1925
+ line_df = concat(
1926
+ [
1927
+ DataFrame(data=frontier_weights, columns=eframe.columns_lvl_zero),
1928
+ DataFrame({"stdev": frontier_x, "ret": frontier_y}),
1929
+ ],
1930
+ axis="columns",
1931
+ )
1932
+ line_df["sharpe"] = line_df.ret / line_df.stdev
1933
+
1934
+ limit_small = 0.0001
1935
+ line_df = line_df.mask(line_df.abs() < limit_small, 0.0)
1936
+ line_df["text"] = line_df.apply(
1937
+ lambda c: "<br>".join(
1938
+ [f"{c[nm]:.1%} - {nm}" for nm in eframe.columns_lvl_zero],
1939
+ ),
1940
+ axis="columns",
1941
+ )
1942
+
1943
+ if tweak:
1944
+ limit_tweak = 0.001
1945
+ line_df["stdev_diff"] = line_df.stdev.pct_change()
1946
+ line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
1947
+ line_df = line_df.drop(columns="stdev_diff")
1948
+
1949
+ return line_df, simulated, append(optimal, opt_results.x)
1950
+
1951
+
1952
+ def constrain_optimized_portfolios(
1953
+ data: OpenFrame,
1954
+ serie: OpenTimeSeries,
1955
+ portfolioname: str = "Current Portfolio",
1956
+ simulations: int = 10000,
1957
+ curve_points: int = 200,
1958
+ upper_bound: float = 0.25,
1959
+ ) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
1960
+ """
1961
+ Constrain optimized portfolios to those that improve on the current one.
1962
+
1963
+ Parameters
1964
+ ----------
1965
+ data: OpenFrame
1966
+ Portfolio data
1967
+ serie: OpenTimeSeries
1968
+ A
1969
+ portfolioname: str, default: "Current Portfolio"
1970
+ Name of the portfolio
1971
+ simulations: int, default: 10000
1972
+ Number of possible portfolios to simulate
1973
+ curve_points: int, default: 200
1974
+ Number of optimal portfolios on the efficient frontier
1975
+ upper_bound: float, default: 0.25
1976
+ The largest allowed allocation to a single asset
1977
+
1978
+ Returns
1979
+ -------
1980
+ tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
1981
+ The constrained optimal portfolio data
1982
+
1983
+ """
1984
+ lr_frame = data.from_deepcopy()
1985
+ mv_frame = data.from_deepcopy()
1986
+
1987
+ front_frame, sim_frame, optimal = efficient_frontier(
1988
+ eframe=data,
1989
+ num_ports=simulations,
1990
+ frontier_points=curve_points,
1991
+ upperbounds=upper_bound,
1992
+ )
1993
+
1994
+ condition_least_ret = front_frame.ret > serie.arithmetic_ret
1995
+ # noinspection PyArgumentList
1996
+ least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
1997
+ least_ret_port = least_ret_frame.iloc[0]
1998
+ least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
1999
+ least_ret_weights = [least_ret_port[c] for c in lr_frame.columns_lvl_zero]
2000
+ lr_frame.weights = least_ret_weights
2001
+ resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
2002
+
2003
+ condition_most_vol = front_frame.stdev < serie.vol
2004
+ # noinspection PyArgumentList
2005
+ most_vol_frame = front_frame[condition_most_vol].sort_values(
2006
+ by="ret",
2007
+ ascending=False,
2008
+ )
2009
+ most_vol_port = most_vol_frame.iloc[0]
2010
+ most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
2011
+ most_vol_weights = [most_vol_port[c] for c in mv_frame.columns_lvl_zero]
2012
+ mv_frame.weights = most_vol_weights
2013
+ resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
2014
+
2015
+ return lr_frame, resleast, mv_frame, resmost
2016
+
2017
+
2018
+ def prepare_plot_data(
2019
+ assets: OpenFrame,
2020
+ current: OpenTimeSeries,
2021
+ optimized: NDArray[float64],
2022
+ ) -> DataFrame:
2023
+ """
2024
+ Prepare date to be used as point_frame in the sharpeplot function.
2025
+
2026
+ Parameters
2027
+ ----------
2028
+ assets: OpenFrame
2029
+ Portfolio data with individual assets and a weighted portfolio
2030
+ current: OpenTimeSeries
2031
+ The current or initial portfolio based on given weights
2032
+ optimized: DataFrame
2033
+ Data optimized with the efficient_frontier method
2034
+
2035
+ Returns
2036
+ -------
2037
+ DataFrame
2038
+ The data prepared with mean returns, volatility and weights
2039
+
2040
+ """
2041
+ txt = "<br>".join(
2042
+ [
2043
+ f"{wgt:.1%} - {nm}"
2044
+ for wgt, nm in zip(
2045
+ cast(list[float], assets.weights),
2046
+ assets.columns_lvl_zero,
2047
+ )
2048
+ ],
2049
+ )
2050
+
2051
+ opt_text = "<br>".join(
2052
+ [
2053
+ f"{wgt:.1%} - {nm}"
2054
+ for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero)
2055
+ ],
2056
+ )
2057
+ vol: Series[float] = assets.vol
2058
+ plotframe = DataFrame(
2059
+ data=[
2060
+ assets.arithmetic_ret,
2061
+ vol,
2062
+ Series(
2063
+ data=[""] * assets.item_count,
2064
+ index=vol.index,
2065
+ ),
2066
+ ],
2067
+ index=["ret", "stdev", "text"],
2068
+ )
2069
+ plotframe.columns = plotframe.columns.droplevel(level=1)
2070
+ plotframe["Max Sharpe Portfolio"] = [optimized[0], optimized[1], opt_text]
2071
+ plotframe[current.label] = [current.arithmetic_ret, current.vol, txt]
2072
+
2073
+ return plotframe
2074
+
2075
+
2076
+ def sharpeplot( # noqa: C901
2077
+ sim_frame: DataFrame = None,
2078
+ line_frame: DataFrame = None,
2079
+ point_frame: DataFrame = None,
2080
+ point_frame_mode: LiteralLinePlotMode = "markers",
2081
+ filename: Optional[str] = None,
2082
+ directory: Optional[DirectoryPath] = None,
2083
+ titletext: Optional[str] = None,
2084
+ output_type: LiteralPlotlyOutput = "file",
2085
+ include_plotlyjs: LiteralPlotlyJSlib = "cdn",
2086
+ *,
2087
+ title: bool = True,
2088
+ add_logo: bool = True,
2089
+ auto_open: bool = True,
2090
+ ) -> tuple[Figure, str]:
2091
+ """
2092
+ Create scatter plot coloured by Sharpe Ratio.
2093
+
2094
+ Parameters
2095
+ ----------
2096
+ sim_frame: DataFrame, optional
2097
+ Data from the simulate_portfolios method.
2098
+ line_frame: DataFrame, optional
2099
+ Data from the efficient_frontier method.
2100
+ point_frame: DataFrame, optional
2101
+ Data to highlight current and efficient portfolios.
2102
+ point_frame_mode: LiteralLinePlotMode, default: markers
2103
+ Which type of scatter to use.
2104
+ filename: str, optional
2105
+ Name of the Plotly html file
2106
+ directory: DirectoryPath, optional
2107
+ Directory where Plotly html file is saved
2108
+ titletext: str, optional
2109
+ Text for the plot title
2110
+ output_type: LiteralPlotlyOutput, default: "file"
2111
+ Determines output type
2112
+ include_plotlyjs: LiteralPlotlyJSlib, default: "cdn"
2113
+ Determines how the plotly.js library is included in the output
2114
+ title: bool, default: True
2115
+ Whether to add standard plot title
2116
+ add_logo: bool, default: True
2117
+ Whether to add Captor logo
2118
+ auto_open: bool, default: True
2119
+ Determines whether to open a browser window with the plot
2120
+
2121
+ Returns
2122
+ -------
2123
+ Figure
2124
+ The scatter plot with simulated and optimized results
2125
+
2126
+ """
2127
+ returns = []
2128
+ risk = []
2129
+
2130
+ if directory:
2131
+ dirpath = Path(directory).resolve()
2132
+ elif Path.home().joinpath("Documents").exists():
2133
+ dirpath = Path.home().joinpath("Documents")
2134
+ else:
2135
+ dirpath = Path(stack()[1].filename).parent
2136
+
2137
+ if not filename:
2138
+ filename = "sharpeplot.html"
2139
+ plotfile = dirpath.joinpath(filename)
2140
+
2141
+ fig, logo = load_plotly_dict()
2142
+ figure = Figure(fig)
2143
+
2144
+ if sim_frame is not None:
2145
+ returns.extend(list(sim_frame.loc[:, "ret"]))
2146
+ risk.extend(list(sim_frame.loc[:, "stdev"]))
2147
+ figure.add_scatter(
2148
+ x=sim_frame.loc[:, "stdev"],
2149
+ y=sim_frame.loc[:, "ret"],
2150
+ hoverinfo="skip",
2151
+ marker={
2152
+ "size": 10,
2153
+ "opacity": 0.5,
2154
+ "color": sim_frame.loc[:, "sharpe"],
2155
+ "colorscale": "Jet",
2156
+ "reversescale": True,
2157
+ "colorbar": {"thickness": 20, "title": "Ratio<br>ret / vol"},
2158
+ },
2159
+ mode="markers",
2160
+ name="simulated portfolios",
2161
+ )
2162
+ if line_frame is not None:
2163
+ returns.extend(list(line_frame.loc[:, "ret"]))
2164
+ risk.extend(list(line_frame.loc[:, "stdev"]))
2165
+ figure.add_scatter(
2166
+ x=line_frame.loc[:, "stdev"],
2167
+ y=line_frame.loc[:, "ret"],
2168
+ text=line_frame.loc[:, "text"],
2169
+ hovertemplate="%{text}<br>Return %{y}<br>Vol %{x}",
2170
+ line={"width": 2.5, "dash": "solid"},
2171
+ mode="lines",
2172
+ name="Efficient frontier",
2173
+ )
2174
+
2175
+ colorway = cast(dict[str, list[str]], fig["layout"]).get("colorway")[
2176
+ : len(point_frame.columns)
2177
+ ]
2178
+
2179
+ if point_frame is not None:
2180
+ for col, clr in zip(point_frame.columns, colorway):
2181
+ returns.extend([point_frame.loc["ret", col]])
2182
+ risk.extend([point_frame.loc["stdev", col]])
2183
+ figure.add_scatter(
2184
+ x=[point_frame.loc["stdev", col]],
2185
+ xhoverformat=".2%",
2186
+ y=[point_frame.loc["ret", col]],
2187
+ yhoverformat=".2%",
2188
+ hovertext=[point_frame.loc["text", col]],
2189
+ hoverinfo="x+y+text+name",
2190
+ marker={"size": 20, "color": clr},
2191
+ mode=point_frame_mode,
2192
+ name=col,
2193
+ text=col,
2194
+ textfont={"size": 14},
2195
+ textposition="bottom center",
2196
+ )
2197
+
2198
+ figure.update_layout(
2199
+ xaxis={"tickformat": ".1%"},
2200
+ xaxis_title="volatility",
2201
+ yaxis={
2202
+ "tickformat": ".1%",
2203
+ "scaleanchor": "x",
2204
+ "scaleratio": 1,
2205
+ },
2206
+ yaxis_title="annual return",
2207
+ showlegend=False,
2208
+ )
2209
+ if title:
2210
+ if titletext is None:
2211
+ titletext = "<b>Risk and Return</b><br>"
2212
+ figure.update_layout(title={"text": titletext, "font": {"size": 32}})
2213
+
2214
+ if add_logo:
2215
+ figure.add_layout_image(logo)
2216
+
2217
+ if output_type == "file":
2218
+ plot(
2219
+ figure_or_data=figure,
2220
+ filename=str(plotfile),
2221
+ auto_open=auto_open,
2222
+ auto_play=False,
2223
+ link_text="",
2224
+ include_plotlyjs=cast(bool, include_plotlyjs),
2225
+ config=fig["config"],
2226
+ output_type=output_type,
2227
+ )
2228
+ string_output = str(plotfile)
2229
+ else:
2230
+ div_id = filename.split(sep=".")[0]
2231
+ string_output = to_html(
2232
+ fig=figure,
2233
+ config=fig["config"],
2234
+ auto_play=False,
2235
+ include_plotlyjs=cast(bool, include_plotlyjs),
2236
+ full_html=False,
2237
+ div_id=div_id,
2238
+ )
2239
+
2240
+ return figure, string_output
openseries/series.py CHANGED
@@ -50,6 +50,7 @@ from openseries.types import (
50
50
  TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
51
51
 
52
52
 
53
+ # noinspection PyUnresolvedReferences
53
54
  class OpenTimeSeries(_CommonModel):
54
55
 
55
56
  """
openseries/simulation.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Defining the ReturnSimulation class."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import datetime as dt
@@ -6,7 +7,12 @@ from typing import Optional, cast
6
7
 
7
8
  from numpy import multiply, sqrt
8
9
  from numpy.random import PCG64, Generator, SeedSequence
9
- from pandas import DataFrame, Index, MultiIndex, concat
10
+ from pandas import (
11
+ DataFrame,
12
+ Index,
13
+ MultiIndex,
14
+ concat,
15
+ )
10
16
  from pydantic import (
11
17
  BaseModel,
12
18
  ConfigDict,
@@ -428,7 +434,7 @@ class ReturnSimulation(BaseModel):
428
434
  Returns
429
435
  -------
430
436
  pandas.DataFrame
431
- Object based on the simulation(s)
437
+ The simulation(s) data
432
438
 
433
439
  """
434
440
  d_range = generate_calendar_date_range(
openseries/types.py CHANGED
@@ -138,8 +138,6 @@ LiteralPlotlyOutput = Literal["file", "div"]
138
138
  LiteralPlotlyJSlib = Literal[True, False, "cdn"]
139
139
  LiteralOlsFitMethod = Literal["pinv", "qr"]
140
140
  LiteralPortfolioWeightings = Literal["eq_weights", "eq_risk", "inv_vol", "mean_var"]
141
- LiteralCovMethod = Literal["ledoit-wolf", "standard"]
142
- LiteralRiskParityMethod = Literal["ccd", "slsqp"]
143
141
  LiteralOlsFitCovType = Literal[
144
142
  "nonrobust",
145
143
  "fixed scale",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openseries
3
- Version: 1.4.12
3
+ Version: 1.5.1
4
4
  Summary: Package for analyzing financial timeseries.
5
5
  Home-page: https://github.com/CaptorAB/OpenSeries
6
6
  License: BSD-3-Clause
@@ -20,7 +20,6 @@ Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Topic :: Office/Business :: Financial :: Investment
23
- Requires-Dist: ffn (>=1.0.0,<2.0.0)
24
23
  Requires-Dist: holidays (>=0.30,<1.0)
25
24
  Requires-Dist: numpy (>=1.23.2,<=2.0.0)
26
25
  Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
@@ -29,6 +28,7 @@ Requires-Dist: plotly (>=5.18.0,<6.0.0)
29
28
  Requires-Dist: pyarrow (>=14.0.2,<16.0.0)
30
29
  Requires-Dist: pydantic (>=2.5.2,<3.0.0)
31
30
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
31
+ Requires-Dist: requests (>=2.20.0,<3.0.0)
32
32
  Requires-Dist: scipy (>=1.11.4,<2.0.0)
33
33
  Requires-Dist: statsmodels (>=0.14.0,<1.0.0)
34
34
  Project-URL: Repository, https://github.com/CaptorAB/OpenSeries
@@ -43,12 +43,13 @@ width="81" height="100" align="left" float="right"/><br/>
43
43
  # OpenSeries
44
44
 
45
45
  [![PyPI version](https://img.shields.io/pypi/v/openseries.svg)](https://pypi.org/project/openseries/)
46
- [![Conda version](
47
- https://anaconda.org/conda-forge/openseries/badges/version.svg)](https://anaconda.org/conda-forge/openseries)
46
+ [![Conda version](https://img.shields.io/conda/vn/conda-forge/openseries.svg)](https://anaconda.org/conda-forge/openseries)
47
+ [![Conda platforms](https://img.shields.io/conda/pn/conda-forge/openseries.svg)](https://anaconda.org/conda-forge/openseries)
48
48
  [![Python version](https://img.shields.io/pypi/pyversions/openseries.svg)](https://www.python.org/)
49
49
  [![GitHub Action Test Suite](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml/badge.svg)](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml)
50
50
  [![Coverage](https://cdn.jsdelivr.net/gh/CaptorAB/OpenSeries@master/coverage.svg)](https://github.com/CaptorAB/OpenSeries/actions/workflows/test.yml)
51
51
  [![Styling, Linting & Type checks](https://github.com/CaptorAB/OpenSeries/actions/workflows/check.yml/badge.svg)](https://github.com/CaptorAB/OpenSeries/actions/workflows/check.yml)
52
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
52
53
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://beta.ruff.rs/docs/)
53
54
  [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
54
55
 
@@ -198,11 +199,11 @@ make lint
198
199
 
199
200
  ### On some files in the project
200
201
 
201
- | File | Description |
202
- |:-----------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
203
- | [series.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) | Defines the class _OpenTimeSeries_ for managing and analyzing a single timeseries. The module also defines a function `timeseries_chain` that can be used to chain two timeseries objects together. |
204
- | [frame.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) | Defines the class _OpenFrame_ for managing a group of timeseries, and e.g. calculate a portfolio timeseries from a rebalancing strategy between timeseries. |
205
- | [simulation.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/simulation.py) | Defines the class _ReturnSimulation_ to create simulated financial timeseries. Used in the project's test suite |
202
+ | File | Description |
203
+ |:-----------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
204
+ | [series.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) | Defines the class _OpenTimeSeries_ for managing and analyzing a single timeseries. The module also defines a function `timeseries_chain` that can be used to chain two timeseries objects together. |
205
+ | [frame.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) | Defines the class _OpenFrame_ for managing a group of timeseries, and e.g. calculate a portfolio timeseries from a rebalancing strategy between timeseries. The module also defines functions to simulate, optimize, and plot portfolios. |
206
+ | [simulation.py](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/simulation.py) | Defines the class _ReturnSimulation_ to create simulated financial timeseries. Used in the project's test suite |
206
207
 
207
208
  ### Class methods used to construct objects.
208
209
 
@@ -271,24 +272,24 @@ make lint
271
272
 
272
273
  ### Methods that apply only to the [OpenFrame](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) class.
273
274
 
274
- | Method | Applies to | Description |
275
- |:------------------------|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
276
- | `merge_series` | `OpenFrame` | Merges the Pandas Dataframes of the constituent OpenTimeSeries. |
277
- | `trunc_frame` | `OpenFrame` | Truncates the OpenFrame to a common period. |
278
- | `add_timeseries` | `OpenFrame` | Adds a given OpenTimeSeries to the OpenFrame. |
279
- | `delete_timeseries` | `OpenFrame` | Deletes an OpenTimeSeries from the OpenFrame. |
280
- | `relative` | `OpenFrame` | Calculates a new series that is the relative performance of two others. |
281
- | `make_portfolio` | `OpenFrame` | Calculates a portfolio timeseries based on the series and weights. Weights can be provided as a list, or a weight strategy can be chosen from *equal weights*, *equal risk*, *inverted volatility* or *minimum variance* as defined in the [ffn](https://github.com/pmorissette/ffn) package. |
282
- | `ord_least_squares_fit` | `OpenFrame` | Performs a regression and an [Ordinary Least Squares](https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html) fit. |
283
- | `beta` | `OpenFrame` | Calculates [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
284
- | `jensen_alpha` | `OpenFrame` | Calculates [Jensen's Alpha](https://www.investopedia.com/terms/j/jensensmeasure.asp) of an asset relative a market. |
285
- | `tracking_error_func` | `OpenFrame` | Calculates the [tracking errors](https://www.investopedia.com/terms/t/trackingerror.asp) relative to a selected series in the OpenFrame. |
286
- | `info_ratio_func` | `OpenFrame` | Calculates the [information ratios](https://www.investopedia.com/terms/i/informationratio.asp) relative to a selected series in the OpenFrame. |
287
- | `capture_ratio_func` | `OpenFrame` | Calculates up, down and up/down [capture ratios](https://www.investopedia.com/terms/d/down-market-capture-ratio.asp) relative to a selected series. |
288
- | `rolling_info_ratio` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [information ratio](https://www.investopedia.com/terms/i/informationratio.asp) between two series. |
289
- | `rolling_beta` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
290
- | `rolling_corr` | `OpenFrame` | Calculates and adds a series of rolling [correlations](https://www.investopedia.com/terms/c/correlation.asp) between two other series. |
291
- | `ewma_risk` | `OpenFrame` | Returns a `pandas.DataFrame` with volatility and correlation based on [Exponentially Weighted Moving Average](https://www.investopedia.com/articles/07/ewma.asp). |
275
+ | Method | Applies to | Description |
276
+ |:------------------------|:------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
277
+ | `merge_series` | `OpenFrame` | Merges the Pandas Dataframes of the constituent OpenTimeSeries. |
278
+ | `trunc_frame` | `OpenFrame` | Truncates the OpenFrame to a common period. |
279
+ | `add_timeseries` | `OpenFrame` | Adds a given OpenTimeSeries to the OpenFrame. |
280
+ | `delete_timeseries` | `OpenFrame` | Deletes an OpenTimeSeries from the OpenFrame. |
281
+ | `relative` | `OpenFrame` | Calculates a new series that is the relative performance of two others. |
282
+ | `make_portfolio` | `OpenFrame` | Calculates a portfolio timeseries based on the series and weights. Weights can be provided as a list, or a weight strategy can be set as *equal weights* or *inverted volatility*. |
283
+ | `ord_least_squares_fit` | `OpenFrame` | Performs a regression and an [Ordinary Least Squares](https://www.statsmodels.org/stable/examples/notebooks/generated/ols.html) fit. |
284
+ | `beta` | `OpenFrame` | Calculates [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
285
+ | `jensen_alpha` | `OpenFrame` | Calculates [Jensen's Alpha](https://www.investopedia.com/terms/j/jensensmeasure.asp) of an asset relative a market. |
286
+ | `tracking_error_func` | `OpenFrame` | Calculates the [tracking errors](https://www.investopedia.com/terms/t/trackingerror.asp) relative to a selected series in the OpenFrame. |
287
+ | `info_ratio_func` | `OpenFrame` | Calculates the [information ratios](https://www.investopedia.com/terms/i/informationratio.asp) relative to a selected series in the OpenFrame. |
288
+ | `capture_ratio_func` | `OpenFrame` | Calculates up, down and up/down [capture ratios](https://www.investopedia.com/terms/d/down-market-capture-ratio.asp) relative to a selected series. |
289
+ | `rolling_info_ratio` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [information ratio](https://www.investopedia.com/terms/i/informationratio.asp) between two series. |
290
+ | `rolling_beta` | `OpenFrame` | Returns a pandas.DataFrame with the rolling [Beta](https://www.investopedia.com/terms/b/beta.asp) of an asset relative a market. |
291
+ | `rolling_corr` | `OpenFrame` | Calculates and adds a series of rolling [correlations](https://www.investopedia.com/terms/c/correlation.asp) between two other series. |
292
+ | `ewma_risk` | `OpenFrame` | Returns a `pandas.DataFrame` with volatility and correlation based on [Exponentially Weighted Moving Average](https://www.investopedia.com/articles/07/ewma.asp). |
292
293
 
293
294
  ### Methods that apply to both the [OpenTimeSeries](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/series.py) and the [OpenFrame](https://github.com/CaptorAB/OpenSeries/blob/master/openseries/frame.py) class.
294
295
 
@@ -0,0 +1,15 @@
1
+ openseries/__init__.py,sha256=hA7I5IFk88EnX6eyBbI1KLT_FGcmPIKF49xa5g3T8Yg,41
2
+ openseries/_common_model.py,sha256=whzIHppEGjXqv2C5ZBg7c-QTCC6dHWBjNKX40WV_c6g,72469
3
+ openseries/_risk.py,sha256=JnwAklqs2G3YIp9KTNKbumqs4VgfFIk-eFs0dZ-_jCY,3299
4
+ openseries/datefixer.py,sha256=_HNiPR6S3agwOAk8gl3wIdegR6uDbh-00J1Zgo4GMl8,12423
5
+ openseries/frame.py,sha256=en_GL0_l9OHsZk6AmYin5Pip7geLyTeLZNAZqIR4xcI,73772
6
+ openseries/load_plotly.py,sha256=kIjvJ2H1sIXWsjd-mZclLvj7ebh-4Hdb1dwB2gR9b-Y,1807
7
+ openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
8
+ openseries/plotly_layouts.json,sha256=ahx8-dL4_RPzvHtBOX0SiL0AH7xQJzNRSDhGrSmU-Og,1429
9
+ openseries/series.py,sha256=jHbJkzFUeGokUJZ1Md4Hx1keSwpNMjPq1r_FDCputIE,28312
10
+ openseries/simulation.py,sha256=_1cYO4VnlRI6khk6Th8ziN5Toz1iry0jFKICHQub3V0,13550
11
+ openseries/types.py,sha256=Qw-ny6IZtdMeuHJF6FvIE0B2stnN3sdqq9fySIKUWK4,7660
12
+ openseries-1.5.1.dist-info/LICENSE.md,sha256=cPUabMxJ6-ziqzqS6aLGkR-ilIOKe_s3Qtyp0ioTmo0,1521
13
+ openseries-1.5.1.dist-info/METADATA,sha256=N1NAZ6l6V2pfc-EZAzF1kVTaLNAg7OrfVr1juoaUPMA,43665
14
+ openseries-1.5.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ openseries-1.5.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- openseries/__init__.py,sha256=hA7I5IFk88EnX6eyBbI1KLT_FGcmPIKF49xa5g3T8Yg,41
2
- openseries/_common_model.py,sha256=G1Ilih_qNJH2jGcYv60uATc0fZYzo-Vc_VBKl1E6wdg,72476
3
- openseries/_risk.py,sha256=u5-gP673_XVtCB0UAYoE1Ikw0gUjIaVgnwj142rqk1Q,3417
4
- openseries/datefixer.py,sha256=_HNiPR6S3agwOAk8gl3wIdegR6uDbh-00J1Zgo4GMl8,12423
5
- openseries/frame.py,sha256=VI5pX44kf4uygYSJrBZBoSPOLkbx3J5EpY37enMFXXY,58779
6
- openseries/load_plotly.py,sha256=kIjvJ2H1sIXWsjd-mZclLvj7ebh-4Hdb1dwB2gR9b-Y,1807
7
- openseries/plotly_captor_logo.json,sha256=F5nhMzEyxKywtjvQqMTKgKRCJQYMDIiBgDSxdte8Clo,178
8
- openseries/plotly_layouts.json,sha256=ahx8-dL4_RPzvHtBOX0SiL0AH7xQJzNRSDhGrSmU-Og,1429
9
- openseries/series.py,sha256=ix-kMUy8H1Kuz7ZeqZ_wSOLCq-_zjm2hrJ63Ytadijc,28274
10
- openseries/simulation.py,sha256=GgBvGdoYyDqjkIlmWwJN3fEhCm952Yv3w8A59XSZwqY,13539
11
- openseries/types.py,sha256=6Sq8tKH5-tIFq7C8K594vT4yZrLCNZ8H6vc6TGPk0r8,7764
12
- openseries-1.4.12.dist-info/LICENSE.md,sha256=cPUabMxJ6-ziqzqS6aLGkR-ilIOKe_s3Qtyp0ioTmo0,1521
13
- openseries-1.4.12.dist-info/METADATA,sha256=mQBTS6jQBiyOz9hps8sAVJ067h-eB9DH2hyQbQUcGa0,45158
14
- openseries-1.4.12.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
15
- openseries-1.4.12.dist-info/RECORD,,