Skip to content

Commit 2632db1

Browse files
committed
feat: the frequency is no longer limited to one year in CWID cash flow strategy
1 parent b26fdba commit 2632db1

File tree

4 files changed

+51
-30
lines changed

4 files changed

+51
-30
lines changed

main.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
cf_strategy = ok.CutWithdrawalsIfDrawdown(pf)
4848

4949
cf_strategy.initial_investment = 10_000_000
50-
cf_strategy.frequency = "year"
51-
cf_strategy.amount = -10_000_000 * 0.05
50+
cf_strategy.frequency = "none"
51+
cf_strategy.amount = -10_000_000 * 0.05 / 12
5252
cf_strategy.indexation = 0.09
5353
cf_strategy.crash_threshold_reduction = [
5454
(.10, .20),
@@ -91,6 +91,7 @@
9191

9292
# wi = pf.dcf.wealth_index(discounting="pv", include_negative_values=False)
9393
cf = pf.dcf.cash_flow_ts(discounting="pv", remove_if_wealth_index_negative=True).resample("Y").sum()
94+
# cf = pf.dcf.cash_flow_ts(discounting="pv", remove_if_wealth_index_negative=True)
9495
# wi = pf.dcf.monte_carlo_wealth(discounting="fv", include_negative_values=False)
9596
# cf = pf.dcf.monte_carlo_cash_flow(discounting="pv", remove_if_wealth_index_negative=True)
9697
# print(cf)
@@ -102,7 +103,7 @@
102103
# plt.yscale('linear') # linear or log
103104
# plt.show()
104105
#
105-
# df = cf[0]
106+
df = cf[0]
106107
cf.plot(
107108
kind="bar",
108109
legend=False

okama/portfolios/cashflow_strategies.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -366,14 +366,13 @@ class TimeSeriesStrategy(CashFlow):
366366
def __init__(
367367
self,
368368
parent: core.Portfolio,
369-
frequency: Optional[str] = "none",
370369
initial_investment: float = 0,
371370
time_series_dic: dict = {},
372371
time_series_discounted_values: bool = False
373372
):
374373
super().__init__(
375374
parent,
376-
frequency=frequency,
375+
frequency="none",
377376
initial_investment=initial_investment,
378377
time_series_dic=time_series_dic,
379378
time_series_discounted_values=time_series_discounted_values
@@ -600,6 +599,7 @@ class CutWithdrawalsIfDrawdown(IndexationStrategy):
600599
def __init__(
601600
self,
602601
parent: core.Portfolio,
602+
frequency: Optional[str] = "year",
603603
initial_investment: float = 1000.0,
604604
time_series_dic: dict = {},
605605
time_series_discounted_values: bool = False,
@@ -609,7 +609,7 @@ def __init__(
609609
):
610610
super().__init__(
611611
parent=parent,
612-
frequency="year",
612+
frequency=frequency,
613613
initial_investment=initial_investment,
614614
time_series_dic=time_series_dic,
615615
time_series_discounted_values=time_series_discounted_values,
@@ -634,17 +634,6 @@ def __repr__(self):
634634
}
635635
return repr(pd.Series(dic))
636636

637-
@property
638-
def frequency(self):
639-
return "year"
640-
641-
@frequency.setter
642-
def frequency(self, value):
643-
if value != "year":
644-
raise AttributeError("In CWAC the 'frequency' can only be equal to a year.")
645-
else:
646-
CashFlow.frequency.fset(self, "year")
647-
648637
@property
649638
def crash_threshold_reduction(self):
650639
return self._crash_threshold_reduction

okama/portfolios/dcf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def cashflow_parameters(self) -> Optional[cf.CashFlow]:
8787

8888
@cashflow_parameters.setter
8989
def cashflow_parameters(self, cashflow_parameters):
90+
self.cashflow_parameters._clear_cf_cache()
9091
self._cashflow_parameters = cashflow_parameters
9192

9293
def set_mc_parameters(self, distribution: str, period: int, number: int):

okama/portfolios/dcf_calculations.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def get_wealth_indexes_fv_with_cashflow(
4343
ror_cashflow_df = ror.assign(cashflow_ts=cash_flow_ts)
4444
ror_cashflow_df.fillna(0, inplace=True)
4545
n_rows = ror.shape[0]
46-
discount_factors = (1.0 + dcf_object.discount_rate / settings._MONTHS_PER_YEAR) ** np.arange(n_rows)
46+
monthly_discount_rate = (1 + dcf_object.discount_rate) ** (1 / settings._MONTHS_PER_YEAR) - 1
47+
discount_factors = (1.0 + monthly_discount_rate) ** np.arange(n_rows)
4748
if task == 'backtest':
4849
if dcf_object.cashflow_parameters.time_series_discounted_values:
4950
ror_cashflow_df.loc[:, "cashflow_ts"] = ror_cashflow_df.loc[:, "cashflow_ts"].mul(discount_factors, axis=0)
@@ -53,28 +54,39 @@ def get_wealth_indexes_fv_with_cashflow(
5354
else:
5455
raise ValueError(f"Unknown task: {task}. It must be 'monte_carlo' or 'backtest'")
5556
else:
56-
ror_cashflow_df = ror.to_frame() if not isinstance(ror, pd.DataFrame) else ror
57+
ror_cashflow_df = ror
5758
ror_cashflow_df.loc[:, "cashflow_ts"] = 0
5859
cash_flow_ts = ror_cashflow_df["cashflow_ts"] # cash flow monthly time series
5960
periods_per_year = settings.frequency_periods_per_year[cashflow_parameters.frequency]
61+
if hasattr(cashflow_parameters, "indexation") and cashflow_parameters.frequency != "none":
62+
indexation_per_period = (1 + cashflow_parameters.indexation) ** (1 / periods_per_year) - 1
6063
if cashflow_parameters.frequency == "month" or cashflow_parameters.NAME == "time_series":
6164
# Fast Calculation
6265
s = pd.Series(dtype=float, name=portfolio_symbol)
6366
for n, row in enumerate(ror.itertuples()):
6467
date = row[0]
6568
r = row[portfolio_position + 1]
6669
if cashflow_parameters.NAME == "fixed_amount":
67-
cashflow = amount * (1 + cashflow_parameters.indexation / settings._MONTHS_PER_YEAR) ** n
70+
cashflow = amount * (1 + indexation_per_period) ** n
6871
elif cashflow_parameters.NAME == "fixed_percentage":
6972
cashflow = cashflow_parameters.percentage / periods_per_year * period_initial_amount
7073
elif cashflow_parameters.NAME == "time_series":
7174
cashflow = 0
75+
elif cashflow_parameters.NAME == "CWID":
76+
withdrawal_without_drawdowns = amount * (1 + indexation_per_period) ** n
77+
if drawdowns[date] < 0:
78+
cashflow = cashflow_parameters.calculate_withdrawal_size(
79+
drawdown=drawdowns[date],
80+
withdrawal_without_drawdowns=withdrawal_without_drawdowns,
81+
)
82+
else:
83+
cashflow = withdrawal_without_drawdowns
7284
else:
7385
raise ValueError("Wrong cashflow strategy name value.")
7486
period_initial_amount = period_initial_amount * (r + 1) + cashflow + cash_flow_ts[date]
7587
date = row[0]
7688
s[date] = period_initial_amount
77-
else:
89+
elif cashflow_parameters.frequency != "month" and cashflow_parameters.frequency != "none":
7890
# Slow Calculation
7991
pandas_frequency = cashflow_parameters._pandas_frequency
8092
months_in_full_period = settings._MONTHS_PER_YEAR / cashflow_parameters.periods_per_year
@@ -98,7 +110,7 @@ def get_wealth_indexes_fv_with_cashflow(
98110
period_wealth_index = period_initial_amount * (1 + ror_ts).cumprod()
99111
# CashFlow END period
100112
if cashflow_parameters.NAME == "fixed_amount":
101-
cashflow_value = amount * (1 + cashflow_parameters.indexation / periods_per_year) ** n
113+
cashflow_value = amount * (1 + indexation_per_period) ** n
102114
elif cashflow_parameters.NAME == "fixed_percentage":
103115
cashflow_value = cashflow_parameters.percentage / periods_per_year * period_initial_amount
104116
elif cashflow_parameters.NAME == "VDS":
@@ -108,14 +120,14 @@ def get_wealth_indexes_fv_with_cashflow(
108120
number_of_periods=n,
109121
)
110122
elif cashflow_parameters.NAME == "CWID":
111-
regular_withdrawal = amount * (1 + cashflow_parameters.indexation / periods_per_year) ** n
123+
withdrawal_without_drawdowns = amount * (1 + indexation_per_period) ** n
112124
if drawdowns[last_date] < 0:
113125
cashflow_value = cashflow_parameters.calculate_withdrawal_size(
114126
drawdown=drawdowns[last_date],
115-
regular_withdrawal=regular_withdrawal,
127+
withdrawal_without_drawdowns=withdrawal_without_drawdowns,
116128
)
117129
else:
118-
cashflow_value = regular_withdrawal
130+
cashflow_value = withdrawal_without_drawdowns
119131
else:
120132
raise ValueError("Wrong cashflow_method value.")
121133
cashflow_value *= period_fraction # adjust cash flow to the period length (months)
@@ -124,6 +136,10 @@ def get_wealth_indexes_fv_with_cashflow(
124136
period_initial_amount = period_final_balance
125137
wealth_df = pd.concat([None if wealth_df.empty else wealth_df, period_wealth_index], sort=False)
126138
s = wealth_df.squeeze()
139+
elif cashflow_parameters.frequency == "none":
140+
s = helpers.Frame.get_wealth_indexes(
141+
ror=ror.loc[:, portfolio_symbol], initial_amount=period_initial_amount_cached
142+
)
127143
first_date = s.index[0]
128144
first_wealth_index_date = first_date - 1 # set first date to one month earlie
129145
s.loc[first_wealth_index_date] = period_initial_amount_cached
@@ -167,7 +183,8 @@ def get_cash_flow_fv(
167183
ror_cashflow_df = ror.assign(cashflow_ts=cash_flow_ts)
168184
ror_cashflow_df.fillna(0, inplace=True)
169185
n_rows = ror.shape[0]
170-
discount_factors = (1.0 + dcf_object.discount_rate / settings._MONTHS_PER_YEAR) ** np.arange(n_rows)
186+
monthly_discount_rate = (1 + dcf_object.discount_rate) ** (1 / settings._MONTHS_PER_YEAR) - 1
187+
discount_factors = (1.0 + monthly_discount_rate) ** np.arange(n_rows)
171188
if task == 'backtest':
172189
if dcf_object.cashflow_parameters.time_series_discounted_values:
173190
ror_cashflow_df.loc[:, "cashflow_ts"] = ror_cashflow_df.loc[:, "cashflow_ts"].mul(discount_factors,
@@ -183,25 +200,36 @@ def get_cash_flow_fv(
183200
ror_cashflow_df.loc[:, "cashflow_ts"] = 0
184201
cash_flow_ts = ror_cashflow_df["cashflow_ts"] # cash flow monthly time series
185202
periods_per_year = settings.frequency_periods_per_year[cashflow_parameters.frequency]
203+
if hasattr(cashflow_parameters, "indexation") and cashflow_parameters.frequency != "none":
204+
indexation_per_period = (1 + cashflow_parameters.indexation) ** (1 / periods_per_year) - 1
186205
if cashflow_parameters.frequency == "month" or cashflow_parameters.NAME == "time_series":
187206
# Fast Calculation
188207
for n, row in enumerate(ror.itertuples()):
189208
date = row[0]
190209
r = row[portfolio_position + 1]
191210
# Calculate regular cash flow
192211
if cashflow_parameters.NAME == "fixed_amount":
193-
cashflow = amount * (1 + cashflow_parameters.indexation / settings._MONTHS_PER_YEAR) ** n
212+
cashflow = amount * (1 + indexation_per_period) ** n
194213
elif cashflow_parameters.NAME == "fixed_percentage":
195214
cashflow = cashflow_parameters.percentage / periods_per_year * period_initial_amount
196215
elif cashflow_parameters.NAME == "time_series":
197216
cashflow = 0
217+
elif cashflow_parameters.NAME == "CWID":
218+
withdrawal_without_drawdowns = amount * (1 + indexation_per_period) ** n
219+
if drawdowns[date] < 0:
220+
cashflow = cashflow_parameters.calculate_withdrawal_size(
221+
drawdown = drawdowns[date],
222+
withdrawal_without_drawdowns = withdrawal_without_drawdowns,
223+
)
224+
else:
225+
cashflow = withdrawal_without_drawdowns
198226
else:
199227
raise ValueError("Wrong cashflow strategy name value.")
200228
# add Extra Withdrawals/Contributions
201229
cs_value = cashflow + cash_flow_ts[date]
202230
period_initial_amount = period_initial_amount * (r + 1) + cs_value
203231
cs_fv[date] = cs_value
204-
else:
232+
elif cashflow_parameters.frequency != "month" and cashflow_parameters.frequency != "none":
205233
# Slow Calculation
206234
pandas_frequency = settings.frequency_mapping[cashflow_parameters.frequency]
207235
months_in_full_period = settings._MONTHS_PER_YEAR / cashflow_parameters.periods_per_year
@@ -224,7 +252,7 @@ def get_cash_flow_fv(
224252
period_wealth_index = period_initial_amount * (1 + ror_ts).cumprod()
225253
# CashFlow END period (Regular Cash Flow)
226254
if cashflow_parameters.NAME == "fixed_amount":
227-
cashflow_value = amount * (1 + cashflow_parameters.indexation / periods_per_year) ** n
255+
cashflow_value = amount * (1 + indexation_per_period) ** n
228256
elif cashflow_parameters.NAME == "fixed_percentage":
229257
cashflow_value = cashflow_parameters.percentage / periods_per_year * period_initial_amount
230258
elif cashflow_parameters.NAME == "VDS":
@@ -234,7 +262,7 @@ def get_cash_flow_fv(
234262
number_of_periods=n,
235263
)
236264
elif cashflow_parameters.NAME == "CWID":
237-
withdrawal_without_drawdowns = amount * (1 + cashflow_parameters.indexation / periods_per_year) ** n
265+
withdrawal_without_drawdowns = amount * (1 + indexation_per_period) ** n
238266
if drawdowns[last_date] < 0:
239267
cashflow_value = cashflow_parameters.calculate_withdrawal_size(
240268
drawdown = drawdowns[last_date],
@@ -251,6 +279,8 @@ def get_cash_flow_fv(
251279
period_initial_amount = period_final_balance
252280
cashflow_ts_local.iloc[-1] += cashflow_value
253281
cs_fv = pd.concat([None if cs_fv.empty else cs_fv, cashflow_ts_local], sort=False)
282+
elif cashflow_parameters.frequency == "none":
283+
cs_fv = pd.Series([0] * len(ror.index), index=ror.index) # all zeroes
254284
return cs_fv
255285

256286

0 commit comments

Comments
 (0)