Skip to content

Commit c000efd

Browse files
committed
feat: the discount rate is an Effective Annual Rate (EAR)
discount_monthly_cash_flow() function is added and used in Portfolio.dcf
1 parent 649abee commit c000efd

File tree

4 files changed

+418
-456
lines changed

4 files changed

+418
-456
lines changed

main.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@
3737
# cf_strategy.frequency = "year" # withdrawals frequency
3838
# cf_strategy.percentage = -0.40
3939

40-
# # Indexation CF strategy
41-
# cf_strategy = ok.IndexationStrategy(pf)
42-
#
43-
# cf_strategy.initial_investment = 83_000_000
44-
# cf_strategy.frequency = "year"
45-
# cf_strategy.amount = 1_500_000 * 12
46-
# cf_strategy.indexation = 0.09
40+
# Indexation CF strategy
41+
cf_strategy = ok.IndexationStrategy(pf)
42+
43+
cf_strategy.initial_investment = 10_000_000
44+
cf_strategy.frequency = "year"
45+
cf_strategy.amount = 10_000_000 * 0.05
46+
cf_strategy.indexation = 0.09
4747

4848
# d = {
4949
# "2015-06": -35_000_000,
@@ -52,16 +52,16 @@
5252
# cf_strategy.time_series_dic = d
5353
# cf_strategy.time_series_discounted_values = False
5454

55-
# Fixed Percentage strategy
56-
cf_strategy = ok.VanguardDynamicSpending(pf)
57-
cf_strategy.initial_investment = 10_000_000
58-
cf_strategy.frequency = "year"
59-
cf_strategy.percentage = -0.15
60-
cf_strategy.indexation = 0.09
61-
cf_strategy.maximum_annual_withdrawal = 10_000_000 / 5 # 20%
62-
cf_strategy.minimum_annual_withdrawal = 10_000_000 / 10 # 10%
63-
cf_strategy.ceiling = 0.10
64-
cf_strategy.floor = -0.10
55+
# # VDS strategy
56+
# cf_strategy = ok.VanguardDynamicSpending(pf)
57+
# cf_strategy.initial_investment = 10_000_000
58+
# cf_strategy.frequency = "year"
59+
# cf_strategy.percentage = -0.15
60+
# cf_strategy.indexation = 0.09
61+
# cf_strategy.maximum_annual_withdrawal = 10_000_000 / 5 # 20%
62+
# cf_strategy.minimum_annual_withdrawal = 10_000_000 / 10 # 10%
63+
# cf_strategy.ceiling = 0.10
64+
# cf_strategy.floor = -0.10
6565

6666
pf.dcf.cashflow_parameters = cf_strategy # assign the cash flow strategy to portfolio
6767

@@ -78,27 +78,28 @@
7878
number=100
7979
)
8080

81-
# wi = pf.dcf.wealth_index(discounting="fv", include_negative_values=False)
82-
# cf = pf.dcf.cash_flow_ts(discounting="pv", remove_if_wealth_index_negative=True).resample("Y").sum()
83-
wi = pf.dcf.monte_carlo_wealth(discounting="fv", include_negative_values=False)
84-
# cf = pf.dcf.monte_carlo_cash_flow(discounting="fv", remove_if_wealth_index_negative=True)
81+
wi = pf.dcf.wealth_index(discounting="pv", include_negative_values=False)
82+
cf = pf.dcf.cash_flow_ts(discounting="pv", remove_if_wealth_index_negative=True)
83+
# wi = pf.dcf.monte_carlo_wealth(discounting="fv", include_negative_values=False)
84+
# cf = pf.dcf.monte_carlo_cash_flow(discounting="pv", remove_if_wealth_index_negative=True)
8585
# print(cf)
8686

8787
wi.plot(
8888
# kind="bar",
8989
legend=False
9090
)
91-
plt.yscale('log') # linear or log
91+
plt.yscale('linear') # linear or log
9292
plt.show()
9393

94-
# cf.plot(
95-
# kind="bar",
96-
# legend=False
97-
# )
98-
# plt.yscale('linear') # linear or log
99-
# plt.show()
100-
94+
df = cf
95+
df[df != 0].plot(
96+
kind="bar",
97+
legend=False
98+
)
99+
plt.yscale('linear') # linear or log
100+
plt.show()
101101

102+
print(df[df != 0])
102103

103104
# df = pf.dcf.monte_carlo_wealth_fv
104105
# print(df)

main_notebook.ipynb

Lines changed: 367 additions & 405 deletions
Large diffs are not rendered by default.

