Source code for quant_risk.portfolio.mean_variance

""" This module implements classes for various portfolio optimization methods."""

import numpy as np
import pandas as pd
from typing import OrderedDict, Union
import pypfopt
from pypfopt import expected_returns, risk_models

__all__ = [
    'MeanVariance'
]

[docs]class MeanVariance: """Constructor to instantiate the class based on the input parameters. Parameters ---------- historicalPrices : pd.DataFrame DataFrame of historical prices for each ticker, with column name as name of ticker and index as timestamps tickers : list, optional List of tickers of the assets in the portfolio, by default None frequency: int, optional Frequency of the data passed, default is daily, i.e., 252 days bounds : Union[tuple,list] Minimum and maximum weight of each asset or a single pair if all weights are identical, (-1,1) if shorting is allowed, by default (0,1) riskFreeRate : float, optional Risk free rate, by default None solver : str, optional Name of solver, by default None. List of solvers: cp.installed_solvers() solverOptions : dict, optional Parameters for the given solver in the format {parameter:value}, by default None verbose : bool, optional Whether performance and debugging information should be printed, by default False """ def __init__(self, historicalPrices: pd.DataFrame, frequency: int=252, bounds: Union[tuple,list] = (0,1), riskFreeRate: float = None, solver: str = None, solverOptions: dict = None, verbose: bool = False): """Constructor to instantiate the class based on the input parameters. Parameters ---------- historicalPrices : pd.DataFrame DataFrame of historical prices for each ticker, with column name as name of ticker and index as timestamps tickers : list, optional List of tickers of the assets in the portfolio, by default None frequency: int, optional Frequency of the data passed, default is daily, i.e., 252 days bounds : Union[tuple,list] Minimum and maximum weight of each asset or a single pair if all weights are identical, (-1,1) if shorting is allowed, by default (0,1) riskFreeRate : float, optional Risk free rate, by default None solver : str, optional Name of solver, by default None. List of solvers: cp.installed_solvers() solverOptions : dict, optional Parameters for the given solver in the format {parameter:value}, by default None verbose : bool, optional Whether performance and debugging information should be printed, by default False """ expectedReturns = expected_returns.mean_historical_return(historicalPrices,frequency=frequency) covarianceMatrix = risk_models.CovarianceShrinkage(historicalPrices).ledoit_wolf() self.historicalPrices = historicalPrices self.expectedReturns=expectedReturns self.covarianceMatrix = covarianceMatrix self.portfolio = pypfopt.EfficientFrontier(expectedReturns, covarianceMatrix, bounds, solver, verbose, solverOptions) if riskFreeRate is None: self.riskFreeRate = 0 else: self.riskFreeRate = riskFreeRate self.weights = None
[docs] def fit(self, method: str = 'max_sharpe', **kwargs) -> dict: """Optimize the portfolio by maxizing the Sharpe Ratio, and return the tickers and their respective weights. Parameters ---------- method : str, optional Different methods by which one can maximise the portfolio. Please have a look at the following link for the available methods that are available for optimisation : https://pyportfolioopt.readthedocs.io/en/latest/MeanVariance.html #TODO: We can always add more objectives to the solver so that we can get a better estimate of our weights. # We can take some lower or upper bounds from the investment team as an input and use that as a contraint in our optimization by default 'max_sharpe' Returns ------- dict Returns a dictionary with format {ticker:weight} """ if method not in dir(self.portfolio): raise ValueError(f"The Chosen method '{method}'' is not a valid optimisation method. Please have a look at the documentation and try again.") self.weights = eval(f"dict(self.portfolio.{method}(**kwargs))") return self.weights
[docs] def stats(self, verbose: bool = True) -> tuple: """Generate the expected annual return, annual volatility and Sharpe Ratio of the portfolio. Parameters ---------- verbose : bool, optional Print the statistics, by default True Returns ------- tuple Calculated statistics in the format (expected annual return, annual volatility, Sharpe Ratio) """ stat = self.portfolio.portfolio_performance(verbose=verbose,risk_free_rate=self.riskFreeRate) return stat
[docs] def getRiskFreeRate(self) -> float: """Returns the risk free rate Returns ------- float Risk free rate """ return round(self.riskFreeRate,2)
[docs] def getHistoricalPrices(self) -> pd.DataFrame: """Returns the historical prices Returns ------- pd.DataFrame DataFrame of historical prices for each ticker, with column name as name of ticker and index as timestamps """ return self.historicalPrices
[docs] def getExpectedReturns(self) -> pd.DataFrame: """Returns the expected returns Returns ------- pd.DataFrame DataFrame of expected returns, with index as ticker names """ return self.expectedReturns
[docs] def getCovarianceMatrix(self) -> pd.DataFrame: """Returns the historical prices Returns ------- pd.DataFrame DataFrame of covariance between tickers """ return self.covarianceMatrix