Skip to content

Commit

Permalink
- new get_monte_carlo method in Efficient Frontier class
Browse files Browse the repository at this point in the history
- CAGR is calculated in Efficient Frontier points instead of "CAGR by approximation"
  • Loading branch information
chilango74 committed Dec 23, 2020
1 parent 3a87450 commit 5ca9ad2
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 165 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<img src="https://img.shields.io/badge/python-v3-brightgreen.svg"
alt="python"></a> &nbsp;
<a href="https://pypi.org/project/okama/">
<img src="https://img.shields.io/badge/pypi-v0.90-brightgreen.svg"
<img src="https://img.shields.io/badge/pypi-v0.91-brightgreen.svg"
alt="pypi"></a> &nbsp;
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/license-MIT-brightgreen.svg"
Expand All @@ -18,7 +18,9 @@
_okama_ is a Python package developed for asset allocation and investment portfolio optimization tasks according to Modern Portfolio Theory (MPT).

The package is supplied with **free** «end of day» historical stock markets data and macroeconomic indicators through API.

>...entities should not be multiplied without necessity
>
> -- <cite>William of Ockham (c. 1287–1347)</cite>
## Okama main features

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

Expand Down
508 changes: 397 additions & 111 deletions notebooks/03 efficient frontier.ipynb

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions okama/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@
from okama.helpers import Float, Frame, Rebalance, Date
from okama.settings import namespaces
import okama.settings

