openseries 1.9.7__py3-none-any.whl → 2.0.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.
openseries/frame.py CHANGED
@@ -1,11 +1,4 @@
1
- """Defining 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
+ """The OpenFrame class."""
9
2
 
10
3
  from __future__ import annotations
11
4
 
@@ -18,11 +11,14 @@ from numpy import (
18
11
  array,
19
12
  asarray,
20
13
  concatenate,
14
+ corrcoef,
21
15
  cov,
22
16
  diff,
23
17
  divide,
24
18
  float64,
25
19
  isinf,
20
+ isnan,
21
+ linalg,
26
22
  log,
27
23
  nan,
28
24
  sqrt,
@@ -64,14 +60,19 @@ from .owntypes import (
64
60
  LiteralPandasReindexMethod,
65
61
  LiteralPortfolioWeightings,
66
62
  LiteralTrunc,
63
+ MaxDiversificationNaNError,
64
+ MaxDiversificationNegativeWeightsError,
67
65
  MergingResultedInEmptyError,
68
66
  MixedValuetypesError,
67
+ MultipleCurrenciesError,
69
68
  NoWeightsError,
70
69
  OpenFramePropertiesList,
70
+ PortfolioItemsNotWithinFrameError,
71
71
  RatioInputError,
72
72
  ResampleDataLossError,
73
73
  Self,
74
74
  ValueType,
75
+ WeightsNotProvidedError,
75
76
  )
76
77
  from .series import OpenTimeSeries
77
78
 
@@ -93,7 +94,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
93
94
  List of weights in float format.
94
95
 
95
96
  Returns:
96
- -------
97
+ --------
97
98
  OpenFrame
98
99
  Object of the class OpenFrame
99
100
 
@@ -132,7 +133,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
132
133
  List of weights in float format.
133
134
 
134
135
  Returns:
135
- -------
136
+ --------
136
137
  OpenFrame
137
138
  Object of the class OpenFrame
138
139
 
@@ -164,7 +165,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
164
165
  """Create copy of the OpenFrame object.
165
166
 
166
167
  Returns:
167
- -------
168
+ --------
168
169
  OpenFrame
169
170
  An OpenFrame object
170
171
 
@@ -183,7 +184,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
183
184
  The Pandas merge method.
184
185
 
185
186
  Returns:
186
- -------
187
+ --------
187
188
  OpenFrame
188
189
  An OpenFrame object
189
190
 
@@ -227,7 +228,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
227
228
  The properties to calculate. Defaults to calculating all available.
228
229
 
229
230
  Returns:
230
- -------
231
+ --------
231
232
  pandas.DataFrame
232
233
  Properties of the contituent OpenTimeSeries
233
234
 
@@ -246,7 +247,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
246
247
  """Number of observations of all constituents.
247
248
 
248
249
  Returns:
249
- -------
250
+ --------
250
251
  Pandas.Series[int]
251
252
  Number of observations of all constituents
252
253
 
@@ -262,7 +263,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
262
263
  """Number of constituents.
263
264
 
264
265
  Returns:
265
- -------
266
+ --------
266
267
  int
267
268
  Number of constituents
268
269
 
@@ -274,7 +275,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
274
275
  """Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
275
276
 
276
277
  Returns:
277
- -------
278
+ --------
278
279
  list[str]
279
280
  Level 0 values of the MultiIndex columns in the .tsdf DataFrame
280
281
 
@@ -286,7 +287,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
286
287
  """Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
287
288
 
288
289
  Returns:
289
- -------
290
+ --------
290
291
  list[ValueType]
291
292
  Level 1 values of the MultiIndex columns in the .tsdf DataFrame
292
293
 
@@ -298,7 +299,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
298
299
  """The first dates in the timeseries of all constituents.
299
300
 
300
301
  Returns:
301
- -------
302
+ --------
302
303
  Pandas.Series[dt.date]
303
304
  The first dates in the timeseries of all constituents
304
305
 
@@ -315,7 +316,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
315
316
  """The last dates in the timeseries of all constituents.
316
317
 
317
318
  Returns:
318
- -------
319
+ --------
319
320
  Pandas.Series[dt.date]
320
321
  The last dates in the timeseries of all constituents
321
322
 