okama/portfolios/dcf.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def __repr__(self):
6060
@property
6161
def discount_rate(self) -> float:
6262
"""
63-
Portfolio cash flow discount rate.
63+
Annual effective discount rate for portfolio cash flow.
6464
6565
Returns
6666
-------
@@ -182,14 +182,10 @@ def wealth_index(self, discounting: Literal["fv", "pv"], include_negative_values
182182
if discounting.lower() == "fv":
183183
return wealth_index_fv
184184
elif discounting.lower() == "pv":
185-
n_rows = wealth_index_fv.shape[0]
186-
discount_factors = (1.0 + self.discount_rate / settings._MONTHS_PER_YEAR) ** np.arange(n_rows)
187-
wealth_index_pv = wealth_index_fv.div(discount_factors, axis=0)
188-
return wealth_index_pv
185+
return dcf_calculations.discount_monthly_cash_flow(wealth_index_fv, self.discount_rate)
189186
else:
190187
raise ValueError("'discounting' must be either 'fv' or 'pv'")
191188

192-
193189
def cash_flow_ts(self, discounting: Literal["fv", "pv"], remove_if_wealth_index_negative: bool = True) -> pd.Series:
194190
"""
195191
Wealth index Future Values (FV) time series for the portfolio with cash flow (contributions and
@@ -240,10 +236,7 @@ def cash_flow_ts(self, discounting: Literal["fv", "pv"], remove_if_wealth_index_
240236
if discounting.lower() == "fv":
241237
return cash_flow_fv
242238
elif discounting.lower() == "pv":
243-
n_rows = cash_flow_fv.shape[0]
244-
discount_factors = (1.0 + self.discount_rate / settings._MONTHS_PER_YEAR) ** np.arange(n_rows)
245-
cash_flow_pv = cash_flow_fv.div(discount_factors, axis=0)
246-
return cash_flow_pv
239+
return dcf_calculations.discount_monthly_cash_flow(cash_flow_fv, self.discount_rate)
247240
else:
248241
raise ValueError("'discounting' must be either 'fv' or 'pv'")
249242

@@ -499,10 +492,7 @@ def monte_carlo_wealth(
499492
if discounting.lower() == "fv":
500493
return monte_carlo_wealth_fv
501494
elif discounting.lower() == "pv":
502-
n_rows = monte_carlo_wealth_fv.shape[0]
503-
discount_factors = (1.0 + self.discount_rate / settings._MONTHS_PER_YEAR) ** np.arange(n_rows)
504-
monte_carlo_wealth_pv = monte_carlo_wealth_fv.div(discount_factors, axis=0)
505-
return monte_carlo_wealth_pv
495+
return dcf_calculations.discount_monthly_cash_flow(monte_carlo_wealth_fv, self.discount_rate)
506496
else:
507497
raise ValueError("'discounting' must be either 'fv' or 'pv'")
508498

@@ -567,9 +557,7 @@ def monte_carlo_cash_flow(
567557
if discounting.lower() == "fv":
568558
return mc_cash_flow_fv
569559
elif discounting.lower() == "pv":
570-
n_rows = mc_cash_flow_fv.shape[0]
571-
discount_factors = (1.0 + self.discount_rate / settings._MONTHS_PER_YEAR) ** np.arange(n_rows)
572-
return mc_cash_flow_fv.div(discount_factors, axis=0)
560+
return dcf_calculations.discount_monthly_cash_flow(mc_cash_flow_fv, self.discount_rate)
573561
else:
574562
raise ValueError("'discounting' must be either 'fv' or 'pv'")
575563

@@ -630,12 +618,12 @@ def plot_forecast_monte_carlo(
630618
years = months / settings._MONTHS_PER_YEAR
631619
periods = years / settings.frequency_periods_per_year[self.cashflow_parameters.frequency]
632620
self.cashflow_parameters.amount *= (1.0 + self.cashflow_parameters.indexation) ** periods
633-
s2 = self.monte_carlo_wealth_fv
621+
s2 = self.monte_carlo_wealth(discounting="fv", include_negative_values=False)
634622
for s in s2:
635623
s2[s].plot(legend=None)
636624
self.cashflow_parameters = backup_obj
637625
else:
638-
s2 = self.monte_carlo_wealth_fv
626+
s2 = self.monte_carlo_wealth(discounting="fv", include_negative_values=False)
639627
s2.plot(legend=None)
640628
self.cashflow_parameters._clear_cf_cache()
641629

@@ -684,7 +672,7 @@ def monte_carlo_survival_period(self, threshold: float = 0) -> pd.Series:
684672
>>> s.quantile(50 / 100)
685673
np.float64(17.5)
686674
"""
687-
s2 = self.monte_carlo_wealth_fv
675+
s2 = self.monte_carlo_wealth(discounting="fv", include_negative_values=False)
688676
dates: pd.Series = helpers.Frame.get_survival_date(s2, self.discount_rate, threshold)
689677
return dates.apply(helpers.Date.get_period_length, args=(self.parent.last_date,))
690678

okama/portfolios/dcf_calculations.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def get_wealth_indexes_fv_with_cashflow(
8585
for n, x in enumerate(ror_cashflow_df.resample(rule=pandas_frequency, convention="start")):
8686
ror_ts = x[1].iloc[:, portfolio_position] # select ror part of the grouped data
8787
cashflow_ts_local = x[1].loc[:, "cashflow_ts"].copy()
88-
# CashFlow inside period
88+
# CashFlow inside period (Extra withdrawals/contributions)
8989
if (cashflow_ts_local != 0).any():
9090
period_wealth_index = pd.Series(dtype=float, name=portfolio_symbol)
9191
for k, (date, r) in enumerate(ror_ts.items()):
@@ -245,4 +245,15 @@ def remove_negative_values(input_s: pd.Series) -> pd.Series:
245245
s[s.index > survival_date] = np.nan
246246
except IndexError:
247247
pass
248-
return s
248+
return s
249+
250+
251+
def discount_monthly_cash_flow(
252+
cash_flow_fv: Union[pd.Series, pd.DataFrame],
253+
annual_effective_discount_rate: float
254+
) -> Union[pd.Series, pd.DataFrame]:
255+
number_of_months = cash_flow_fv.shape[0]
256+
monlthly_discount_rate = (1 + annual_effective_discount_rate) ** (1 / settings._MONTHS_PER_YEAR) - 1
257+
discount_factors = (1.0 + monlthly_discount_rate) ** np.arange(number_of_months)
258+
cash_flow_pv = cash_flow_fv.div(discount_factors, axis=0)
259+
return cash_flow_pv

0 commit comments

Comments
 (0)