├── .gitignore ├── LICENSE ├── README.md ├── Yahoo_Finance_Tickers.xlsx ├── example.py ├── portfolio_optimize ├── __init__.py └── optimizer.py ├── requirements.txt ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .DS_Store 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | doc/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | 88 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 89 | __pypackages__/ 90 | 91 | # Celery stuff 92 | celerybeat-schedule 93 | celerybeat.pid 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | # pytype static type analyzer 126 | .pytype/ 127 | 128 | # Cython debug symbols 129 | cython_debug/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Manu Jayawardana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Portfolio Optimize 2 | 3 | A simple Python package for optimizing investment portfolios using historical return data from Yahoo Finance. Users can easily determine the optimal portfolio allocation among a given set of tickers based on the mean-variance optimization method or other algorithms. 4 | 5 | ## Features 6 | 7 | - Easy-to-use interface for defining a portfolio of tickers. 8 | - Supports customization of the data window (in years) for historical data analysis. 9 | - Allows choosing between mean-variance optimization and other optimization algorithms. 10 | - Includes functionality to plot the efficient frontier for the selected portfolio. 11 | 12 | ## Installation 13 | 14 | ``` 15 | pip install portfolio-optimize 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### Portfolio Optimization 21 | 22 | ```python 23 | from portfolio_optimize.portfolio_optimize import PortfolioOptimize 24 | 25 | # Initialize the optimizer 26 | portfolio = PortfolioOptimize(tickers=["MSFT", "AAPL", "GOOG"], window=5, optimization="MV") 27 | 28 | # Optimize the portfolio 29 | optimal_weights = portfolio.optimize() 30 | 31 | print(optimal_weights) 32 | ``` 33 | 34 | ### Plotting the Efficient Frontier 35 | 36 | ```python 37 | # Assuming you've already created and optimized the `portfolio` as shown above 38 | 39 | # Plot the efficient frontier for the set of tickers 40 | portfolio.graph() 41 | ``` 42 | 43 | ## License 44 | 45 | This project is licensed under the MIT License - see the LICENSE file for details. 46 | 47 | ## Disclaimer 48 | 49 | This software is provided for educational purposes only. It is not intended for financial, investment, trading, or any other type of professional advice. Use at your own risk. The author(s) and contributors do not accept any responsibility for any decisions or actions taken based on the use of this software. Always conduct your own research and consult with financial advisors before making any investment decisions. 50 | 51 | ## Contributing 52 | 53 | This project is under ongoing development, and contributions, corrections, and improvements are welcome. Please feel free to open issues or pull requests on [GitHub](https://github.com/manujajay/portfolio-optimize/tree/main) if you have suggestions or code enhancements. 54 | -------------------------------------------------------------------------------- /Yahoo_Finance_Tickers.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manujajay/portfolio-optimize/95bf4fc4353744504ea0120677657e316d08a110/Yahoo_Finance_Tickers.xlsx -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from portfolio_optimize import PortfolioOptimize 2 | 3 | # Define your stock tickers and parameters 4 | tickers = ["AAPL", "MSFT", "GOOG"] 5 | window = 5 # years of data 6 | optimization = "MV" # mean-variance optimization 7 | 8 | # Initialize and run the optimizer 9 | portfolio = PortfolioOptimize(tickers=tickers, window=window, optimization=optimization) 10 | optimal_weights = portfolio.optimize() 11 | print("Optimal Weights:", optimal_weights) 12 | 13 | # Plot the efficient frontier 14 | portfolio.graph() 15 | 16 | # Perform and display backtesting results 17 | portfolio.backtest() -------------------------------------------------------------------------------- /portfolio_optimize/__init__.py: -------------------------------------------------------------------------------- 1 | from .optimizer import PortfolioOptimize 2 | -------------------------------------------------------------------------------- /portfolio_optimize/optimizer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import yfinance as yf 4 | from datetime import datetime, timedelta 5 | import matplotlib.pyplot as plt 6 | from scipy.optimize import minimize 7 | from tqdm.auto import tqdm 8 | 9 | class PortfolioOptimize: 10 | def __init__(self, tickers, window=5, optimization='MV'): 11 | """ 12 | Initializes the portfolio optimization class. 13 | 14 | Parameters: 15 | - tickers: List of stock tickers to include in the portfolio. 16 | - window: The number of years of historical data to consider for optimization. 17 | - optimization: The type of optimization to perform ('MV' for mean-variance). 18 | """ 19 | self.tickers = tickers 20 | self.window = window 21 | self.optimization = optimization 22 | self.weights = None 23 | self.returns = None 24 | self.cov_matrix = None 25 | self.data = None 26 | self.risk_free_rate = None 27 | 28 | def fetch_risk_free_rate(self): 29 | """Fetches the current risk-free rate using the 13-week Treasury bill rate (^IRX) as a proxy.""" 30 | print("Fetching the current risk-free rate...") 31 | treasury_ticker = '^IRX' 32 | end_date = datetime.today() 33 | start_date = end_date - timedelta(days=365 * self.window) 34 | treasury_data = yf.download(treasury_ticker, start=start_date, end=end_date)['Adj Close'] 35 | # Convert the average annual yield to a daily rate 36 | self.risk_free_rate = treasury_data.mean() / 100 / 252 37 | print("Risk-free rate fetched.") 38 | 39 | 40 | def fetch_data(self): 41 | """Fetches historical stock data for the given tickers.""" 42 | print("Fetching historical stock data...") 43 | end_date = datetime.today() 44 | start_date = end_date - timedelta(days=365 * self.window) 45 | self.data = yf.download(self.tickers, start=start_date, end=end_date)['Adj Close'] 46 | print("Stock data fetched.") 47 | 48 | 49 | def calculate_expected_returns_and_cov(self): 50 | """Calculates expected returns and the covariance matrix for the stocks.""" 51 | print("Calculating expected returns and covariance matrix...") 52 | returns = self.data.pct_change().dropna() 53 | self.returns = returns.mean() 54 | self.cov_matrix = returns.cov() 55 | print("Calculations completed.") 56 | 57 | def optimize(self): 58 | """ 59 | Optimizes the portfolio to maximize the Sharpe ratio, which is the ratio of 60 | excess return to volatility. 61 | """ 62 | print("Starting portfolio optimization...") 63 | if self.data is None: 64 | self.fetch_data() 65 | if self.risk_free_rate is None: 66 | self.fetch_risk_free_rate() 67 | self.calculate_expected_returns_and_cov() 68 | 69 | num_assets = len(self.tickers) 70 | bounds = tuple((0.0, 1.0) for asset in range(num_assets)) 71 | 72 | def objective(weights): 73 | port_return = np.dot(weights, self.returns) * 252 74 | port_volatility = np.sqrt(np.dot(weights.T, np.dot(self.cov_matrix, weights))) * np.sqrt(252) 75 | sharpe_ratio = (port_return - self.risk_free_rate) / port_volatility 76 | return -sharpe_ratio # We minimize the negative Sharpe ratio to maximize it 77 | 78 | constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1},) 79 | initial_guess = num_assets * [1. / num_assets,] 80 | 81 | result = minimize(objective, initial_guess, method='SLSQP', bounds=bounds, constraints=constraints) 82 | self.weights = result.x 83 | print("Optimization completed.") 84 | return dict(zip(self.tickers, self.weights)) 85 | 86 | def graph(self): 87 | """ 88 | Plots the efficient frontier for the portfolio. The efficient frontier shows 89 | the highest expected return for a given level of risk. 90 | """ 91 | if self.cov_matrix is None or self.returns is None: 92 | print("You must optimize the portfolio before plotting.") 93 | return 94 | 95 | print("Plotting the efficient frontier...") 96 | num_portfolios = 10000 97 | results = np.zeros((3, num_portfolios)) 98 | 99 | for i in tqdm(range(num_portfolios), desc="Simulating portfolios"): 100 | weights = np.random.random(len(self.tickers)) 101 | weights /= np.sum(weights) 102 | port_return = np.dot(weights, self.returns) * 252 103 | port_volatility = np.sqrt(np.dot(weights.T, np.dot(self.cov_matrix, weights))) * np.sqrt(252) 104 | sharpe_ratio = (port_return - self.risk_free_rate) / port_volatility 105 | results[0,i] = port_volatility 106 | results[1,i] = port_return 107 | results[2,i] = sharpe_ratio 108 | 109 | plt.scatter(results[0,:], results[1,:], c=results[2,:], cmap='viridis') 110 | plt.colorbar(label='Sharpe Ratio') 111 | plt.xlabel('Volatility (Standard Deviation)') 112 | plt.ylabel('Expected Return') 113 | plt.title('Efficient Frontier') 114 | plt.show() 115 | 116 | def portfolio_performance(self, weights): 117 | """ 118 | Calculates the performance of the portfolio based on the given weights. 119 | 120 | Returns the portfolio's expected annual return, volatility, and Sharpe ratio. 121 | """ 122 | print("Calculating portfolio performance...") 123 | port_return = np.dot(weights, self.returns) * 252 124 | port_volatility = np.sqrt(np.dot(weights.T, np.dot(self.cov_matrix, weights))) * np.sqrt(252) 125 | sharpe_ratio = (port_return - self.risk_free_rate) / port_volatility 126 | return port_return, port_volatility, sharpe_ratio 127 | 128 | def backtest(self): 129 | """ 130 | Simulates historical performance of the optimized portfolio, plots the cumulative returns, 131 | and calculates key performance metrics. 132 | """ 133 | if self.weights is None: 134 | print("Optimization must be completed before backtesting.") 135 | return 136 | 137 | # Calculate daily returns of the portfolio 138 | daily_returns = self.data.pct_change() 139 | portfolio_daily_returns = daily_returns.dot(self.weights) 140 | 141 | # Calculate cumulative returns 142 | cumulative_returns = (1 + portfolio_daily_returns).cumprod() 143 | 144 | # Plot cumulative returns 145 | plt.figure(figsize=(10, 6)) 146 | cumulative_returns.plot() 147 | plt.title('Portfolio Cumulative Returns') 148 | plt.xlabel('Date') 149 | plt.ylabel('Cumulative Returns') 150 | plt.show() 151 | 152 | # Performance metrics 153 | total_return = cumulative_returns.iloc[-1] - 1 154 | annualized_return = np.power(cumulative_returns.iloc[-1], 252 / len(portfolio_daily_returns)) - 1 155 | annualized_volatility = portfolio_daily_returns.std() * np.sqrt(252) 156 | sharpe_ratio = (annualized_return - self.risk_free_rate) / annualized_volatility 157 | 158 | # Display performance metrics 159 | print(f"Total Return: {total_return:.2%}") 160 | print(f"Annualized Return: {annualized_return:.2%}") 161 | print(f"Annualized Volatility: {annualized_volatility:.2%}") 162 | print(f"Sharpe Ratio: {sharpe_ratio:.2f}") 163 | 164 | 165 | # Example usage 166 | tickers = ["AAPL", "MSFT", "GOOG"] 167 | portfolio = PortfolioOptimize(tickers=tickers, window=5, optimization='MV') 168 | optimal_weights = portfolio.optimize() 169 | print("Optimal Weights:", optimal_weights) 170 | portfolio.graph() 171 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | yfinance 2 | numpy 3 | pandas 4 | matplotlib 5 | tqdm 6 | mkl 7 | scipy 8 | mkl-service -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="portfolio-optimize", 5 | version="1.2.2", # Incrementing version to reflect new changes 6 | author="Manu Jayawardana", 7 | author_email="manujajayawardanais@gmail.com", 8 | description="A Python package for portfolio optimization. (Note: This package is under ongoing development. Contributions and corrections are welcome!)", 9 | long_description=open('README.md').read(), 10 | long_description_content_type='text/markdown', 11 | url="https://github.com/manujajay/portfolio-optimize", 12 | packages=find_packages(), 13 | install_requires=["numpy", "pandas", "yfinance", "matplotlib", "tqdm"], # Updated dependencies 14 | classifiers=[ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ], 19 | python_requires='>=3.6', 20 | ) 21 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from portfolio_optimize.portfolio_optimize import PortfolioOptimize 2 | 3 | # Define your parameters directly 4 | tickers = ["AAPL", "MSFT", "GOOG"] 5 | window = 5 # years 6 | optimization = "MV" # Mean-Variance Optimization 7 | 8 | # Initialize, optimize, and plot in a few lines 9 | portfolio = PortfolioOptimize(tickers=tickers, window=window, optimization=optimization) 10 | optimal_weights = portfolio.optimize() 11 | print("Optimal Weights:", optimal_weights) 12 | portfolio.graph() 13 | --------------------------------------------------------------------------------