@@ -592,30 +592,84 @@ def tracking_error(self):
592592 @property
593593 def index_corr (self ):
594594 """
595- Returns the accumulated correlation with the index (or benchmark) time series for the assets.
595+ Compute expanding correlation with the index (or benchmark) time series for the assets.
596596 Index should be in the first position (first column).
597597 The period should be at least 12 months.
598598 """
599599 return Index .cov_cor (self .ror , fn = 'corr' )
600600
601- def index_rolling_corr (self , window = 60 ):
601+ def index_rolling_corr (self , window : int = 60 ):
602602 """
603- Returns the rolling correlation with the index (or benchmark) time series for the assets.
603+ Compute rolling correlation with the index (or benchmark) time series for the assets.
604604 Index should be in the first position (first column).
605605 The period should be at least 12 months.
606- window - the rolling window size (default is 5 years).
606+ window - the rolling window size in months (default is 5 years).
607607 """
608608 return Index .rolling_cov_cor (self .ror , window = window , fn = 'corr' )
609609
610610 @property
611611 def index_beta (self ):
612612 """
613- Returns beta coefficient time series for the assets.
613+ Compute beta coefficient time series for the assets.
614614 Index (or benchmark) should be in the first position (first column).
615615 Rolling window size should be at least 12 months.
616616 """
617617 return Index .beta (self .ror )
618618
619+ # distributions
620+ @property
621+ def skewness (self ):
622+ """
623+ Compute expanding skewness of the return time series for each asset.
624+ For normally distributed data, the skewness should be about zero.
625+ A skewness value greater than zero means that there is more weight in the right tail of the distribution.
626+ """
627+ return Frame .skewness (self .ror )
628+
629+ def skewness_rolling (self , window : int = 60 ):
630+ """
631+ Compute rolling skewness of the return time series for each asset.
632+ For normally distributed data, the skewness should be about zero.
633+ A skewness value greater than zero means that there is more weight in the right tail of the distribution.
634+
635+ window - the rolling window size in months (default is 5 years).
636+ The window size should be at least 12 months.
637+ """
638+ return Frame .skewness_rolling (self .ror , window = window )
639+
640+ @property
641+ def kurtosis (self ):
642+ """
643+ Calculate expanding Fisher (normalized) kurtosis time series for each asset.
644+ Kurtosis is the fourth central moment divided by the square of the variance.
645+ Kurtosis should be close to zero for normal distribution.
646+ """
647+ return Frame .kurtosis (self .ror )
648+
649+ def kurtosis_rolling (self , window : int = 60 ):
650+ """
651+ Calculate rolling Fisher (normalized) kurtosis time series for each asset.
652+ Kurtosis is the fourth central moment divided by the square of the variance.
653+ Kurtosis should be close to zero for normal distribution.
654+
655+ window - the rolling window size in months (default is 5 years).
656+ The window size should be at least 12 months.
657+ """
658+ return Frame .kurtosis_rolling (self .ror , window = window )
659+
660+ @property
661+ def jarque_bera (self ):
662+ """
663+ Jarque-Bera is a test for normality.
664+ It shows whether the returns have the skewness and kurtosis matching a normal distribution.
665+
666+ Returns:
667+ (The test statistic, The p-value for the hypothesis test)
668+ Low statistic numbers correspond to normal distribution.
669+ TODO: implement for daily values
670+ """
671+ return Frame .jarque_bera (self .ror )
672+
619673
620674class Portfolio :
621675 """
@@ -683,6 +737,11 @@ def weights(self, weights: list):
683737
684738 @property
685739 def returns_ts (self ) -> pd .Series :
740+ """
741+ Rate of return time series for portfolio.
742+ Returns:
743+ pd.Series
744+ """
686745 s = Frame .get_portfolio_return_ts (self .weights , self ._ror )
687746 s .rename ('portfolio' , inplace = True )
688747 return s
@@ -947,11 +1006,7 @@ def forecast_from_history(self, percentiles: List[int] = [10, 50, 90]) -> pd.Dat
9471006 df .index .rename ('years' , inplace = True )
9481007 return df
9491008
950- def forecast_monte_carlo_norm_returns (self , years : int = 5 , n : int = 100 ) -> pd .DataFrame :
951- """
952- Generates N random returns time series with normal distribution.
953- Forecast period should not exceed 1/2 of portfolio history period length.
954- """
1009+ def _forecast_preparation (self , years ):
9551010 max_period_years = round (self .period_length / 2 )
9561011 if max_period_years < 1 :
9571012 raise ValueError (f'Time series does not have enough history to forecast.'
@@ -964,32 +1019,55 @@ def forecast_monte_carlo_norm_returns(self, years: int = 5, n: int = 100) -> pd.
9641019 start_period = self .last_date .to_period ('M' )
9651020 end_period = self .last_date .to_period ('M' ) + period_months - 1
9661021 ts_index = pd .period_range (start_period , end_period , freq = 'M' )
1022+ return period_months , ts_index
1023+
1024+ def forecast_monte_carlo_returns (self , distr : str = 'norm' , years : int = 5 , n : int = 100 ) -> pd .DataFrame :
1025+ """
1026+ Generates N random returns time series with normal or lognormal distributions.
1027+ Forecast period should not exceed 1/2 of portfolio history period length.
1028+ """
1029+ period_months , ts_index = self ._forecast_preparation (years )
9671030 # random returns
968- random_returns = np .random .normal (self .mean_return_monthly , self .risk_monthly , (period_months , n ))
1031+ if distr == 'norm' :
1032+ random_returns = np .random .normal (self .mean_return_monthly , self .risk_monthly , (period_months , n ))
1033+ elif distr == 'lognorm' :
1034+ ln_ret = np .log (self .returns_ts + 1. )
1035+ mu = ln_ret .mean () # arithmetic mean of logarithmic returns
1036+ std = ln_ret .std () # standard deviation of logarithmic returns
1037+ random_returns = np .random .lognormal (mu , std , size = (period_months , n )) - 1.
1038+ else :
1039+ raise ValueError ('distr should be "norm" (default) or "lognormal".' )
9691040 return_ts = pd .DataFrame (data = random_returns , index = ts_index )
9701041 return return_ts
9711042
972- def forecast_monte_carlo_norm_wealth_indexes (self , years : int = 5 , n : int = 100 ) -> pd .DataFrame :
1043+ def forecast_monte_carlo_wealth_indexes (self , distr : str = 'norm' , years : int = 5 , n : int = 100 ) -> pd .DataFrame :
9731044 """
974- Generates N future wealth indexes with normally distributed monthly returns for a given period.
1045+ Generates N future random wealth indexes with monthly returns for a given period.
1046+ Random distribution could be normal or lognormal.
9751047 """
976- return_ts = self .forecast_monte_carlo_norm_returns (years = years , n = n )
1048+ if distr not in ['norm' , 'lognorm' ]:
1049+ raise ValueError ('distr should be "norm" (default) or "lognormal".' )
1050+ return_ts = self .forecast_monte_carlo_returns (distr = distr , years = years , n = n )
9771051 first_value = self .wealth_index ['portfolio' ].values [- 1 ]
9781052 forecast_wealth = Frame .get_wealth_indexes (return_ts , first_value )
9791053 return forecast_wealth
9801054
9811055 def forecast_monte_carlo_percentile_wealth_indexes (self ,
1056+ distr : str = 'norm' ,
9821057 years : int = 5 ,
9831058 percentiles : List [int ] = [10 , 50 , 90 ],
9841059 today_value : Optional [int ] = None ,
9851060 n : int = 1000 ,
9861061 ) -> Dict [int , float ]:
9871062 """
988- Calculates the final values of N forecasted wealth indexes with normal distribution assumption.
989- Final values are taken for given percentiles.
990- today_value - the value of portfolio today (before forecast period)
1063+ Calculates the final values of N forecasted random wealth indexes.
1064+ Random distribution could be normal or lognormal.
1065+ Final values are taken for a given percentiles list.
1066+ today_value - the value of portfolio today (before forecast period).
9911067 """
992- wealth_indexes = self .forecast_monte_carlo_norm_wealth_indexes (years = years , n = n )
1068+ if distr not in ['norm' , 'lognorm' ]:
1069+ raise ValueError ('distr should be "norm" (default) or "lognormal".' )
1070+ wealth_indexes = self .forecast_monte_carlo_wealth_indexes (distr = distr , years = years , n = n )
9931071 results = dict ()
9941072 for percentile in percentiles :
9951073 value = wealth_indexes .iloc [- 1 , :].quantile (percentile / 100 )
@@ -998,3 +1076,56 @@ def forecast_monte_carlo_percentile_wealth_indexes(self,
9981076 modifier = today_value / self .wealth_index ['portfolio' ].values [- 1 ]
9991077 results .update ((x , y * modifier )for x , y in results .items ())
10001078 return results
1079+
1080+ # distributions
1081+ @property
1082+ def skewness (self ):
1083+ """
1084+ Compute expanding skewness of the return time series.
1085+ For normally distributed data, the skewness should be about zero.
1086+ A skewness value greater than zero means that there is more weight in the right tail of the distribution.
1087+ """
1088+ return Frame .skewness (self .returns_ts )
1089+
1090+ def skewness_rolling (self , window : int = 60 ):
1091+ """
1092+ Compute rolling skewness of the return time series.
1093+ For normally distributed data, the skewness should be about zero.
1094+ A skewness value greater than zero means that there is more weight in the right tail of the distribution.
1095+
1096+ window - the rolling window size in months (default is 5 years).
1097+ The window size should be at least 12 months.
1098+ """
1099+ return Frame .skewness_rolling (self .returns_ts , window = window )
1100+
1101+ @property
1102+ def kurtosis (self ):
1103+ """
1104+ Calculate expanding Fisher (normalized) kurtosis time series for portfolio returns.
1105+ Kurtosis is the fourth central moment divided by the square of the variance.
1106+ Kurtosis should be close to zero for normal distribution.
1107+ """
1108+ return Frame .kurtosis (self .returns_ts )
1109+
1110+ def kurtosis_rolling (self , window : int = 60 ):
1111+ """
1112+ Calculate rolling Fisher (normalized) kurtosis time series for portfolio returns.
1113+ Kurtosis is the fourth central moment divided by the square of the variance.
1114+ Kurtosis should be close to zero for normal distribution.
1115+
1116+ window - the rolling window size in months (default is 5 years).
1117+ The window size should be at least 12 months.
1118+ """
1119+ return Frame .kurtosis_rolling (self .returns_ts , window = window )
1120+
1121+ @property
1122+ def jarque_bera (self ):
1123+ """
1124+ Jarque-Bera is a test for normality.
1125+ It shows whether the returns have the skewness and kurtosis matching a normal distribution.
1126+
1127+ Returns:
1128+ (The test statistic, The p-value for the hypothesis test)
1129+ Low statistic numbers correspond to normal distribution.
1130+ """
1131+ return Frame .jarque_bera (self .returns_ts )
0 commit comments