Skip to content

Commit 5ca9ad2

Browse files
committed
- new get_monte_carlo method in Efficient Frontier class
- CAGR is calculated in Efficient Frontier points instead of "CAGR by approximation"
1 parent 3a87450 commit 5ca9ad2

File tree

8 files changed

+480
-165
lines changed

8 files changed

+480
-165
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<img src="https://img.shields.io/badge/python-v3-brightgreen.svg"
55
alt="python"></a> &nbsp;
66
<a href="https://pypi.org/project/okama/">
7-
<img src="https://img.shields.io/badge/pypi-v0.90-brightgreen.svg"
7+
<img src="https://img.shields.io/badge/pypi-v0.91-brightgreen.svg"
88
alt="pypi"></a> &nbsp;
99
<a href="https://opensource.org/licenses/MIT">
1010
<img src="https://img.shields.io/badge/license-MIT-brightgreen.svg"
@@ -18,7 +18,9 @@
1818
_okama_ is a Python package developed for asset allocation and investment portfolio optimization tasks according to Modern Portfolio Theory (MPT).
1919

2020
The package is supplied with **free** «end of day» historical stock markets data and macroeconomic indicators through API.
21-
21+
>...entities should not be multiplied without necessity
22+
>
23+
> -- <cite>William of Ockham (c. 1287–1347)</cite>
2224
## Okama main features
2325

2426
- Investment portfolio constrained Markowitz Mean-Variance Analysis (MVA) and optimization
@@ -29,7 +31,7 @@ The package is supplied with **free** «end of day» historical stock markets da
2931
- Testing distribution on historical data
3032
- Dividend yield and other dividend indicators for stocks
3133
- Backtesting and comparing historical performance of broad range of assets and indexes in multiple currencies
32-
- Methods to track the perfomance of index funds (ETF) and compare them with benchmarks
34+
- Methods to track the performance of index funds (ETF) and compare them with benchmarks
3335
- Main macroeconomic indicators: inflation, central banks rates
3436
- Matplotlib visualization scripts for the Efficient Frontier, Transition map and assets risk / return performance
3537

notebooks/03 efficient frontier.ipynb

Lines changed: 397 additions & 111 deletions
Large diffs are not rendered by default.

okama/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@
2929
from okama.helpers import Float, Frame, Rebalance, Date
3030
from okama.settings import namespaces
3131
import okama.settings
32+
33+
__version__ = '0.91'

