Skip to content

Commit 1fbe000

Browse files
committed
fix: VAR & CVAR in Portfilio
get_var_historic and get_cvar_historic in Portfolio class use get_rolling_cumulative_return method. Several docstrings are inserted.
1 parent 5221bd6 commit 1fbe000

12 files changed

+784
-846
lines changed

main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import okama as ok
22

3-
x = ok.AssetList(['ILSRUB.CBR'])
3+
x = ok.AssetList(['T.US'])
4+
print(x.dividend_yield)
45

56

67

notebooks/01 howto.ipynb

Lines changed: 211 additions & 175 deletions
Large diffs are not rendered by default.

notebooks/02 index funds perfomance.ipynb

Lines changed: 31 additions & 43 deletions
Large diffs are not rendered by default.

notebooks/03 efficient frontier single period.ipynb

Lines changed: 115 additions & 131 deletions
Large diffs are not rendered by default.

notebooks/04 efficient frontier multi-period.ipynb

Lines changed: 127 additions & 135 deletions
Large diffs are not rendered by default.

notebooks/05 backtesting distribution.ipynb

Lines changed: 91 additions & 115 deletions
Large diffs are not rendered by default.

notebooks/06 forecasting.ipynb

Lines changed: 104 additions & 118 deletions
Large diffs are not rendered by default.

okama/assets.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from .macro import Inflation
77
from .common.helpers import Float, Frame, Date, Index
8-
# from .common._decorators import doc
98
from .settings import default_ticker, PeriodLength, _MONTHS_PER_YEAR
109
from .api.data_queries import QueryData
1110
from .api.namespaces import get_assets_namespaces
@@ -614,8 +613,7 @@ def describe(self, years: Tuple[int, ...] = (1, 5, 10), tickers: bool = True) ->
614613
@property
615614
def mean_return(self) -> pd.Series:
616615
"""
617-
TODO: Finish description
618-
Calculate mean return (arithmetic mean) for the assets.
616+
Calculate annualized mean return (arithmetic mean) for the assets.
619617
"""
620618
df = self._add_inflation()
621619
mean = df.mean()
@@ -624,7 +622,9 @@ def mean_return(self) -> pd.Series:
624622
@property
625623
def real_mean_return(self) -> pd.Series:
626624
"""
627-
Calculates real mean return (arithmetic mean) for the assets.
625+
Calculates annualized real mean return (arithmetic mean) for the assets.
626+
627+
Real rate of return is adjusted for inflation.
628628
"""
629629
if hasattr(self, 'inflation'):
630630
df = pd.concat([self.ror, self.inflation_ts], axis=1, join='inner', copy='false')
@@ -635,6 +635,9 @@ def real_mean_return(self) -> pd.Series:
635635
return (1. + ror_mean) / (1. + infl_mean) - 1.
636636

637637
def _get_asset_dividends(self, tick, remove_forecast=True) -> pd.Series:
638+
"""
639+
Get dividend time series for a single symbol.
640+
"""
638641
first_period = pd.Period(self.first_date, freq='M')
639642
first_day = first_period.to_timestamp(how='Start')
640643
last_period = pd.Period(self.last_date, freq='M')
@@ -649,6 +652,11 @@ def _get_asset_dividends(self, tick, remove_forecast=True) -> pd.Series:
649652
return s.add(pad_s, fill_value=0)
650653

651654
def _get_dividends(self, remove_forecast=True) -> pd.DataFrame:
655+
"""
656+
Get dividend time series for all assets.
657+
658+
If remove_forecast=True all forecasted (future) data is removed from time series.
659+
"""
652660
if self._dividends_ts.empty:
653661
dic = {}
654662
for tick in self.symbols:
@@ -660,9 +668,11 @@ def _get_dividends(self, remove_forecast=True) -> pd.DataFrame:
660668
@property
661669
def dividend_yield(self) -> pd.DataFrame:
662670
"""
663-
Calculate last twelve months (LTM) dividend yield time series monthly.
664-
Calculates yield assuming original asset currency (not adjusting to AssetList currency).
671+
Calculate last twelve months (LTM) dividend yield time series (monthly) for each asset.
672+
673+
All yields are calculated in the original asset currency (not adjusting to AssetList currency).
665674
Forecast dividends are removed.
675+
Zero value time series are created for assets without dividends.
666676
"""
667677
if self._dividend_yield.empty:
668678
frame = {}
@@ -701,14 +711,16 @@ def dividend_yield(self) -> pd.DataFrame:
701711
@property
702712
def dividends_annual(self) -> pd.DataFrame:
703713
"""
704-
Time series of dividends for a calendar year.
714+
Return calendar year dividends time series for each asset.
705715
"""
706716
return self._get_dividends().resample('Y').sum()
707717