@@ -332,7 +333,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
332
333
  """Number of days from the first date to the last for all items in the frame.
333
334
 
334
335
  Returns:
335
- -------
336
+ --------
336
337
  Pandas.Series[int]
337
338
  Number of days from the first date to the last for all
338
339
  items in the frame.
@@ -348,7 +349,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
348
349
  """Convert series of values into series of returns.
349
350
 
350
351
  Returns:
351
- -------
352
+ --------
352
353
  OpenFrame
353
354
  The returns of the values in the series
354
355
 
@@ -374,7 +375,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
374
375
  is calculated
375
376
 
376
377
  Returns:
377
- -------
378
+ --------
378
379
  OpenFrame
379
380
  An OpenFrame object
380
381
 
@@ -393,7 +394,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
393
394
  """Convert series of returns into cumulative series of values.
394
395
 
395
396
  Returns:
396
- -------
397
+ --------
397
398
  OpenFrame
398
399
  An OpenFrame object
399
400
 
@@ -432,7 +433,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
432
433
  The date offset string that sets the resampled frequency
433
434
 
434
435
  Returns:
435
- -------
436
+ --------
436
437
  OpenFrame
437
438
  An OpenFrame object
438
439
 
@@ -481,7 +482,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
481
482
  Controls the method used to align values across columns
482
483
 
483
484
  Returns:
484
- -------
485
+ --------
485
486
  OpenFrame
486
487
  An OpenFrame object
487
488
 
@@ -562,7 +563,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
562
563
  comparisons
563
564
 
564
565
  Returns:
565
- -------
566
+ --------
566
567
  Pandas.DataFrame
567
568
  Series volatilities and correlation
568
569
 
@@ -658,10 +659,13 @@ class OpenFrame(_CommonModel[SeriesFloat]):
658
659
  def correl_matrix(self: Self) -> DataFrame:
659
660
  """Correlation matrix.
660
661
 
662
+ This property returns the correlation matrix of the time series
663
+ in the frame.
664
+
661
665
  Returns:
662
- -------
663
- Pandas.DataFrame
664
- Correlation matrix
666
+ --------
667
+ pandas.DataFrame
668
+ Correlation matrix of the time series in the frame.
665
669
 
