@@ -1016,168 +1016,3 @@ def diff_custom_calendar_timedeltas(start, end, freq):
10161016 timediff = end - start
10171017 delta_days = timediff .components .days - actual_days
10181018 return timediff - pd .Timedelta (days = delta_days )
1019-
1020- def subportfolio_cumulative_returns (returns , period , freq = None ):
1021- """
1022- Builds cumulative returns from 'period' returns. This function simulates
1023- the cumulative effect that a series of gains or losses (the 'returns')
1024- have on an original amount of capital over a period of time.
1025-
1026- if F is the frequency at which returns are computed (e.g. 1 day if
1027- 'returns' contains daily values) and N is the period for which the retuns
1028- are computed (e.g. returns after 1 day, 5 hours or 3 days) then:
1029- - if N <= F the cumulative retuns are trivially computed as Compound Return
1030- - if N > F (e.g. F 1 day, and N is 3 days) then the returns overlap and the
1031- cumulative returns are computed building and averaging N interleaved sub
1032- portfolios (started at subsequent periods 1,2,..,N) each one rebalancing
1033- every N periods. This correspond to an algorithm which trades the factor
1034- every single time it is computed, which is statistically more robust and
1035- with a lower volatity compared to an algorithm that trades the factor
1036- every N periods and whose returns depend on the specific starting day of
1037- trading.
1038-
1039- Also note that when the factor is not computed at a specific frequency, for
1040- exaple a factor representing a random event, it is not efficient to create
1041- multiples sub-portfolios as it is not certain when the factor will be
1042- traded and this would result in an underleveraged portfolio. In this case
1043- the simulated portfolio is fully invested whenever an event happens and if
1044- a subsequent event occur while the portfolio is still invested in a
1045- previous event then the portfolio is rebalanced and split equally among the
1046- active events.
1047-
1048- Parameters
1049- ----------
1050- returns: pd.Series
1051- pd.Series containing factor 'period' forward returns, the index
1052- contains timestamps at which the trades are computed and the values
1053- correspond to returns after 'period' time
1054- period: pandas.Timedelta or string
1055- Length of period for which the returns are computed (1 day, 2 mins,
1056- 3 hours etc). It can be a Timedelta or a string in the format accepted
1057- by Timedelta constructor ('1 days', '1D', '30m', '3h', '1D1h', etc)
1058- freq : pandas DateOffset, optional
1059- Used to specify a particular trading calendar. If not present
1060- returns.index.freq will be used
1061-
1062- Returns
1063- -------
1064- Cumulative returns series : pd.Series
1065- Example:
1066- 2015-07-16 09:30:00 -0.012143
1067- 2015-07-16 12:30:00 0.012546
1068- 2015-07-17 09:30:00 0.045350
1069- 2015-07-17 12:30:00 0.065897
1070- 2015-07-20 09:30:00 0.030957
1071- """
1072-
1073- if not isinstance (period , pd .Timedelta ):
1074- period = pd .Timedelta (period )
1075-
1076- if freq is None :
1077- freq = returns .index .freq
1078-
1079- if freq is None :
1080- freq = BDay ()
1081- warnings .warn ("'freq' not set, using business day calendar" ,
1082- UserWarning )
1083-
1084- #
1085- # returns index contains factor computation timestamps, then add returns
1086- # timestamps too (factor timestamps + period) and save them to 'full_idx'
1087- # Cumulative returns will use 'full_idx' index,because we want a cumulative
1088- # returns value for each entry in 'full_idx'
1089- #
1090- trades_idx = returns .index .copy ()
1091- returns_idx = utils .add_custom_calendar_timedelta (trades_idx , period , freq )
1092- full_idx = trades_idx .union (returns_idx )
1093-
1094- #
1095- # Build N sub_returns from the single returns Series. Each sub_retuns
1096- # stream will contain non-overlapping returns.
1097- # In the next step we'll compute the portfolio returns averaging the
1098- # returns happening on those overlapping returns streams
1099- #
1100- sub_returns = []
1101- print (returns .shape )
1102- while len (trades_idx ) > 0 :
1103-
1104- #
1105- # select non-overlapping returns starting with first timestamp in index
1106- #
1107- sub_index = []
1108- next = trades_idx .min ()
1109- while next <= trades_idx .max ():
1110- sub_index .append (next )
1111- next = utils .add_custom_calendar_timedelta (next , period , freq )
1112- # make sure to fetch the next available entry after 'period'
1113- try :
1114- i = trades_idx .get_loc (next , method = 'bfill' )
1115- next = trades_idx [i ]
1116- except KeyError :
1117- break
1118-
1119- sub_index = pd .DatetimeIndex (sub_index , tz = full_idx .tz )
1120- subret = returns [sub_index ]
1121-
1122- # make the index to have all entries in 'full_idx'
1123- subret = subret .reindex (full_idx )
1124-
1125- #
1126- # compute intermediate returns values for each index in subret that are
1127- # in between the timestaps at which the factors are computed and the
1128- # timestamps at which the 'period' returns actually happen
1129- #
1130- for pret_idx in reversed (sub_index ):
1131-
1132- pret = subret [pret_idx ]
1133-
1134- # get all timestamps between factor computation and period returns
1135- pret_end_idx = \
1136- utils .add_custom_calendar_timedelta (pret_idx , period , freq )
1137- slice = subret [(subret .index > pret_idx ) & (
1138- subret .index <= pret_end_idx )].index
1139-
1140- if pd .isnull (pret ):
1141- continue
1142-
1143- def rate_of_returns (ret , period ):
1144- return ((np .nansum (ret ) + 1 )** (1. / period )) - 1
1145-
1146- # compute intermediate 'period' returns values, note that this also
1147- # moves the final 'period' returns value from trading timestamp to
1148- # trading timestamp + 'period'
1149- for slice_idx in slice :
1150- sub_period = utils .diff_custom_calendar_timedeltas (
1151- pret_idx , slice_idx , freq )
1152- subret [slice_idx ] = rate_of_returns (pret , period / sub_period )
1153-
1154- subret [pret_idx ] = np .nan
1155-
1156- # transform returns as percentage change from previous value
1157- subret [slice [1 :]] = (subret [slice ] + 1 ).pct_change ()[slice [1 :]]
1158-
1159- sub_returns .append (subret )
1160- trades_idx = trades_idx .difference (sub_index )
1161-
1162- #
1163- # Compute portfolio cumulative returns averaging the returns happening on
1164- # overlapping returns streams.
1165- #
1166- sub_portfolios = pd .concat (sub_returns , axis = 1 )
1167- portfolio = pd .Series (index = sub_portfolios .index )
1168-
1169- for i , (index , row ) in enumerate (sub_portfolios .iterrows ()):
1170-
1171- # check the active portfolios, count() returns non-nans elements
1172- active_subfolios = row .count ()
1173-
1174- # fill forward portfolio value
1175- portfolio .iloc [i ] = portfolio .iloc [i - 1 ] if i > 0 else 1.
1176-
1177- if active_subfolios <= 0 :
1178- continue
1179-
1180- # current portfolio is the average of active sub_portfolios
1181- portfolio .iloc [i ] *= (row + 1 ).mean (skipna = True )
1182-
1183- return portfolio
0 commit comments