okama/frontier.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def minimize_risk(self,
154154
Returns a "point" with monthly values:
155155
- weights
156156
- mean return
157-
- aproximate vaue for the CAGR
157+
- CAGR
158158
- risk (std)
159159
Target return is a monthly or annual value:
160160
monthly_return = False / True
@@ -190,15 +190,19 @@ def objective_function(w):
190190
# Annualize risk and return
191191
a_r = Float.annualize_return(target_return)
192192
a_risk = Float.annualize_risk(risk=risk, mean_return=target_return)
193-
# Risk adjusted return approximation
194-
r_gmean = Float.approx_return_risk_adjusted(mean_return=a_r, std=a_risk)
193+
# # Risk adjusted return approximation
194+
# r_gmean = Float.approx_return_risk_adjusted(mean_return=a_r, std=a_risk)
195+
# CAGR calculation
196+
portfolio_return_ts = Frame.get_portfolio_return_ts(weights.x, ror)
197+
cagr = Frame.get_cagr(portfolio_return_ts)
195198
if not self.labels_are_tickers:
196199
asset_labels = list(self.names.values())
197200
else:
198201
asset_labels = self.symbols
199202
point = {x: y for x, y in zip(asset_labels, weights.x)}
200203
point['Mean return'] = a_r
201-
point['CAGR (approx)'] = r_gmean
204+
point['CAGR'] = cagr
205+
# point['CAGR (approx)'] = r_gmean
202206
point['Risk'] = a_risk
203207
else:
204208
raise Exception("No solutions were found")
@@ -234,7 +238,7 @@ def ef_points(self) -> pd.DataFrame:
234238
The columns of the DataFrame:
235239
- weights
236240
- mean return
237-
- aproximate vaue for the CAGR
241+
- CAGR
238242
- risk (std)
239243
All the values are annualized.
240244
"""
@@ -243,5 +247,29 @@ def ef_points(self) -> pd.DataFrame:
243247
for x in target_rs:
244248
row = self.minimize_risk(x, monthly_return=True)
245249
df = df.append(row, ignore_index=True)
246-
df = Frame.change_columns_order(df, ['Risk', 'Mean return', 'CAGR (approx)'])
250+
df = Frame.change_columns_order(df, ['Risk', 'Mean return', 'CAGR'])
247251
return df
252+
253+
def get_monte_carlo(self, n: int = 100, kind: str = 'mean') -> pd.DataFrame:
254+
"""
255+
Generate N random risk / cagr point for portfolios.
256+
Risk and cagr are calculated for a set of random weights.
257+
"""
258+
weights_series = Float.get_random_weights(n, self.ror.shape[1])
259+
260+
# Portfolio risk and return for each set of weights
261+
random_portfolios = pd.DataFrame(dtype=float)
262+
for weights in weights_series:
263+
risk_monthly = Frame.get_portfolio_risk(weights, self.ror)
264+
mean_return_monthly = Frame.get_portfolio_mean_return(weights, self.ror)
265+
risk = Float.annualize_risk(risk_monthly, mean_return_monthly)
266+
mean_return = Float.annualize_return(mean_return_monthly)
267+
if kind == 'cagr':
268+
cagr = Float.approx_return_risk_adjusted(mean_return, risk)
269+
row = dict(Risk=risk, CAGR=cagr)
270+
elif kind == 'mean':
271+
row = dict(Risk=risk, Return=mean_return)
272+
else:
273+
raise ValueError('kind should be "mean" or "cagr"')
274+
random_portfolios = random_portfolios.append(row, ignore_index=True)
275+
return random_portfolios

okama/frontier_reb.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -490,16 +490,10 @@ def ef_points(self) -> pd.DataFrame:
490490

491491
def get_monte_carlo(self, n: int = 100) -> pd.DataFrame:
492492
"""
493-
Calculates random risk / cagr point for rebalanced portfolios for a given asset list.
493+
Generate N random risk / cagr point for rebalanced portfolios.
494494
Risk and cagr are calculated for a set of random weights.
495495
"""
496-
# Random weights
497-
rand_nos = np.random.rand(n, self.ror.shape[1])
498-
weights_transposed = rand_nos.transpose() / rand_nos.sum(axis=1)
499-
weights = weights_transposed.transpose()
500-
weights_df = pd.DataFrame(weights)
501-
# weights_df = weights_df.aggregate(list, axis=1) # Converts df to DataFrame of lists
502-
weights_df = weights_df.aggregate(np.array, axis=1) # Converts df to DataFrame of np.array
496+
weights_df = Float.get_random_weights(n, self.ror.shape[1])
503497

504498
# Portfolio risk and cagr for each set of weights
505499
portfolios_ror = weights_df.aggregate(Rebalance.rebalanced_portfolio_return_ts, ror=self.ror, period=self.reb_period)

okama/helpers.py

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ def approx_return_risk_adjusted(mean_return: float, std: float) -> float:
5353
"""
5454
return np.exp(np.log(1. + mean_return) - 0.5 * std ** 2 / (1. + mean_return) ** 2) - 1.
5555

56+
@staticmethod
57+
def get_random_weights(n: int, w_shape: int) -> pd.Series:
58+
"""
59+
Produce N random normalized weights of a given shape.
60+
"""
61+
# Random weights
62+
rand_nos = np.random.rand(n, w_shape)
63+
weights_transposed = rand_nos.transpose() / rand_nos.sum(axis=1)
64+
weights = weights_transposed.transpose()
65+
weights_df = pd.DataFrame(weights)
66+
return weights_df.aggregate(np.array, axis=1) # Converts df to DataFrame of np.array
67+
5668

5769
class Frame:
5870
"""
@@ -81,41 +93,14 @@ def get_portfolio_return_ts(cls, weights: list, ror: pd.DataFrame) -> pd.Series:
8193
@classmethod
8294
def get_portfolio_mean_return(cls, weights: list, ror: pd.DataFrame) -> float:
8395
"""
84-
Computes mean return of a portfolio (month scale).
96+
Computes mean return of a portfolio (monthly).
8597
"""
8698
# cls.weights_sum_is_one(weights)
8799
weights = np.asarray(weights)
88100
if isinstance(ror.mean(), float): # required for a single asset portfolio
89101
return ror.mean()
90102
return weights.T @ ror.mean()
91103

92-
@staticmethod
93-
def get_ror(close_ts: pd.Series, period: str = 'M') -> pd.Series:
94-
"""
95-
Calculates rate of return time series given a close ts.
96-
Periods:
97-
'D' - daily return
98-
'M' - monthly return
99-
"""
100-
if period == 'D':
101-
ror = close_ts.pct_change().iloc[1:]
102-
return ror
103-
if period == 'M':
104-
close_ts = close_ts.resample('M').last()
105-
# Replacing zeroes by NaN and padding
106-
# TODO: replace with pd .where(condition, value, inplace=True)
107-
if (close_ts == 0).any():
108-
toxic = close_ts[close_ts == 0]
109-
for i in toxic.index:
110-
close_ts[i] = None
111-
close_ts.fillna(method='backfill', inplace=True, limit=3)
112-
if close_ts.isna().any():
113-
raise Exception("Too many NaN or zeros in data. Can't pad the data.")
114-
ror = close_ts.pct_change().iloc[1:]
115-
return ror
116-
else:
117-
raise TypeError(f"{period} is not a supported period")
118-
119104
@staticmethod
120105
def get_cagr(ror: Union[pd.DataFrame, pd.Series]) -> Union[pd.Series, float]:
121106
"""

okama/plots.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def plot_assets(self, kind='mean', tickers='tickers', pct_values=False) -> plt.a
3535
Plots assets scatter (annual risks, annual returns) with the tickers annotations.
3636
type:
3737
mean - mean return
38-
cagr_app - CAGR by approximation
3938
cagr - CAGR from monthly returns time series
4039
tickers:
4140
- 'tickers' - shows tickers values (default)
@@ -45,12 +44,14 @@ def plot_assets(self, kind='mean', tickers='tickers', pct_values=False) -> plt.a
4544
if kind == 'mean':
4645
risks = self.risk_annual
4746
returns = Float.annualize_return(self.ror.mean())
48-
elif kind == 'cagr_app':
49-
risks = self.risk_annual
50-
returns = Float.approx_return_risk_adjusted(Float.annualize_return(self.ror.mean()), risks)
47+
# elif kind == 'cagr_app':
48+
# risks = self.risk_annual
49+
# returns = Float.approx_return_risk_adjusted(Float.annualize_return(self.ror.mean()), risks)
5150
elif kind == 'cagr':
5251
risks = self.risk_annual
5352
returns = self.get_cagr().loc[self.symbols]
53+
else:
54+
raise ValueError('kind should be "mean", "cagr" or "cagr_app".')
5455
# set lists for single point scatter
5556
if len(self.symbols) < 2:
5657
risks = [risks]

setup.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
from setuptools import setup
2+
import codecs
3+
import os.path
24

35
# read the contents of README file
4-
from os import path
5-
6-
this_directory = path.abspath(path.dirname(__file__))
7-
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
6+
this_directory = os.path.abspath(os.path.dirname(__file__))
7+
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
88
long_description = f.read()
99

10+
11+
# read version information from __init__.py
12+
def read(rel_path):
13+
here = os.path.abspath(os.path.dirname(__file__))
14+
with codecs.open(os.path.join(here, rel_path), 'r') as fp:
15+
return fp.read()
16+
17+
18+
def get_version(rel_path):
19+
for line in read(rel_path).splitlines():
20+
if line.startswith('__version__'):
21+
delim = '"' if '"' in line else "'"
22+
return line.split(delim)[1]
23+
else:
24+
raise RuntimeError("Unable to find version string.")
25+
26+
1027
setup(
1128
name='okama',
12-
version='0.90',
29+
version=get_version("okama/__init__.py"),
1330
license='MIT',
1431
description='Modern Portfolio Theory (MPT) Python package',
1532
long_description=long_description,

0 commit comments

Comments
 (0)