__version__ = '0.91'
40 changes: 34 additions & 6 deletions okama/frontier.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def minimize_risk(self,
Returns a "point" with monthly values:
- weights
- mean return
- aproximate vaue for the CAGR
- CAGR
- risk (std)
Target return is a monthly or annual value:
monthly_return = False / True
Expand Down Expand Up @@ -190,15 +190,19 @@ def objective_function(w):
# Annualize risk and return
a_r = Float.annualize_return(target_return)
a_risk = Float.annualize_risk(risk=risk, mean_return=target_return)
# Risk adjusted return approximation
r_gmean = Float.approx_return_risk_adjusted(mean_return=a_r, std=a_risk)
# # Risk adjusted return approximation
# r_gmean = Float.approx_return_risk_adjusted(mean_return=a_r, std=a_risk)
# CAGR calculation
portfolio_return_ts = Frame.get_portfolio_return_ts(weights.x, ror)
cagr = Frame.get_cagr(portfolio_return_ts)
if not self.labels_are_tickers:
asset_labels = list(self.names.values())
else:
asset_labels = self.symbols
point = {x: y for x, y in zip(asset_labels, weights.x)}
point['Mean return'] = a_r
point['CAGR (approx)'] = r_gmean
point['CAGR'] = cagr
# point['CAGR (approx)'] = r_gmean
point['Risk'] = a_risk
else:
raise Exception("No solutions were found")
Expand Down Expand Up @@ -234,7 +238,7 @@ def ef_points(self) -> pd.DataFrame:
The columns of the DataFrame:
- weights
- mean return
- aproximate vaue for the CAGR
- CAGR
- risk (std)
All the values are annualized.
"""
Expand All @@ -243,5 +247,29 @@ def ef_points(self) -> pd.DataFrame:
for x in target_rs:
row = self.minimize_risk(x, monthly_return=True)
df = df.append(row, ignore_index=True)
df = Frame.change_columns_order(df, ['Risk', 'Mean return', 'CAGR (approx)'])
df = Frame.change_columns_order(df, ['Risk', 'Mean return', 'CAGR'])
return df

def get_monte_carlo(self, n: int = 100, kind: str = 'mean') -> pd.DataFrame:
"""
Generate N random risk / cagr point for portfolios.
Risk and cagr are calculated for a set of random weights.
"""
weights_series = Float.get_random_weights(n, self.ror.shape[1])

# Portfolio risk and return for each set of weights
random_portfolios = pd.DataFrame(dtype=float)
for weights in weights_series:
risk_monthly = Frame.get_portfolio_risk(weights, self.ror)
mean_return_monthly = Frame.get_portfolio_mean_return(weights, self.ror)
risk = Float.annualize_risk(risk_monthly, mean_return_monthly)
mean_return = Float.annualize_return(mean_return_monthly)
if kind == 'cagr':
cagr = Float.approx_return_risk_adjusted(mean_return, risk)
row = dict(Risk=risk, CAGR=cagr)
elif kind == 'mean':
row = dict(Risk=risk, Return=mean_return)
else:
raise ValueError('kind should be "mean" or "cagr"')
random_portfolios = random_portfolios.append(row, ignore_index=True)
return random_portfolios
10 changes: 2 additions & 8 deletions okama/frontier_reb.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,16 +490,10 @@ def ef_points(self) -> pd.DataFrame:

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

# Portfolio risk and cagr for each set of weights
portfolios_ror = weights_df.aggregate(Rebalance.rebalanced_portfolio_return_ts, ror=self.ror, period=self.reb_period)
Expand Down
41 changes: 13 additions & 28 deletions okama/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ def approx_return_risk_adjusted(mean_return: float, std: float) -> float:
"""
return np.exp(np.log(1. + mean_return) - 0.5 * std ** 2 / (1. + mean_return) ** 2) - 1.

@staticmethod
def get_random_weights(n: int, w_shape: int) -> pd.Series:
"""
Produce N random normalized weights of a given shape.
"""
# Random weights
rand_nos = np.random.rand(n, w_shape)
weights_transposed = rand_nos.transpose() / rand_nos.sum(axis=1)
weights = weights_transposed.transpose()
weights_df = pd.DataFrame(weights)
return weights_df.aggregate(np.array, axis=1) # Converts df to DataFrame of np.array


class Frame:
"""
Expand Down Expand Up @@ -81,41 +93,14 @@ def get_portfolio_return_ts(cls, weights: list, ror: pd.DataFrame) -> pd.Series:
@classmethod
def get_portfolio_mean_return(cls, weights: list, ror: pd.DataFrame) -> float:
"""
Computes mean return of a portfolio (month scale).
Computes mean return of a portfolio (monthly).
"""
# cls.weights_sum_is_one(weights)
weights = np.asarray(weights)
if isinstance(ror.mean(), float): # required for a single asset portfolio
return ror.mean()
return weights.T @ ror.mean()

@staticmethod
def get_ror(close_ts: pd.Series, period: str = 'M') -> pd.Series:
"""
Calculates rate of return time series given a close ts.
Periods:
'D' - daily return
'M' - monthly return
"""
if period == 'D':
ror = close_ts.pct_change().iloc[1:]
return ror
if period == 'M':
close_ts = close_ts.resample('M').last()
# Replacing zeroes by NaN and padding
# TODO: replace with pd .where(condition, value, inplace=True)
if (close_ts == 0).any():
toxic = close_ts[close_ts == 0]
for i in toxic.index:
close_ts[i] = None
close_ts.fillna(method='backfill', inplace=True, limit=3)
if close_ts.isna().any():
raise Exception("Too many NaN or zeros in data. Can't pad the data.")
ror = close_ts.pct_change().iloc[1:]
return ror
else:
raise TypeError(f"{period} is not a supported period")

@staticmethod
def get_cagr(ror: Union[pd.DataFrame, pd.Series]) -> Union[pd.Series, float]:
"""
Expand Down
9 changes: 5 additions & 4 deletions okama/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def plot_assets(self, kind='mean', tickers='tickers', pct_values=False) -> plt.a
Plots assets scatter (annual risks, annual returns) with the tickers annotations.
type:
mean - mean return
cagr_app - CAGR by approximation
cagr - CAGR from monthly returns time series
tickers:
- 'tickers' - shows tickers values (default)
Expand All @@ -45,12 +44,14 @@ def plot_assets(self, kind='mean', tickers='tickers', pct_values=False) -> plt.a
if kind == 'mean':
risks = self.risk_annual
returns = Float.annualize_return(self.ror.mean())
elif kind == 'cagr_app':
risks = self.risk_annual
returns = Float.approx_return_risk_adjusted(Float.annualize_return(self.ror.mean()), risks)
# elif kind == 'cagr_app':
# risks = self.risk_annual
# returns = Float.approx_return_risk_adjusted(Float.annualize_return(self.ror.mean()), risks)
elif kind == 'cagr':
risks = self.risk_annual
returns = self.get_cagr().loc[self.symbols]
else:
raise ValueError('kind should be "mean", "cagr" or "cagr_app".')
# set lists for single point scatter
if len(self.symbols) < 2:
risks = [risks]
Expand Down
27 changes: 22 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
from setuptools import setup
import codecs
import os.path

# read the contents of README file
from os import path

this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
this_directory = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
long_description = f.read()


# read version information from __init__.py
def read(rel_path):
here = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(here, rel_path), 'r') as fp:
return fp.read()


def get_version(rel_path):
for line in read(rel_path).splitlines():
if line.startswith('__version__'):
delim = '"' if '"' in line else "'"
return line.split(delim)[1]
else:
raise RuntimeError("Unable to find version string.")


setup(
name='okama',
version='0.90',
version=get_version("okama/__init__.py"),
license='MIT',
description='Modern Portfolio Theory (MPT) Python package',
long_description=long_description,
Expand Down

0 comments on commit 5ca9ad2

Please sign in to comment.