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/__init__.py +2 -7
- openseries/_common_model.py +151 -90
- openseries/_risk.py +3 -10
- openseries/datefixer.py +9 -16
- openseries/frame.py +369 -43
- openseries/load_plotly.py +3 -10
- openseries/owntypes.py +24 -9
- openseries/portfoliotools.py +6 -13
- openseries/report.py +3 -10
- openseries/series.py +18 -25
- openseries/simulation.py +12 -19
- openseries-2.0.1.dist-info/METADATA +128 -0
- openseries-2.0.1.dist-info/RECORD +18 -0
- openseries-1.9.7.dist-info/METADATA +0 -365
- openseries-1.9.7.dist-info/RECORD +0 -18
- {openseries-1.9.7.dist-info → openseries-2.0.1.dist-info}/WHEEL +0 -0
- {openseries-1.9.7.dist-info → openseries-2.0.1.dist-info}/licenses/LICENSE.md +0 -0
openseries/frame.py
CHANGED
@@ -1,11 +1,4 @@
|
|
1
|
-
"""
|
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
|
-
|
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
|
|