skfolio 0.3.1__py3-none-any.whl → 0.4.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.
- skfolio/datasets/_base.py +1 -1
- skfolio/measures/_measures.py +1 -1
- skfolio/model_selection/_walk_forward.py +265 -51
- skfolio/population/_population.py +215 -152
- skfolio/portfolio/_base.py +48 -9
- skfolio/portfolio/_multi_period_portfolio.py +45 -0
- skfolio/portfolio/_portfolio.py +82 -46
- skfolio/utils/tools.py +18 -1
- {skfolio-0.3.1.dist-info → skfolio-0.4.0.dist-info}/METADATA +1 -1
- {skfolio-0.3.1.dist-info → skfolio-0.4.0.dist-info}/RECORD +13 -13
- {skfolio-0.3.1.dist-info → skfolio-0.4.0.dist-info}/WHEEL +1 -1
- {skfolio-0.3.1.dist-info → skfolio-0.4.0.dist-info}/LICENSE +0 -0
- {skfolio-0.3.1.dist-info → skfolio-0.4.0.dist-info}/top_level.txt +0 -0
@@ -16,9 +16,10 @@ import plotly.graph_objects as go
|
|
16
16
|
import scipy.interpolate as sci
|
17
17
|
|
18
18
|
import skfolio.typing as skt
|
19
|
+
from skfolio.measures import RatioMeasure
|
19
20
|
from skfolio.portfolio import BasePortfolio, MultiPeriodPortfolio
|
20
21
|
from skfolio.utils.sorting import non_denominated_sort
|
21
|
-
from skfolio.utils.tools import deduplicate_names
|
22
|
+
from skfolio.utils.tools import deduplicate_names, optimal_rounding_decimals
|
22
23
|
|
23
24
|
|
24
25
|
class Population(list):
|
@@ -201,94 +202,62 @@ class Population(list):
|
|
201
202
|
def measures(
|
202
203
|
self,
|
203
204
|
measure: skt.Measure,
|
204
|
-
names: skt.Names | None = None,
|
205
|
-
tags: skt.Tags | None = None,
|
206
205
|
) -> np.ndarray:
|
207
206
|
"""Vector of portfolios measures for each portfolio from the
|
208
|
-
population
|
207
|
+
population.
|
209
208
|
|
210
209
|
Parameters
|
211
210
|
----------
|
212
211
|
measure : Measure
|
213
212
|
The portfolio measure.
|
214
213
|
|
215
|
-
names : str | list[str], optional
|
216
|
-
If provided, the population is filtered by portfolio names.
|
217
|
-
|
218
|
-
tags : str | list[str], optional
|
219
|
-
If provided, the population is filtered by portfolio tags.
|
220
|
-
|
221
214
|
Returns
|
222
215
|
-------
|
223
216
|
values : ndarray
|
224
217
|
The vector of portfolios measures.
|
225
218
|
"""
|
226
|
-
|
227
|
-
return np.array([ptf.__getattribute__(measure.value) for ptf in population])
|
219
|
+
return np.array([ptf.__getattribute__(measure.value) for ptf in self])
|
228
220
|
|
229
221
|
def measures_mean(
|
230
222
|
self,
|
231
223
|
measure: skt.Measure,
|
232
|
-
names: skt.Names | None = None,
|
233
|
-
tags: skt.Tags | None = None,
|
234
224
|
) -> float:
|
235
225
|
"""Mean of portfolios measures for each portfolio from the
|
236
|
-
population
|
226
|
+
population.
|
237
227
|
|
238
228
|
Parameters
|
239
229
|
----------
|
240
230
|
measure : Measure
|
241
231
|
The portfolio measure.
|
242
232
|
|
243
|
-
names : str | list[str], optional
|
244
|
-
If provided, the population is filtered by portfolio names.
|
245
|
-
|
246
|
-
tags : str | list[str], optional
|
247
|
-
If provided, the population is filtered by portfolio tags.
|
248
|
-
|
249
233
|
Returns
|
250
234
|
-------
|
251
235
|
value : float
|
252
236
|
The mean of portfolios measures.
|
253
237
|
"""
|
254
|
-
return self.measures(measure=measure
|
238
|
+
return self.measures(measure=measure).mean()
|
255
239
|
|
256
240
|
def measures_std(
|
257
241
|
self,
|
258
242
|
measure: skt.Measure,
|
259
|
-
names: skt.Names | None = None,
|
260
|
-
tags: skt.Tags | None = None,
|
261
243
|
) -> float:
|
262
244
|
"""Standard-deviation of portfolios measures for each portfolio from the
|
263
|
-
population
|
245
|
+
population.
|
264
246
|
|
265
247
|
Parameters
|
266
248
|
----------
|
267
249
|
measure : Measure
|
268
250
|
The portfolio measure.
|
269
251
|
|
270
|
-
names : str | list[str], optional
|
271
|
-
If provided, the population is filtered by portfolio names.
|
272
|
-
|
273
|
-
tags : str | list[str], optional
|
274
|
-
If provided, the population is filtered by portfolio tags.
|
275
|
-
|
276
252
|
Returns
|
277
253
|
-------
|
278
254
|
value : float
|
279
255
|
The standard-deviation of portfolios measures.
|
280
256
|
"""
|
281
|
-
return self.measures(measure=measure
|
257
|
+
return self.measures(measure=measure).std()
|
282
258
|
|
283
|
-
def sort_measure(
|
284
|
-
|
285
|
-
measure: skt.Measure,
|
286
|
-
reverse: bool = False,
|
287
|
-
names: skt.Names | None = None,
|
288
|
-
tags: skt.Tags | None = None,
|
289
|
-
) -> "Population":
|
290
|
-
"""Sort the population by a given portfolio measure and filter the portfolios
|
291
|
-
by names and tags.
|
259
|
+
def sort_measure(self, measure: skt.Measure, reverse: bool = False) -> "Population":
|
260
|
+
"""Sort the population by a given portfolio measure.
|
292
261
|
|
293
262
|
Parameters
|
294
263
|
----------
|
@@ -298,21 +267,14 @@ class Population(list):
|
|
298
267
|
reverse : bool, default=False
|
299
268
|
If this is set to True, the order is reversed.
|
300
269
|
|
301
|
-
names : str | list[str], optional
|
302
|
-
If provided, the population is filtered by portfolio names.
|
303
|
-
|
304
|
-
tags : str | list[str], optional
|
305
|
-
If provided, the population is filtered by portfolio tags.
|
306
|
-
|
307
270
|
Returns
|
308
271
|
-------
|
309
272
|
values : Populations
|
310
273
|
The sorted population.
|
311
274
|
"""
|
312
|
-
population = self.filter(names=names, tags=tags)
|
313
275
|
return self.__class__(
|
314
276
|
sorted(
|
315
|
-
|
277
|
+
self,
|
316
278
|
key=lambda x: x.__getattribute__(measure.value),
|
317
279
|
reverse=reverse,
|
318
280
|
)
|
@@ -322,8 +284,6 @@ class Population(list):
|
|
322
284
|
self,
|
323
285
|
measure: skt.Measure,
|
324
286
|
q: float,
|
325
|
-
names: skt.Names | None = None,
|
326
|
-
tags: skt.Tags | None = None,
|
327
287
|
) -> BasePortfolio:
|
328
288
|
"""Returns the portfolio corresponding to the `q` quantile for a given portfolio
|
329
289
|
measure.
|
@@ -336,12 +296,6 @@ class Population(list):
|
|
336
296
|
q : float
|
337
297
|
The quantile value.
|
338
298
|
|
339
|
-
names : str | list[str], optional
|
340
|
-
If provided, the population is filtered by portfolio names.
|
341
|
-
|
342
|
-
tags : str | list[str], optional
|
343
|
-
If provided, the population is filtered by portfolio tags.
|
344
|
-
|
345
299
|
Returns
|
346
300
|
-------
|
347
301
|
values : BasePortfolio
|
@@ -349,17 +303,13 @@ class Population(list):
|
|
349
303
|
"""
|
350
304
|
if not 0 <= q <= 1:
|
351
305
|
raise ValueError("The quantile`q` must be between 0 and 1")
|
352
|
-
sorted_portfolios = self.sort_measure(
|
353
|
-
measure=measure, reverse=False, names=names, tags=tags
|
354
|
-
)
|
306
|
+
sorted_portfolios = self.sort_measure(measure=measure, reverse=False)
|
355
307
|
k = max(0, int(np.round(len(sorted_portfolios) * q)) - 1)
|
356
308
|
return sorted_portfolios[k]
|
357
309
|
|
358
310
|
def min_measure(
|
359
311
|
self,
|
360
312
|
measure: skt.Measure,
|
361
|
-
names: skt.Names | None = None,
|
362
|
-
tags: skt.Tags | None = None,
|
363
313
|
) -> BasePortfolio:
|
364
314
|
"""Returns the portfolio with the minimum measure.
|
365
315
|
|
@@ -368,24 +318,16 @@ class Population(list):
|
|
368
318
|
measure : Measure
|
369
319
|
The portfolio measure.
|
370
320
|
|
371
|
-
names : str | list[str], optional
|
372
|
-
If provided, the population is filtered by portfolio names.
|
373
|
-
|
374
|
-
tags : str | list[str], optional
|
375
|
-
If provided, the population is filtered by portfolio tags.
|
376
|
-
|
377
321
|
Returns
|
378
322
|
-------
|
379
323
|
values : BasePortfolio
|
380
324
|
The portfolio with minimum measure.
|
381
325
|
"""
|
382
|
-
return self.quantile(measure=measure, q=0
|
326
|
+
return self.quantile(measure=measure, q=0)
|
383
327
|
|
384
328
|
def max_measure(
|
385
329
|
self,
|
386
330
|
measure: skt.Measure,
|
387
|
-
names: skt.Names | None = None,
|
388
|
-
tags: skt.Tags | None = None,
|
389
331
|
) -> BasePortfolio:
|
390
332
|
"""Returns the portfolio with the maximum measure.
|
391
333
|
|
@@ -394,24 +336,16 @@ class Population(list):
|
|
394
336
|
measure: Measure
|
395
337
|
The portfolio measure.
|
396
338
|
|
397
|
-
names : str | list[str], optional
|
398
|
-
If provided, the population is filtered by portfolio names.
|
399
|
-
|
400
|
-
tags : str | list[str], optional
|
401
|
-
If provided, the population is filtered by portfolio tags.
|
402
|
-
|
403
339
|
Returns
|
404
340
|
-------
|
405
341
|
values : BasePortfolio
|
406
342
|
The portfolio with maximum measure.
|
407
343
|
"""
|
408
|
-
return self.quantile(measure=measure, q=1
|
344
|
+
return self.quantile(measure=measure, q=1)
|
409
345
|
|
410
346
|
def summary(
|
411
347
|
self,
|
412
348
|
formatted: bool = True,
|
413
|
-
names: skt.Names | None = None,
|
414
|
-
tags: skt.Tags | None = None,
|
415
349
|
) -> pd.DataFrame:
|
416
350
|
"""Summary of the portfolios in the population
|
417
351
|
|
@@ -422,69 +356,135 @@ class Population(list):
|
|
422
356
|
units.
|
423
357
|
The default is `True`.
|
424
358
|
|
425
|
-
names : str | list[str], optional
|
426
|
-
If provided, the population is filtered by portfolio names.
|
427
|
-
|
428
|
-
tags : str | list[str], optional
|
429
|
-
If provided, the population is filtered by portfolio tags.
|
430
|
-
|
431
359
|
Returns
|
432
360
|
-------
|
433
361
|
summary : pandas DataFrame
|
434
362
|
The population's portfolios summary
|
435
363
|
"""
|
436
364
|
|
437
|
-
portfolios = self.filter(names=names, tags=tags)
|
438
365
|
df = pd.concat(
|
439
|
-
[p.summary(formatted=formatted) for p in
|
440
|
-
keys=[p.name for p in
|
366
|
+
[p.summary(formatted=formatted) for p in self],
|
367
|
+
keys=[p.name for p in self],
|
441
368
|
axis=1,
|
442
369
|
)
|
443
370
|
return df
|
444
371
|
|
445
372
|
def composition(
|
446
373
|
self,
|
447
|
-
names: skt.Names | None = None,
|
448
|
-
tags: skt.Tags | None = None,
|
449
374
|
display_sub_ptf_name: bool = True,
|
450
375
|
) -> pd.DataFrame:
|
451
|
-
"""Composition of
|
376
|
+
"""Composition of each portfolio in the population.
|
452
377
|
|
453
378
|
Parameters
|
454
379
|
----------
|
455
|
-
names : str | list[str], optional
|
456
|
-
If provided, the population is filtered by portfolio names.
|
457
|
-
|
458
|
-
tags : str | list[str], optional
|
459
|
-
If provided, the population is filtered by portfolio tags.
|
460
|
-
|
461
380
|
display_sub_ptf_name : bool, default=True
|
462
381
|
If this is set to True, each sub-portfolio name composing a multi-period
|
463
382
|
portfolio is displayed.
|
464
383
|
|
465
384
|
Returns
|
466
385
|
-------
|
467
|
-
|
386
|
+
df : DataFrame
|
468
387
|
Composition of the portfolios in the population.
|
469
388
|
"""
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
comp = p.composition
|
389
|
+
res = []
|
390
|
+
for ptf in self:
|
391
|
+
comp = ptf.composition
|
474
392
|
if display_sub_ptf_name:
|
475
|
-
if isinstance(
|
393
|
+
if isinstance(ptf, MultiPeriodPortfolio):
|
476
394
|
comp.rename(
|
477
|
-
columns={c: f"{
|
395
|
+
columns={c: f"{ptf.name}_{c}" for c in comp.columns},
|
396
|
+
inplace=True,
|
397
|
+
)
|
398
|
+
else:
|
399
|
+
comp.rename(columns={c: ptf.name for c in comp.columns}, inplace=True)
|
400
|
+
res.append(comp)
|
401
|
+
|
402
|
+
df = pd.concat(res, axis=1)
|
403
|
+
df.columns = deduplicate_names(list(df.columns))
|
404
|
+
df.fillna(0, inplace=True)
|
405
|
+
return df
|
406
|
+
|
407
|
+
def contribution(
|
408
|
+
self,
|
409
|
+
measure: skt.Measure,
|
410
|
+
spacing: float | None = None,
|
411
|
+
display_sub_ptf_name: bool = True,
|
412
|
+
) -> pd.DataFrame:
|
413
|
+
"""Contribution of each asset to a given measure of each portfolio in the
|
414
|
+
population.
|
415
|
+
|
416
|
+
Parameters
|
417
|
+
----------
|
418
|
+
measure : Measure
|
419
|
+
The measure used for the contribution computation.
|
420
|
+
|
421
|
+
spacing : float, optional
|
422
|
+
Spacing "h" of the finite difference:
|
423
|
+
:math:`contribution(wi)= \frac{measure(wi-h) - measure(wi+h)}{2h}`
|
424
|
+
|
425
|
+
display_sub_ptf_name : bool, default=True
|
426
|
+
If this is set to True, each sub-portfolio name composing a multi-period
|
427
|
+
portfolio is displayed.
|
428
|
+
|
429
|
+
Returns
|
430
|
+
-------
|
431
|
+
df : DataFrame
|
432
|
+
Contribution of each asset to a given measure of each portfolio in the
|
433
|
+
population.
|
434
|
+
"""
|
435
|
+
res = []
|
436
|
+
for ptf in self:
|
437
|
+
contribution = ptf.contribution(
|
438
|
+
measure=measure, spacing=spacing, to_df=True
|
439
|
+
)
|
440
|
+
if display_sub_ptf_name:
|
441
|
+
if isinstance(ptf, MultiPeriodPortfolio):
|
442
|
+
contribution.rename(
|
443
|
+
columns={c: f"{ptf.name}_{c}" for c in contribution.columns},
|
444
|
+
inplace=True,
|
478
445
|
)
|
479
446
|
else:
|
480
|
-
|
481
|
-
|
447
|
+
contribution.rename(
|
448
|
+
columns={c: ptf.name for c in contribution.columns}, inplace=True
|
449
|
+
)
|
450
|
+
res.append(contribution)
|
482
451
|
|
483
|
-
df = pd.concat(
|
452
|
+
df = pd.concat(res, axis=1)
|
484
453
|
df.columns = deduplicate_names(list(df.columns))
|
485
454
|
df.fillna(0, inplace=True)
|
486
455
|
return df
|
487
456
|
|
457
|
+
def rolling_measure(
|
458
|
+
self, measure: skt.Measure = RatioMeasure.SHARPE_RATIO, window: int = 30
|
459
|
+
) -> pd.DataFrame:
|
460
|
+
"""Compute the measure over a rolling window for each portfolio in the
|
461
|
+
population.
|
462
|
+
|
463
|
+
Parameters
|
464
|
+
----------
|
465
|
+
measure : ct.Measure, default=RatioMeasure.SHARPE_RATIO
|
466
|
+
The measure. The default measure is the Sharpe Ratio.
|
467
|
+
|
468
|
+
window : int, default=30
|
469
|
+
The window size. The default value is `30` observations.
|
470
|
+
|
471
|
+
Returns
|
472
|
+
-------
|
473
|
+
dataframe : pandas DataFrame
|
474
|
+
The rolling measures.
|
475
|
+
"""
|
476
|
+
|
477
|
+
rolling_measures = []
|
478
|
+
names = []
|
479
|
+
for ptf in self:
|
480
|
+
rolling_measures.append(ptf.rolling_measure(measure=measure, window=window))
|
481
|
+
names.append(_ptf_name_with_tag(ptf))
|
482
|
+
df = pd.concat(rolling_measures, axis=1)
|
483
|
+
df.columns = deduplicate_names(names)
|
484
|
+
# Sort index because pd.concat unsort NaNs at the end
|
485
|
+
df.sort_index(inplace=True)
|
486
|
+
return df
|
487
|
+
|
488
488
|
def plot_distribution(
|
489
489
|
self,
|
490
490
|
measure_list: list[skt.Measure],
|
@@ -518,7 +518,7 @@ class Population(list):
|
|
518
518
|
for measure in measure_list:
|
519
519
|
if tag_list is not None:
|
520
520
|
for tag in tag_list:
|
521
|
-
values.append(self.measures(measure=measure
|
521
|
+
values.append(self.filter(tags=tag).measures(measure=measure))
|
522
522
|
labels.append(f"{measure} - {tag}")
|
523
523
|
else:
|
524
524
|
values.append(self.measures(measure=measure))
|
@@ -542,8 +542,6 @@ class Population(list):
|
|
542
542
|
self,
|
543
543
|
log_scale: bool = False,
|
544
544
|
idx: slice | np.ndarray | None = None,
|
545
|
-
names: skt.Names | None = None,
|
546
|
-
tags: skt.Tags | None = None,
|
547
545
|
) -> go.Figure:
|
548
546
|
"""Plot the population's portfolios cumulative returns.
|
549
547
|
Non-compounded cumulative returns start at 0.
|
@@ -560,12 +558,6 @@ class Population(list):
|
|
560
558
|
Indexes or slice of the observations to plot.
|
561
559
|
The default (`None`) is to take all observations.
|
562
560
|
|
563
|
-
names : str | list[str], optional
|
564
|
-
If provided, the population is filtered by portfolio names.
|
565
|
-
|
566
|
-
tags : str | list[str], optional
|
567
|
-
If provided, the population is filtered by portfolio tags.
|
568
|
-
|
569
561
|
Returns
|
570
562
|
-------
|
571
563
|
plot : Figure
|
@@ -573,16 +565,13 @@ class Population(list):
|
|
573
565
|
"""
|
574
566
|
if idx is None:
|
575
567
|
idx = slice(None)
|
576
|
-
portfolios = self.filter(names=names, tags=tags)
|
577
|
-
if not portfolios:
|
578
|
-
raise ValueError("No portfolio found")
|
579
568
|
|
580
569
|
cumulative_returns = []
|
581
570
|
names = []
|
582
571
|
compounded = []
|
583
|
-
for ptf in
|
572
|
+
for ptf in self:
|
584
573
|
cumulative_returns.append(ptf.cumulative_returns_df)
|
585
|
-
names.append(
|
574
|
+
names.append(_ptf_name_with_tag(ptf))
|
586
575
|
compounded.append(ptf.compounded)
|
587
576
|
compounded = set(compounded)
|
588
577
|
|
@@ -631,22 +620,11 @@ class Population(list):
|
|
631
620
|
fig.update_yaxes(type="log")
|
632
621
|
return fig
|
633
622
|
|
634
|
-
def plot_composition(
|
635
|
-
self,
|
636
|
-
names: skt.Names | None = None,
|
637
|
-
tags: skt.Tags | None = None,
|
638
|
-
display_sub_ptf_name: bool = True,
|
639
|
-
) -> go.Figure:
|
623
|
+
def plot_composition(self, display_sub_ptf_name: bool = True) -> go.Figure:
|
640
624
|
"""Plot the compositions of the portfolios in the population.
|
641
625
|
|
642
626
|
Parameters
|
643
627
|
----------
|
644
|
-
names : str | list[str], optional
|
645
|
-
If provided, the population is filtered by portfolio names.
|
646
|
-
|
647
|
-
tags : str | list[str], optional
|
648
|
-
If provided, the population is filtered by portfolio tags.
|
649
|
-
|
650
628
|
display_sub_ptf_name : bool, default=True
|
651
629
|
If this is set to True, each sub-portfolio name composing a multi-period
|
652
630
|
portfolio is displayed.
|
@@ -656,15 +634,11 @@ class Population(list):
|
|
656
634
|
plot : Figure
|
657
635
|
Returns the plotly Figure object.
|
658
636
|
"""
|
659
|
-
df = self.composition(
|
660
|
-
names=names, tags=tags, display_sub_ptf_name=display_sub_ptf_name
|
661
|
-
).T
|
637
|
+
df = self.composition(display_sub_ptf_name=display_sub_ptf_name).T
|
662
638
|
fig = px.bar(df, x=df.index, y=df.columns)
|
663
639
|
fig.update_layout(
|
664
640
|
title="Portfolios Composition",
|
665
|
-
|
666
|
-
"title": "Portfolios",
|
667
|
-
},
|
641
|
+
xaxis_title="Portfolios",
|
668
642
|
yaxis={
|
669
643
|
"title": "Weight",
|
670
644
|
"tickformat": ",.0%",
|
@@ -673,6 +647,53 @@ class Population(list):
|
|
673
647
|
)
|
674
648
|
return fig
|
675
649
|
|
650
|
+
def plot_contribution(
|
651
|
+
self,
|
652
|
+
measure: skt.Measure,
|
653
|
+
spacing: float | None = None,
|
654
|
+
display_sub_ptf_name: bool = True,
|
655
|
+
) -> go.Figure:
|
656
|
+
"""Plot the contribution of each asset to a given measure of the portfolios
|
657
|
+
in the population.
|
658
|
+
|
659
|
+
Parameters
|
660
|
+
----------
|
661
|
+
measure : Measure
|
662
|
+
The measure used for the contribution computation.
|
663
|
+
|
664
|
+
spacing : float, optional
|
665
|
+
Spacing "h" of the finite difference:
|
666
|
+
:math:`contribution(wi)= \frac{measure(wi-h) - measure(wi+h)}{2h}`
|
667
|
+
|
668
|
+
display_sub_ptf_name : bool, default=True
|
669
|
+
If this is set to True, each sub-portfolio name composing a multi-period
|
670
|
+
portfolio is displayed.
|
671
|
+
|
672
|
+
Returns
|
673
|
+
-------
|
674
|
+
plot : Figure
|
675
|
+
Returns the plotly Figure object.
|
676
|
+
"""
|
677
|
+
df = self.contribution(
|
678
|
+
display_sub_ptf_name=display_sub_ptf_name, measure=measure, spacing=spacing
|
679
|
+
).T
|
680
|
+
fig = px.bar(df, x=df.index, y=df.columns)
|
681
|
+
|
682
|
+
yaxis = {
|
683
|
+
"title": "Contribution",
|
684
|
+
}
|
685
|
+
if not measure.is_ratio:
|
686
|
+
n = optimal_rounding_decimals(df.sum(axis=1).max())
|
687
|
+
yaxis["tickformat"] = f",.{n}%"
|
688
|
+
|
689
|
+
fig.update_layout(
|
690
|
+
title=f"{measure} Contribution",
|
691
|
+
xaxis_title="Portfolios",
|
692
|
+
yaxis=yaxis,
|
693
|
+
legend=dict(yanchor="top", y=0.99, xanchor="left", x=1.15),
|
694
|
+
)
|
695
|
+
return fig
|
696
|
+
|
676
697
|
def plot_measures(
|
677
698
|
self,
|
678
699
|
x: skt.Measure,
|
@@ -682,8 +703,6 @@ class Population(list):
|
|
682
703
|
hover_measures: list[skt.Measure] | None = None,
|
683
704
|
show_fronts: bool = False,
|
684
705
|
color_scale: skt.Measure | str | None = None,
|
685
|
-
names: skt.Names | None = None,
|
686
|
-
tags: skt.Tags | None = None,
|
687
706
|
title="Portfolios",
|
688
707
|
) -> go.Figure:
|
689
708
|
"""Plot the 2D (or 3D) scatter points (or surface) of a given set of
|
@@ -716,18 +735,11 @@ class Population(list):
|
|
716
735
|
title : str, default="Portfolios"
|
717
736
|
The graph title. The default value is "Portfolios".
|
718
737
|
|
719
|
-
names : str | list[str], optional
|
720
|
-
If provided, the population is filtered by portfolio names.
|
721
|
-
|
722
|
-
tags : str | list[str], optional
|
723
|
-
If provided, the population is filtered by portfolio tags.
|
724
|
-
|
725
738
|
Returns
|
726
739
|
-------
|
727
740
|
plot : Figure
|
728
741
|
Returns the plotly Figure object.
|
729
742
|
"""
|
730
|
-
portfolios = self.filter(names=names, tags=tags)
|
731
743
|
num_fmt = ":.3f"
|
732
744
|
hover_data = {x: num_fmt, y: num_fmt, "tag": True}
|
733
745
|
|
@@ -749,7 +761,7 @@ class Population(list):
|
|
749
761
|
col_values = [e.value if isinstance(e, skt.Measure) else e for e in columns]
|
750
762
|
res = [
|
751
763
|
[portfolio.__getattribute__(attr) for attr in col_values]
|
752
|
-
for portfolio in
|
764
|
+
for portfolio in self
|
753
765
|
]
|
754
766
|
# Improved formatting
|
755
767
|
columns = [str(e) for e in columns]
|
@@ -760,8 +772,6 @@ class Population(list):
|
|
760
772
|
|
761
773
|
if show_fronts:
|
762
774
|
fronts = self.non_denominated_sort(first_front_only=False)
|
763
|
-
if tags is not None:
|
764
|
-
ValueError("Cannot plot front with tags selected")
|
765
775
|
df["front"] = str(-1)
|
766
776
|
for i, front in enumerate(fronts):
|
767
777
|
for idx in front:
|
@@ -884,3 +894,56 @@ class Population(list):
|
|
884
894
|
legend=dict(yanchor="top", y=0.96, xanchor="left", x=1.25),
|
885
895
|
)
|
886
896
|
return fig
|
897
|
+
|
898
|
+
def plot_rolling_measure(
|
899
|
+
self,
|
900
|
+
measure: skt.Measure = RatioMeasure.SHARPE_RATIO,
|
901
|
+
window: int = 30,
|
902
|
+
) -> go.Figure:
|
903
|
+
"""Plot the measure over a rolling window for each portfolio in the population.
|
904
|
+
|
905
|
+
Parameters
|
906
|
+
----------
|
907
|
+
measure : ct.Measure, default = RatioMeasure.SHARPE_RATIO
|
908
|
+
The measure.
|
909
|
+
|
910
|
+
window : int, default=30
|
911
|
+
The window size.
|
912
|
+
|
913
|
+
Returns
|
914
|
+
-------
|
915
|
+
plot : Figure
|
916
|
+
Returns the plot Figure object
|
917
|
+
"""
|
918
|
+
df = self.rolling_measure(measure=measure, window=window)
|
919
|
+
fig = df.plot(backend="plotly")
|
920
|
+
max_val = np.max(df)
|
921
|
+
min_val = np.min(df)
|
922
|
+
if max_val > 0 > min_val:
|
923
|
+
fig.add_hrect(
|
924
|
+
y0=0, y1=max_val * 1.3, line_width=0, fillcolor="green", opacity=0.1
|
925
|
+
)
|
926
|
+
fig.add_hrect(
|
927
|
+
y0=min_val * 1.3, y1=0, line_width=0, fillcolor="red", opacity=0.1
|
928
|
+
)
|
929
|
+
|
930
|
+
yaxis = {
|
931
|
+
"title": str(measure),
|
932
|
+
}
|
933
|
+
if not measure.is_ratio:
|
934
|
+
n = optimal_rounding_decimals(max_val)
|
935
|
+
yaxis["tickformat"] = f",.{n}%"
|
936
|
+
|
937
|
+
fig.update_layout(
|
938
|
+
title=f"Rolling {measure} - {window} observations window",
|
939
|
+
xaxis_title="Observations",
|
940
|
+
yaxis=yaxis,
|
941
|
+
showlegend=False,
|
942
|
+
)
|
943
|
+
return fig
|
944
|
+
|
945
|
+
|
946
|
+
def _ptf_name_with_tag(portfolio: BasePortfolio) -> str:
|
947
|
+
if portfolio.tag is None:
|
948
|
+
return portfolio.name
|
949
|
+
return f"{portfolio.name}_{portfolio.tag}"
|