708718
@property
709719
def dividend_growing_years(self) -> pd.DataFrame:
710720
"""
711-
Returns the number of growing dividend years for each asset.
721+
Return the number of growing dividend years for each asset.
722+
723+
TODO: finish description. Insert an example
712724
"""
713725
div_growth = self.dividends_annual.pct_change()[1:]
714726
df = pd.DataFrame()
@@ -723,7 +735,7 @@ def dividend_growing_years(self) -> pd.DataFrame:
723735
@property
724736
def dividend_paying_years(self) -> pd.DataFrame:
725737
"""
726-
Returns the number of years of consecutive dividend payments.
738+
Return the number of years of consecutive dividend payments for each asset.
727739
"""
728740
div_annual = self.dividends_annual
729741
frame = pd.DataFrame()
@@ -738,7 +750,7 @@ def dividend_paying_years(self) -> pd.DataFrame:
738750

739751
def get_dividend_mean_growth_rate(self, period=5) -> pd.Series:
740752
"""
741-
Calculates geometric mean of dividends growth rate time series for a certain period.
753+
Calculate geometric mean of dividends growth rate time series for a given period.
742754
Period should be integer and not exceed the available data period_length.
743755
"""
744756
if period > self.pl.years or not isinstance(period, int):

okama/common/_decorators.py

Lines changed: 0 additions & 112 deletions
This file was deleted.

okama/portfolio.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Portfolio:
1515
Implementation of investment portfolio.
1616
Arguments are similar to AssetList (weights are added), but different behavior.
1717
Works with monthly end of day historical rate of return data.
18+
TODO: rebalance_period should be an attribute
1819
"""
1920
def __init__(self,
2021
symbols: Optional[List[str]] = None, *,
@@ -266,15 +267,64 @@ def semideviation_annual(self) -> float:
266267
return Frame.get_semideviation(self.returns_ts) * 12 ** 0.5
267268

268269
def get_var_historic(self, time_frame: int = 12, level=5) -> float:
270+
"""
271+
Calculate historic Value at Risk (VaR) for the portfolio.
272+
273+
The VaR calculates the potential loss of an investment with a given time frame and confidence level.
274+
Loss is a positive number (expressed in cumulative return).
275+
If VaR is negative there are gains at this confidence level.
276+
277+
Parameters
278+
----------
279+
time_frame : int, default 12 (12 months)
280+
level : int, default 5 (5% quantile)
281+
282+
Returns
283+
-------
284+
Float
285+
286+
Examples
287+
--------
288+
>>> x = ok.Portfolio(['SP500TR.INDX', 'SP500BDT.INDX'], last_date='2021-01')
289+
>>> x.get_var_historic(time_frame=12, level=1)
290+
0.24030006476701732
291+
"""
269292
rolling = self.get_rolling_cumulative_return(window=time_frame)
270293
return Frame.get_var_historic(rolling, level)
271294

272295
def get_cvar_historic(self, time_frame: int = 12, level=5) -> float:
296+
"""
297+
Calculate historic Conditional Value at Risk (CVAR, expected shortfall) for the portfolio.
298+
299+
CVaR is the average loss over a specified time period of unlikely scenarios beyond the confidence level.
300+
Loss is a positive number (expressed in cumulative return).
301+
If CVaR is negative there are gains at this confidence level.
302+
303+
Parameters
304+
----------
305+
time_frame : int, default 12 (12 months)
306+
level : int, default 5 (5% quantile)
307+
308+
Returns
309+
-------
310+
Float
311+
312+
Examples
313+
--------
314+
>>> x = ok.Portfolio(['USDEUR.FX', 'BTC-USD.CC'], last_date='2021-01')
315+
>>> x.get_cvar_historic(time_frame=2, level=1)
316+
0.3566909250442616
317+
"""
273318
rolling = self.get_rolling_cumulative_return(window=time_frame)
274319
return Frame.get_cvar_historic(rolling, level)
275320

276321
@property
277322
def drawdowns(self) -> pd.Series:
323+
"""
324+
Calculate drawdowns time series for the portfolio.
325+
326+
The drawdown is the percent decline from a previous peak in wealth index.
327+
"""
278328
return Frame.get_drawdowns(self.returns_ts)
279329

280330
def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
@@ -431,10 +481,18 @@ def _test_forecast_period(self, years):
431481
raise ValueError(f'Forecast period {years} years is not credible. '
432482
f'It should not exceed 1/2 of portfolio history period length {self.period_length / 2} years')
433483

434-
def percentile_inverse(self, distr: str = 'norm', years: int = 1, score: float = 0, n: Optional[int] = None) -> float:
484+
def percentile_inverse(self,
485+
distr: str = 'norm',
486+
years: int = 1,
487+
score: float = 0,
488+
n: Optional[int] = None
489+
) -> float:
435490
"""
436-
Compute the percentile rank of a score relative to an array of CAGR values.
437-
A percentile_inverse of, for example, 80% means that 80% of the scores in distr are below the given score.
491+
Compute the percentile rank of a score (CAGR value) in a given time frame.
492+
493+
If percentile_inverse of, for example, 0% (CAGR value) is equal to 8% for 1 year time frame
494+
it means that 8% of the CAGR values in the distribution are negative in 1 year periods. Or in other words
495+
the probability of getting negative result after 1 year of investments is 8%.
438496
439497
Args:
440498
distr: norm, lognorm, hist - distribution type (normal or lognormal) or hist for CAGR array from history

0 commit comments

Comments
 (0)