666
670
  """
667
671
  corr_matrix = (
@@ -689,7 +693,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
689
693
  The timeseries to add
690
694
 
691
695
  Returns:
692
- -------
696
+ --------
693
697
  OpenFrame
694
698
  An OpenFrame object
695
699
 
@@ -707,7 +711,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
707
711
  The .tsdf column level 0 value of the timeseries to delete
708
712
 
709
713
  Returns:
710
- -------
714
+ --------
711
715
  OpenFrame
712
716
  An OpenFrame object
713
717
 
@@ -746,7 +750,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
746
750
  or end_cut is None.
747
751
 
748
752
  Returns:
749
- -------
753
+ --------
750
754
  OpenFrame
751
755
  An OpenFrame object
752
756
 
@@ -847,7 +851,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
847
851
  comparisons
848
852
 
849
853
  Returns:
850
- -------
854
+ --------
851
855
  Pandas.Series[float]
852
856
  Tracking Errors
853
857
 
@@ -922,7 +926,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
922
926
  comparisons
923
927
 
924
928
  Returns:
925
- -------
929
+ --------
926
930
  Pandas.Series[float]
927
931
  Information Ratios
928
932
 
@@ -1005,7 +1009,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1005
1009
  comparisons
1006
1010
 
1007
1011
  Returns:
1008
- -------
1012
+ --------
1009
1013
  Pandas.Series[float]
1010
1014
  Capture Ratios
1011
1015
 
@@ -1191,7 +1195,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1191
1195
  Variance bias factor taking the value 0 or 1.
1192
1196
 
1193
1197
  Returns:
1194
- -------
1198
+ --------
1195
1199
  float
1196
1200
  Beta as Co-variance of x & y divided by Variance of x
1197
1201
 
@@ -1260,7 +1264,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1260
1264
  If True the fit is added as a new column in the .tsdf Pandas.DataFrame
1261
1265
 
1262
1266
  Returns:
1263
- -------
1267
+ --------
1264
1268
  dict[str, float]
1265
1269
  A dictionary with the coefficient, intercept and rsquared outputs.
1266
1270
 
@@ -1329,7 +1333,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1329
1333
  Variance bias factor taking the value 0 or 1.
1330
1334
 
1331
1335
  Returns:
1332
- -------
1336
+ --------
1333
1337
  float
1334
1338
  Jensen's alpha
1335
1339
 
@@ -1401,7 +1405,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1401
1405
  weight calculation strategies
1402
1406
 
1403
1407
  Returns:
1404
- -------
1408
+ --------
1405
1409
  Pandas.DataFrame
1406
1410
  A basket timeseries
1407
1411
 
@@ -1431,6 +1435,60 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1431
1435
  vol = divide(1.0, std(returns, axis=0, ddof=1))
1432
1436
  vol[isinf(vol)] = nan
1433
1437
  self.weights = list(divide(vol, vol.sum()))
1438
+ elif weight_strat == "max_div":
1439
+ corr_matrix = corrcoef(returns.T)
1440
+ corr_matrix[isinf(corr_matrix)] = nan
1441
+ corr_matrix[isnan(corr_matrix)] = nan
1442
+
1443
+ msga = (
1444
+ "max_div weight strategy failed: "
1445
+ "correlation matrix contains NaN values"
1446
+ )
1447
+ if isnan(corr_matrix).any():
1448
+ raise MaxDiversificationNaNError(msga)
1449
+
1450
+ try:
1451
+ inv_corr_sum = linalg.inv(corr_matrix).sum(axis=1)
1452
+
1453
+ msgb = (
1454
+ "max_div weight strategy failed: "
1455
+ "inverse correlation matrix sum contains NaN values"
1456
+ )
1457
+ if isnan(inv_corr_sum).any():
1458
+ raise MaxDiversificationNaNError(msgb)
1459
+
1460
+ self.weights = list(divide(inv_corr_sum, inv_corr_sum.sum()))
1461
+
1462
+ msgc = (
1463
+ "max_div weight strategy failed: "
1464
+ "final weights contain NaN values"
1465
+ )
1466
+ if any( # pragma: no cover
1467
+ isnan(weight) for weight in self.weights
1468
+ ):
1469
+ raise MaxDiversificationNaNError(msgc)
1470
+
1471
+ msgd = (
1472
+ "max_div weight strategy failed: negative weights detected"
1473
+ f" - weights: {[round(w, 6) for w in self.weights]}"
1474
+ )
1475
+ if any(weight < 0 for weight in self.weights):
1476
+ raise MaxDiversificationNegativeWeightsError(msgd)
1477
+
1478
+ except linalg.LinAlgError as e:
1479
+ msge = (
1480
+ "max_div weight strategy failed: "
1481
+ f"correlation matrix is singular - {e!s}"
1482
+ )
1483
+ raise MaxDiversificationNaNError(msge) from e
1484
+ elif weight_strat == "min_vol_overweight":
1485
+ vols = std(returns, axis=0, ddof=1)
1486
+ min_vol_idx = vols.argmin()
1487
+ min_vol_weight = 0.6
1488
+ remaining_weight = 0.4
1489
+ weights = [remaining_weight / (self.item_count - 1)] * self.item_count
1490
+ weights[min_vol_idx] = min_vol_weight
1491
+ self.weights = weights
1434
1492
  else:
1435
1493
  raise NotImplementedError(msg)
1436
1494
 
@@ -1466,7 +1524,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1466
1524
  Allows locking the periods-in-a-year to simplify test cases and comparisons
1467
1525
 
1468
1526
  Returns:
1469
- -------
1527
+ --------
1470
1528
  Pandas.DataFrame
1471
1529
  Rolling Information Ratios
1472
1530
 
@@ -1532,7 +1590,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1532
1590
  Variance bias factor taking the value 0 or 1.
1533
1591
 
1534
1592
  Returns:
1535
- -------
1593
+ --------
1536
1594
  Pandas.DataFrame
1537
1595
  Rolling Betas
1538
1596
 
@@ -1592,7 +1650,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1592
1650
  The length of the rolling window to use is set as number of observations
1593
1651
 
1594
1652
  Returns:
1595
- -------
1653
+ --------
1596
1654
  Pandas.DataFrame
1597
1655
  Rolling Correlations
1598
1656
 
@@ -1637,7 +1695,7 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1637
1695
  to use as the dependent variable
1638
1696
 
1639
1697
  Returns:
1640
- -------
1698
+ --------
1641
1699
  tuple[pandas.DataFrame, OpenTimeSeries]
1642
1700
  - A DataFrame with the R-squared, the intercept
1643
1701
  and the regression coefficients
@@ -1677,3 +1735,271 @@ class OpenFrame(_CommonModel[SeriesFloat]):
1677
1735
  result = DataFrame(data=output, index=indx, columns=[dependent_column[0]])
1678
1736
 
1679
1737
  return result, predictions.to_cumret()
1738
+
1739
+ def rebalanced_portfolio(
1740
+ self: Self,
1741
+ name: str,
1742
+ items: list[str] | None = None,
1743
+ bal_weights: list[float] | None = None,
1744
+ frequency: int = 1,
1745
+ cash_index: OpenTimeSeries | None = None,
1746
+ *,
1747
+ equal_weights: bool = False,
1748
+ drop_extras: bool = True,
1749
+ ) -> OpenFrame:
1750
+ """Create a rebalanced portfolio from the OpenFrame constituents.
1751
+
1752
+ Parameters
1753
+ ----------
1754
+ name: str
1755
+ Name of the portfolio
1756
+ items: list[str], optional
1757
+ List of items to include in the portfolio. If None, uses all items.
1758
+ bal_weights: list[float], optional
1759
+ List of weights for rebalancing. If None, uses frame weights.
1760
+ frequency: int, default: 1
1761
+ Rebalancing frequency
1762
+ cash_index: OpenTimeSeries, optional
1763
+ Cash index series for cash component
1764
+ equal_weights: bool, default: False
1765
+ If True, use equal weights for all items
1766
+ drop_extras: bool, default: True
1767
+ If True, only return TWR series; if False, return all details
1768
+
1769
+ Returns:
1770
+ --------
1771
+ OpenFrame
1772
+ OpenFrame containing the rebalanced portfolio
1773
+
1774
+ """
1775
+ if bal_weights is None and not equal_weights:
1776
+ if self.weights is None:
1777
+ msg = "Weights must be provided."
1778
+ raise WeightsNotProvidedError(msg)
1779
+ bal_weights = list(self.weights)
1780
+
1781
+ if items is None:
1782
+ items = list(self.columns_lvl_zero)
1783
+ else:
1784
+ msg = "Items must be passed as list."
1785
+ if not isinstance(items, list):
1786
+ raise TypeError(msg)
1787
+ if not items:
1788
+ msg = "Items for portfolio must be within SeriesFrame items."
1789
+ raise PortfolioItemsNotWithinFrameError(msg)
1790
+ if not set(items) <= set(self.columns_lvl_zero):
1791
+ msg = "Items for portfolio must be within SeriesFrame items."
1792
+ raise PortfolioItemsNotWithinFrameError(msg)
1793
+
1794
+ if equal_weights:
1795
+ bal_weights = [1 / len(items)] * len(items)
1796
+
1797
+ if cash_index:
1798
+ cash_index.tsdf = cash_index.tsdf.reindex(self.tsdf.index)
1799
+ cash_values: list[float] = cast(
1800
+ "list[float]", cash_index.tsdf.iloc[:, 0].to_numpy().tolist()
1801
+ )
1802
+ else:
1803
+ cash_values = [1.0] * self.length
1804
+
1805
+ if self.tsdf.isna().to_numpy().any():
1806
+ self.value_nan_handle()
1807
+
1808
+ ccies = list({serie.currency for serie in self.constituents})
1809
+
1810
+ if len(ccies) != 1:
1811
+ msg = "Items for portfolio must be denominated in same currency."
1812
+ raise MultipleCurrenciesError(msg)
1813
+
1814
+ currency = ccies[0]
1815
+
1816
+ instruments = [*items, "cash", name]
1817
+ subheaders = [
1818
+ ValueType.PRICE,
1819
+ "buysell_qty",
1820
+ "position",
1821
+ "value",
1822
+ "twr",
1823
+ "settle",
1824
+ ]
1825
+
1826
+ output = {
1827
+ item: {
1828
+ ValueType.PRICE: [],
1829
+ "buysell_qty": [0.0] * self.length,
1830
+ "position": [0.0] * self.length,
1831
+ "value": [0.0] * self.length,
1832
+ "twr": [0.0] * self.length,
1833
+ "settle": [0.0] * self.length,
1834
+ }
1835
+ for item in items
1836
+ }
1837
+ output.update(
1838
+ {
1839
+ "cash": {
1840
+ ValueType.PRICE: cash_values,
1841
+ "buysell_qty": [0.0] * self.length,
1842
+ "position": [0.0] * self.length,
1843
+ "value": [0.0] * self.length,
1844
+ "twr": [0.0] * self.length,
1845
+ "settle": [0.0] * self.length,
1846
+ },
1847
+ name: {
1848
+ ValueType.PRICE: [1.0] + [0.0] * (self.length - 1),
1849
+ "buysell_qty": [-1.0] + [0.0] * (self.length - 1),
1850
+ "position": [-1.0] + [0.0] * (self.length - 1),
1851
+ "value": [-1.0] + [0.0] * (self.length - 1),
1852
+ "twr": [1.0] + [0.0] * (self.length - 1),
1853
+ "settle": [1.0] + [0.0] * (self.length - 1),
1854
+ },
1855
+ },
1856
+ )
1857
+
1858
+ for item, weight in zip(items, cast("list[float]", bal_weights), strict=False):
1859
+ output[item][ValueType.PRICE] = cast(
1860
+ "list[float]", self.tsdf[(item, ValueType.PRICE)].to_numpy().tolist()
1861
+ )
1862
+ output[item]["buysell_qty"][0] = (
1863
+ weight / self.tsdf[(item, ValueType.PRICE)].iloc[0]
1864
+ )
1865
+ output[item]["position"][0] = output[item]["buysell_qty"][0]
1866
+ output[item]["value"][0] = (
1867
+ output[item]["position"][0] * output[item][ValueType.PRICE][0]
1868
+ )
1869
+ output[item]["settle"][0] = (
1870
+ -output[item]["buysell_qty"][0] * output[item][ValueType.PRICE][0]
1871
+ )
1872
+ output["cash"]["buysell_qty"][0] += output[item]["settle"][0]
1873
+ output[item]["twr"][0] = (
1874
+ output[item]["value"][0] / -output[item]["settle"][0]
1875
+ )
1876
+
1877
+ output["cash"]["position"][0] = (
1878
+ output["cash"]["buysell_qty"][0] + output[name]["settle"][0]
1879
+ )
1880
+ output["cash"]["settle"][0] = -output["cash"]["position"][0]
1881
+
1882
+ counter = 1
1883
+ for day in range(1, self.length):
1884
+ portfolio_value = 0.0
1885
+ settle_value = 0.0
1886
+ if day == frequency * counter:
1887
+ for item, weight in zip(
1888
+ items, cast("list[float]", bal_weights), strict=False
1889
+ ):
1890
+ output[item]["buysell_qty"][day] = (
1891
+ weight
1892
+ - output[item]["value"][day - 1]
1893
+ / -output[name]["value"][day - 1]
1894
+ ) / output[item][ValueType.PRICE][day]
1895
+ output[item]["position"][day] = (
1896
+ output[item]["position"][day - 1]
1897
+ + output[item]["buysell_qty"][day]
1898
+ )
1899
+ output[item]["value"][day] = (
1900
+ output[item]["position"][day]
1901
+ * output[item][ValueType.PRICE][day]
1902
+ )
1903
+ portfolio_value += output[item]["value"][day]
1904
+ output[item]["twr"][day] = (
1905
+ output[item]["value"][day]
1906
+ / (
1907
+ output[item]["value"][day - 1]
1908
+ - output[item]["settle"][day]
1909
+ )
1910
+ * output[item]["twr"][day - 1]
1911
+ )
1912
+ output[item]["settle"][day] = (
1913
+ -output[item]["buysell_qty"][day]
1914
+ * output[item][ValueType.PRICE][day]
1915
+ )
1916
+ settle_value += output[item]["settle"][day]
1917
+ counter += 1
1918
+ else:
1919
+ for item in items:
1920
+ output[item]["position"][day] = output[item]["position"][day - 1]
1921
+ output[item]["value"][day] = (
1922
+ output[item]["position"][day]
1923
+ * output[item][ValueType.PRICE][day]
1924
+ )
1925
+ portfolio_value += output[item]["value"][day]
1926
+ output[item]["twr"][day] = (
1927
+ output[item]["value"][day]
1928
+ / (
1929
+ output[item]["value"][day - 1]
1930
+ - output[item]["settle"][day]
1931
+ )
1932
+ * output[item]["twr"][day - 1]
1933
+ )
1934
+ output["cash"]["buysell_qty"][day] = settle_value
1935
+ output["cash"]["position"][day] = (
1936
+ output["cash"]["position"][day - 1]
1937
+ * output["cash"][ValueType.PRICE][day]
1938
+ / output["cash"][ValueType.PRICE][day - 1]
1939
+ + output["cash"]["buysell_qty"][day]
1940
+ )
1941
+ output["cash"]["value"][day] = output["cash"]["position"][day]
1942
+ portfolio_value += output["cash"]["value"][day]
1943
+ output[name]["position"][day] = output[name]["position"][day - 1]
1944
+ output[name]["value"][day] = -portfolio_value
1945
+ output[name]["twr"][day] = (
1946
+ output[name]["value"][day] / output[name]["position"][day]
1947
+ )
1948
+ output[name][ValueType.PRICE][day] = output[name]["twr"][day]
1949
+
1950
+ result = DataFrame()
1951
+ for outvalue in output.values():
1952
+ result = concat(
1953
+ [
1954
+ result,
1955
+ DataFrame(
1956
+ data=outvalue,
1957
+ index=self.tsdf.index,
1958
+ ),
1959
+ ],
1960
+ axis="columns",
1961
+ )
1962
+ lvlone, lvltwo = [], []
1963
+ for instr in instruments:
1964
+ lvlone.extend([instr] * 6)
1965
+ lvltwo.extend(subheaders)
1966
+ result.columns = MultiIndex.from_arrays([lvlone, lvltwo])
1967
+
1968
+ series = []
1969
+ if drop_extras:
1970
+ used_constituents = [
1971
+ item for item in self.constituents if item.label in items
1972
+ ]
1973
+ series.extend(
1974
+ [
1975
+ OpenTimeSeries.from_df(
1976
+ dframe=result[(item.label, "twr")],
1977
+ valuetype=ValueType.PRICE,
1978
+ baseccy=item.currency,
1979
+ local_ccy=item.local_ccy,
1980
+ )
1981
+ for item in used_constituents
1982
+ ]
1983
+ )
1984
+ series.append(
1985
+ OpenTimeSeries.from_df(
1986
+ dframe=result[(name, "twr")],
1987
+ valuetype=ValueType.PRICE,
1988
+ baseccy=currency,
1989
+ local_ccy=True,
1990
+ ),
1991
+ )
1992
+ else:
1993
+ series.extend(
1994
+ [
1995
+ OpenTimeSeries.from_df(
1996
+ dframe=result.loc[:, col],
1997
+ valuetype=ValueType.PRICE,
1998
+ baseccy=currency,
1999
+ local_ccy=True,
2000
+ ).set_new_label(f"{col[0]}, {col[1]!s}")
2001
+ for col in result.columns
2002
+ ]
2003
+ )
2004
+
2005
+ return OpenFrame(series)
openseries/load_plotly.py CHANGED
@@ -1,11 +1,4 @@
1
- """Function to load plotly layout and configuration from local json file.
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
+ """Function to load plotly layout and configuration from local json file."""
9
2
 
10
3
  from __future__ import annotations
11
4
 
@@ -34,7 +27,7 @@ def _check_remote_file_existence(url: str) -> bool:
34
27
  Path to remote file
35
28
 
36
29
  Returns:
37
- -------
30
+ --------
38
31
  bool
39
32
  True if url is valid and False otherwise
40
33
 
@@ -62,7 +55,7 @@ def load_plotly_dict(
62
55
  Flag whether to load as responsive
63
56
 
64
57
  Returns:
65
- -------
58
+ --------
66
59
  tuple[PlotlyLayoutType, CaptorLogoType]
67
60
  A dictionary with the Plotly config and layout template
68
61