Skip to content

Commit 3cf3150

Browse files
committed
feat: add set_values_monthly() to macro
1 parent 3e33670 commit 3cf3150

File tree

11 files changed

+73
-64
lines changed

11 files changed

+73
-64
lines changed

docs/matplotlib_ext/docscrape.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@ def _parse(self):
397397
msg = "Docstring contains a Receives section but not Yields."
398398
raise ValueError(msg)
399399

400-
for (section, content) in sections:
400+
for section, content in sections:
401401
if not section.startswith(".."):
402402
section = (s.capitalize() for s in section.split(" "))
403403
section = " ".join(section)
@@ -605,7 +605,6 @@ def __init__(self, obj, doc=None, config={}):
605605

606606

607607
class ClassDoc(NumpyDocString):
608-
609608
extra_public_methods = ["__call__"]
610609

611610
def __init__(self, cls, doc=None, modulename="", func_doc=FunctionDoc, config={}):

examples/02 index funds perfomance.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@
439439
}
440440
],
441441
"source": [
442-
"x.index_corr().plot(); # expanding correlation rolling_window = None"
442+
"x.index_corr().plot(); # expanding correlation rolling_window = None"
443443
]
444444
},
445445
{

main.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
# ef = ok.EfficientFrontier(indexes, ccy="RUB", full_frontier=True, inflation=False, n_points=50)
66
# ef.plot_cml(rf_return=0.15, y_axe="cagr")
77
# plt.show()
8+
#
9+
# pf = ok.Portfolio(["SPY.US", "BND.US"], weights=[.5, .5], rebalancing_period="month")
10+
# pf.wealth_index_with_assets.plot()
11+
# plt.show()
12+
13+
i = ok.Inflation("RUB.INFL")
14+
15+
i.set_values_monthly(date="2023-12", value=0.0122)
816

9-
pf = ok.Portfolio(["SPY.US", "BND.US"], weights=[.5, .5], rebalancing_period="monthly")
10-
pf.wealth_index_with_assets.plot()
11-
plt.show()
17+
print(i.values_monthly)
18+
print(i)

okama/common/helpers/helpers.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -359,26 +359,18 @@ class Rebalance:
359359
"""
360360
Methods for rebalancing portfolio.
361361
"""
362+
362363
# From Pandas resamples alias: https://pandas.pydata.org/docs/user_guide/timeseries.html#timeseries-offset-aliases
363-
frequency_mapping = {
364-
"none": "none",
365-
"annually": "Y",
366-
"semi-annually": "2Q",
367-
"quarterly": "Q",
368-
"monthly": "M"
369-
}
370-
371-
def __init__(self,
372-
period: str = "annually",
373-
abs_deviation: Optional[float] = None,
374-
rel_deviation: Optional[float] = None):
364+
frequency_mapping = {"none": "none", "year": "Y", "half-year": "2Q", "quarter": "Q", "month": "M"}
365+
366+
def __init__(
367+
self, period: str = "year", abs_deviation: Optional[float] = None, rel_deviation: Optional[float] = None
368+
):
375369
self.period = period
376370
self.abs_deviation = abs_deviation
377371
self.rel_deviation = rel_deviation
378372
self.pandas_frequency = self.frequency_mapping.get(self.period)
379373

380-
381-
382374
def wealth_ts(self, weights: list, ror: pd.DataFrame) -> pd.Series:
383375
"""
384376
Calculate wealth index time series of rebalanced portfolio given returns time series of the assets.

okama/frontier/multi_period.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def gmv_monthly_weights(self) -> np.ndarray:
228228

229229
# Set the objective function
230230
def objective_function(w):
231-
risk = helpers.Rebalance.return_ts(w, ror, period=period).std()
231+
risk = helpers.Rebalance(period=period).return_ts(w, ror).std()
232232
return risk
233233

234234
# construct the constraints
@@ -272,7 +272,7 @@ def gmv_annual_weights(self) -> np.ndarray:
272272

273273
# Set the objective function
274274
def objective_function(w):
275-
ts = helpers.Rebalance.return_ts(w, ror, period=period)
275+
ts = helpers.Rebalance(period=period).return_ts(w, ror)
276276
mean_return = ts.mean()
277277
risk = ts.std()
278278
return helpers.Float.annualize_risk(risk=risk, mean_return=mean_return)
@@ -296,16 +296,18 @@ def _get_gmv_monthly(self) -> Tuple[float, float]:
296296
Global Minimum Volatility portfolio is a portfolio with the lowest risk of all possible.
297297
"""
298298
return (
299-
helpers.Rebalance.return_ts(
299+
helpers.Rebalance(period=self.rebalancing_period)
300+
.return_ts(
300301
self.gmv_monthly_weights,
301302
self.assets_ror,
302-
period=self.rebalancing_period,
303-
).std(),
304-
helpers.Rebalance.return_ts(
303+
)
304+
.std(),
305+
helpers.Rebalance(period=self.rebalancing_period)
306+
.return_ts(
305307
self.gmv_monthly_weights,
306308
self.assets_ror,
307-
period=self.rebalancing_period,
308-
).mean(),
309+
)
310+
.mean(),
309311
)
310312

311313
@property
@@ -330,7 +332,7 @@ def gmv_annual_values(self) -> Tuple[float, float]:
330332
>>> frontier.gmv_annual_values
331333
(0.03695845106087943, 0.04418318557516887)
332334
"""
333-
returns = helpers.Rebalance.return_ts(self.gmv_annual_weights, self.assets_ror, period=self.rebalancing_period)
335+
returns = helpers.Rebalance(period=self.rebalancing_period).return_ts(self.gmv_annual_weights, self.assets_ror)
334336
return (
335337
helpers.Float.annualize_risk(returns.std(), returns.mean()),
336338
(returns + 1.0).prod() ** (settings._MONTHS_PER_YEAR / returns.shape[0]) - 1.0,
@@ -367,7 +369,7 @@ def global_max_return_portfolio(self) -> dict:
367369
# Set the objective function
368370
def objective_function(w):
369371
# Accumulated return for rebalanced portfolio time series
370-
objective_function.returns = helpers.Rebalance.return_ts(w, ror, period=period)
372+
objective_function.returns = helpers.Rebalance(period=period).return_ts(w, ror)
371373
accumulated_return = (objective_function.returns + 1.0).prod() - 1.0
372374
return -accumulated_return
373375

@@ -393,7 +395,7 @@ def objective_function(w):
393395
return point
394396

395397
def _get_cagr(self, weights):
396-
ts = helpers.Rebalance.return_ts(weights, self.assets_ror, period=self.rebalancing_period)
398+
ts = helpers.Rebalance(period=self.rebalancing_period).return_ts(weights, self.assets_ror)
397399
acc_return = (ts + 1.0).prod() - 1.0
398400
return (1.0 + acc_return) ** (settings._MONTHS_PER_YEAR / ts.shape[0]) - 1.0
399401

@@ -424,7 +426,7 @@ def minimize_risk(self, target_value: float) -> Dict[str, float]:
424426

425427
def objective_function(w):
426428
# annual risk
427-
ts = helpers.Rebalance.return_ts(w, self.assets_ror, period=self.rebalancing_period)
429+
ts = helpers.Rebalance(period=self.rebalancing_period).return_ts(w, self.assets_ror)
428430
risk_monthly = ts.std()
429431
mean_return = ts.mean()
430432
return helpers.Float.annualize_risk(risk_monthly, mean_return)
@@ -480,7 +482,7 @@ def _maximize_risk(self, target_return: float) -> Dict[str, float]:
480482

481483
def objective_function(w):
482484
# annual risk
483-
ts = helpers.Rebalance.return_ts(w, self.assets_ror, period=self.rebalancing_period)
485+
ts = helpers.Rebalance(period=self.rebalancing_period).return_ts(w, self.assets_ror)
484486
risk_monthly = ts.std()
485487
mean_return = ts.mean()
486488
result = -helpers.Float.annualize_risk(risk_monthly, mean_return)
@@ -797,9 +799,8 @@ def get_monte_carlo(self, n: int = 100) -> pd.DataFrame:
797799

798800
# Portfolio risk and cagr for each set of weights
799801
portfolios_ror = weights_df.aggregate(
800-
helpers.Rebalance.return_ts,
802+
helpers.Rebalance(period=self.rebalancing_period).return_ts,
801803
ror=self.assets_ror,
802-
period=self.rebalancing_period,
803804
)
804805
random_portfolios = pd.DataFrame()
805806
for _, data in portfolios_ror.iterrows():

okama/frontier/single_period.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,7 @@ def get_monte_carlo(self, n: int = 100, kind: str = "mean") -> pd.DataFrame:
891891
random_portfolios = helpers.Frame.change_columns_order(random_portfolios, ["Risk", second_column])
892892
return random_portfolios
893893

894-
def plot_transition_map(self, x_axe: str = 'risk', figsize: Optional[tuple] = None) -> plt.axes:
894+
def plot_transition_map(self, x_axe: str = "risk", figsize: Optional[tuple] = None) -> plt.axes:
895895
"""
896896
Plot Transition Map for optimized portfolios on the single period Efficient Frontier.
897897
@@ -941,10 +941,10 @@ def plot_transition_map(self, x_axe: str = 'risk', figsize: Optional[tuple] = No
941941
"""
942942
ef = self.ef_points
943943
linestyle = itertools.cycle(("-", "--", ":", "-."))
944-
if x_axe.lower() == 'cagr':
944+
if x_axe.lower() == "cagr":
945945
xlabel = "CAGR (Compound Annual Growth Rate)"
946946
x_axe = "CAGR"
947-
elif x_axe.lower() == 'risk':
947+
elif x_axe.lower() == "risk":
948948
xlabel = "Risk (volatility)"
949949
x_axe = "Risk"
950950
else:

okama/macro.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55
import pandas as pd
66

7+
import okama.common.validators
78
from okama import settings
89
from okama.api import data_queries, namespaces
910
from okama.common.helpers import helpers
@@ -36,12 +37,9 @@ def __init__(
3637
self._get_symbol_data(symbol)
3738
self._first_date = first_date
3839
self._last_date = last_date
39-
self.first_date: pd.Timestamp = self.values_monthly.index[0].to_timestamp()
40-
self.last_date: pd.Timestamp = self.values_monthly.index[-1].to_timestamp()
41-
self.pl = settings.PeriodLength(
42-
self.values_monthly.shape[0] // settings._MONTHS_PER_YEAR,
43-
self.values_monthly.shape[0] % settings._MONTHS_PER_YEAR,
44-
)
40+
self._values_monthly = self._get_values_monthly()
41+
self._set_first_last_dates()
42+
4543
self._pl_txt = f"{self.pl.years} years, {self.pl.months} months"
4644

4745
def __repr__(self):
@@ -60,6 +58,17 @@ def __repr__(self):
6058
def _check_namespace(self):
6159
pass
6260

61+
def _set_first_last_dates(self):
62+
self.first_date: pd.Timestamp = self.values_monthly.index[0].to_timestamp()
63+
self.last_date: pd.Timestamp = self.values_monthly.index[-1].to_timestamp()
64+
self.pl = settings.PeriodLength(
65+
self.values_monthly.shape[0] // settings._MONTHS_PER_YEAR,
66+
self.values_monthly.shape[0] % settings._MONTHS_PER_YEAR,
67+
)
68+
69+
def _get_values_monthly(self) -> pd.Series:
70+
return data_queries.QueryData.get_macro_ts(self.symbol, self._first_date, self._last_date, period="M")
71+
6372
def _get_symbol_data(self, symbol) -> None:
6473
x = data_queries.QueryData.get_symbol_info(symbol)
6574
self.ticker: str = x["code"]
@@ -78,7 +87,17 @@ def values_monthly(self) -> pd.Series:
7887
Series
7988
Time series of values historical data (monthly).
8089
"""
81-
return data_queries.QueryData.get_macro_ts(self.symbol, self._first_date, self._last_date, period="M")
90+
return self._values_monthly
91+
92+
def set_values_monthly(self, date: str, value: float):
93+
"""
94+
Set monthly value for the past or future date.
95+
96+
The date should be in month period format ("2023-12").
97+
"""
98+
okama.common.validators.validate_real("value", value)
99+
self._values_monthly[pd.Period(date, freq='M')] = value
100+
self._set_first_last_dates()
82101

83102
def describe(self, years: Tuple[int, ...] = (1, 5, 10)) -> pd.DataFrame:
84103
"""

okama/portfolio.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from okama.common.helpers.helpers import Rebalance
1313

1414

15-
1615
class Portfolio(make_asset_list.ListMaker):
1716
"""
1817
Implementation of investment portfolio.
@@ -56,7 +55,7 @@ class Portfolio(make_asset_list.ListMaker):
5655
The weight of an asset is the percent of an investment portfolio that corresponds to the asset.
5756
If weights = None an equally weighted portfolio is created (all weights are equal).
5857
59-
rebalancing_period : {'month', 'year', 'none'}, default 'month'
58+
rebalancing_period : {'none', 'month', 'quarter', 'half-year', 'year'}, default 'month'
6059
Rebalancing period (rebalancing frequency) is predetermined time intervals when
6160
the investor rebalances the portfolio. If 'none' assets weights are not rebalanced.
6261
@@ -176,9 +175,8 @@ def weights_ts(self) -> pd.DataFrame:
176175
>>> plt.show()
177176
"""
178177
if self.rebalancing_period != "month":
179-
return helpers.Rebalance.assets_weights_ts(
178+
return helpers.Rebalance(period=self.rebalancing_period).assets_weights_ts(
180179
ror=self.assets_ror,
181-
period=self.rebalancing_period,
182180
weights=self.weights,
183181
)
184182
values = np.tile(self.weights, (self.ror.shape[0], 1))
@@ -207,7 +205,7 @@ def rebalancing_period(self, rebalancing_period: str):
207205
if rebalancing_period in Rebalance.frequency_mapping.keys():
208206
self._rebalancing_period = rebalancing_period
209207
else:
210-
raise ValueError(f'rebalancing_period must be in {Rebalance.frequency_mapping.keys()}')
208+
raise ValueError(f"rebalancing_period must be in {Rebalance.frequency_mapping.keys()}")
211209

212210
@property
213211
def symbol(self) -> str:

pyproject.toml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,9 @@ matplotlib = "^3.5.1"
3535
requests = "<=2.31.0" # Windows has an issue with 2.32.0
3636
joblib = "^1.1.0"
3737

38-
[tool.poetry.group.test]
39-
optional = true
40-
4138
[tool.poetry.group.test.dependencies]
4239
pytest = "^6.0.0"
43-
black = "^22.3.0"
44-
45-
[tool.poetry.group.docs]
46-
optional = true
40+
black = "^23.11.0"
4741

4842
[tool.poetry.group.docs.dependencies]
4943
sphinx = "^5.0.0"
@@ -55,9 +49,6 @@ pandoc = "^2.2"
5549
recommonmark = "^0.7.1"
5650
Jinja2 = "3.0.3"
5751

58-
[tool.poetry.group.jupyter]
59-
optional = true
60-
6152
[tool.poetry.group.jupyter.dependencies]
6253
jupyter = "^1.0.0"
6354
ipykernel = "^6.15.0"

tests/test_asset_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_currencies(self):
6767
"asset list": "USD",
6868
}
6969
assert self.currencies.names == {
70-
"RUBUSD.FX": "Russian Rouble/US Dollar FX Cross Rate",
70+
"RUBUSD.FX": "Central Bank of Russia rate for RUBUSD (US Dollar)",
7171
"EURUSD.FX": "EURUSD",
7272
"CNYUSD.FX": "Chinese Renminbi/US Dollar FX Cross Rate",
7373
}

0 commit comments

Comments
 (0)