@@ -592,30 +592,84 @@ def tracking_error(self):
592
592
@property
593
593
def index_corr (self ):
594
594
"""
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.
596
596
Index should be in the first position (first column).
597
597
The period should be at least 12 months.
598
598
"""
599
599
return Index .cov_cor (self .ror , fn = 'corr' )
600
600
601
- def index_rolling_corr (self , window = 60 ):
601
+ def index_rolling_corr (self , window : int = 60 ):
602
602
"""
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.
604
604
Index should be in the first position (first column).
605
605
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).
607
607
"""
608
608
return Index .rolling_cov_cor (self .ror , window = window , fn = 'corr' )
609
609
610
610
@property
611
611
def index_beta (self ):
612
612
"""
613
- Returns beta coefficient time series for the assets.
613
+ Compute beta coefficient time series for the assets.
614
614
Index (or benchmark) should be in the first position (first column).
615
615
Rolling window size should be at least 12 months.
616
616
"""
617
617
return Index .beta (self .ror )
618
618
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
+
619
673
620
674
class Portfolio :
621
675
"""
@@ -683,6 +737,11 @@ def weights(self, weights: list):
683
737
684
738
@property
685
739
def returns_ts (self ) -> pd .Series :
740
+ """
741
+ Rate of return time series for portfolio.
742
+ Returns:
743
+ pd.Series
744
+ """
686
745
s = Frame .get_portfolio_return_ts (self .weights , self ._ror )
687
746
s .rename ('portfolio' , inplace = True )
688
747
return s
@@ -947,11 +1006,7 @@ def forecast_from_history(self, percentiles: List[int] = [10, 50, 90]) -> pd.Dat
947
1006
df .index .rename ('years' , inplace = True )
948
1007
return df
949
1008
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 ):
955
1010
max_period_years = round (self .period_length / 2 )
956
1011
if max_period_years < 1 :
957
1012
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.
964
1019
start_period = self .last_date .to_period ('M' )
965
1020
end_period = self .last_date .to_period ('M' ) + period_months - 1
966
1021
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 )
967
1030
# 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".' )
969
1040
return_ts = pd .DataFrame (data = random_returns , index = ts_index )
970
1041
return return_ts
971
1042
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 :
973
1044
"""
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.
975
1047
"""
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 )
977
1051
first_value = self .wealth_index ['portfolio' ].values [- 1 ]
978
1052
forecast_wealth = Frame .get_wealth_indexes (return_ts , first_value )
979
1053
return forecast_wealth
980
1054
981
1055
def forecast_monte_carlo_percentile_wealth_indexes (self ,
1056
+ distr : str = 'norm' ,
982
1057
years : int = 5 ,
983
1058
percentiles : List [int ] = [10 , 50 , 90 ],
984
1059
today_value : Optional [int ] = None ,
985
1060
n : int = 1000 ,
986
1061
) -> Dict [int , float ]:
987
1062
"""
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).
991
1067
"""
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 )
993
1071
results = dict ()
994
1072
for percentile in percentiles :
995
1073
value = wealth_indexes .iloc [- 1 , :].quantile (percentile / 100 )
@@ -998,3 +1076,56 @@ def forecast_monte_carlo_percentile_wealth_indexes(self,
998
1076
modifier = today_value / self .wealth_index ['portfolio' ].values [- 1 ]
999
1077
results .update ((x , y * modifier )for x , y in results .items ())
1000
1078
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