import pandas as pd import yfinance as yf from pypfopt import EfficientFrontier from pypfopt import risk_models from pypfopt import expected_returns from pypfopt import HRPOpt, hierarchical_portfolio class CompData: def __init__(self, company_data): """ Class that manages company and stock data """ self.df = company_data self.company_names = self.df["Name"].to_list() self.company_symbols = (self.df["Ticker"] + ".NS").to_list() # utilities for tranlation name_to_id_dict = dict() id_to_name_dict = dict() for CSymbol, CName in zip(self.company_symbols, self.company_names): name_to_id_dict[CName] = CSymbol for CSymbol, CName in zip(self.company_symbols, self.company_names): id_to_name_dict[CSymbol] = CName self.name_to_id = name_to_id_dict self.id_to_name = id_to_name_dict def fetch_stock_data(self, company_ids: list, start_date: str) -> pd.DataFrame: """ Use yfinance client sdk to fetch stock data from the yahoo finance api """ company_data = pd.DataFrame() # get the stock data for the companies for cname in company_ids: stock_data_temp = yf.download( cname, start=start_date, end=pd.Timestamp.now().strftime("%Y-%m-%d") )["Adj Close"] stock_data_temp.name = cname company_data = pd.merge( company_data, stock_data_temp, how="outer", right_index=True, left_index=True, ) # cleaning the data company_data.dropna(axis=1, how="all", inplace=True) company_data.dropna(inplace=True) for i in company_data.columns: company_data[i] = company_data[i].abs() return company_data def comp_id_to_name(self, list_of_ids: list): return [self.id_to_name[i] for i in list_of_ids] def comp_name_to_id(self, list_of_names: list): return [self.name_to_id[i] for i in list_of_names] class PortfolioOptimizer: def __init__(self, comp_data: CompData, company_ids: list, start_date: str): self.comp_data = comp_data self.stock_data = self.comp_data.fetch_stock_data( company_ids, start_date) self.stock_data_returns = self.stock_data.pct_change().dropna() def optimize(self, method: str, ef_parameter=None): company_asset_weights = 0 # Do the portfolio optimization if method == "Efficient Frontier": mu = expected_returns.mean_historical_return(self.stock_data) S = risk_models.sample_cov(self.stock_data) self.ef = EfficientFrontier(mu, S) if ef_parameter == "Maximum Sharpe Raio": self.ef.max_sharpe() elif ef_parameter == "Minimum Volatility": self.ef.min_volatility() elif ef_parameter == "Efficient Risk": self.ef.efficient_risk(0.5) else: self.ef.efficient_return(0.05) company_asset_weights = pd.DataFrame.from_dict( self.ef.clean_weights(), orient="index" ).reset_index() elif method == "Hierarchical Risk Parity": mu = expected_returns.returns_from_prices(self.stock_data) S = risk_models.sample_cov(self.stock_data) self.ef = HRPOpt(mu, S) company_asset_weights = self.ef.optimize() company_asset_weights = pd.DataFrame.from_dict( company_asset_weights, orient="index", columns=["Weight"] ).reset_index() # cleaning the returned data from the optimization company_asset_weights.columns = ["Ticker", "Allocation"] company_asset_weights["Name"] = self.comp_data.comp_id_to_name( company_asset_weights["Ticker"]) company_asset_weights = company_asset_weights[[ "Name", "Ticker", "Allocation"]] return company_asset_weights def get_portfolio_performance(self): if self.ef is not None: ( expected_annual_return, annual_volatility, sharpe_ratio, ) = self.ef.portfolio_performance() st_portfolio_performance = pd.DataFrame.from_dict( { "Expected annual return": (expected_annual_return * 100).round(2), "Annual volatility": (annual_volatility * 100).round(2), "Sharpe ratio": sharpe_ratio.round(2), }, orient="index", ).reset_index() st_portfolio_performance.columns = ["Metrics", "Summary"] return st_portfolio_performance else: return None def get_portfolio_returns(self): return ( self.stock_data_returns * list(self.ef.clean_weights().values()) ).sum(axis=1) def get_annual_portfolio_returns(self): return self.get_portfolio_returns().resample("Y").apply(lambda x: (x + 1).prod() - 1)