From d66ec7c8eb28e846a047001a80502564c55e124a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Jul 2025 01:59:48 +0000 Subject: [PATCH] Add classical portfolio optimization with scipy and cvxpy Co-authored-by: garveyht --- classical_portfolio.py | 190 +++++++++++++++++++++++++++++++++++++ requirements_classical.txt | 9 ++ 2 files changed, 199 insertions(+) create mode 100644 classical_portfolio.py create mode 100644 requirements_classical.txt diff --git a/classical_portfolio.py b/classical_portfolio.py new file mode 100644 index 0000000..69b1731 --- /dev/null +++ b/classical_portfolio.py @@ -0,0 +1,190 @@ +# Classical Portfolio Optimization - Alternative to D-Wave Implementation +# This version uses scipy and cvxpy instead of quantum solvers + +import numpy as np +import pandas as pd +from scipy.optimize import minimize +import cvxpy as cp +import yfinance as yf +import matplotlib.pyplot as plt + +class ClassicalPortfolioOptimizer: + """Classical portfolio optimization without D-Wave quantum computing.""" + + def __init__(self, stocks, budget=1000, alpha=0.005): + self.stocks = stocks + self.budget = budget + self.alpha = alpha # Risk aversion coefficient + self.data = None + self.returns = None + self.cov_matrix = None + + def load_data(self, file_path=None, start_date='2020-01-01', end_date='2023-01-01'): + """Load stock data from CSV or Yahoo Finance.""" + if file_path: + # Load from CSV + self.data = pd.read_csv(file_path, index_col=0) + else: + # Download from Yahoo Finance + self.data = yf.download(self.stocks, start=start_date, end=end_date)['Adj Close'] + + # Calculate returns and covariance + daily_returns = self.data.pct_change().dropna() + self.returns = daily_returns.mean() * 252 # Annualized returns + self.cov_matrix = daily_returns.cov() * 252 # Annualized covariance + + def optimize_scipy(self): + """Optimize portfolio using scipy (continuous weights).""" + n_stocks = len(self.stocks) + + # Objective function: minimize risk - alpha * return + def objective(weights): + portfolio_return = np.dot(weights, self.returns) + portfolio_risk = np.sqrt(np.dot(weights.T, np.dot(self.cov_matrix, weights))) + return portfolio_risk - self.alpha * portfolio_return + + # Constraints + constraints = [ + {'type': 'eq', 'fun': lambda x: np.sum(x) - 1}, # Weights sum to 1 + ] + + # Bounds: 0 <= weight <= 1 for each stock + bounds = tuple((0, 1) for _ in range(n_stocks)) + + # Initial guess: equal weights + x0 = np.array([1/n_stocks] * n_stocks) + + # Optimize + result = minimize(objective, x0, method='SLSQP', bounds=bounds, constraints=constraints) + + if result.success: + weights = result.x + # Convert weights to number of shares + latest_prices = self.data.iloc[-1] + investment_per_stock = weights * self.budget + shares = (investment_per_stock / latest_prices).astype(int) + + return { + 'shares': dict(zip(self.stocks, shares)), + 'weights': dict(zip(self.stocks, weights)), + 'expected_return': np.dot(weights, self.returns), + 'risk': np.sqrt(np.dot(weights.T, np.dot(self.cov_matrix, weights))), + 'total_cost': sum(shares * latest_prices) + } + else: + print("Optimization failed:", result.message) + return None + + def optimize_cvxpy(self, max_risk=None, min_return=None): + """Optimize portfolio using cvxpy (supports integer constraints).""" + n_stocks = len(self.stocks) + latest_prices = self.data.iloc[-1].values + + # Decision variables: number of shares (integer) + shares = cp.Variable(n_stocks, integer=True) + + # Portfolio value for each stock + portfolio_values = cp.multiply(shares, latest_prices) + + # Total investment + total_investment = cp.sum(portfolio_values) + + # Portfolio weights + weights = portfolio_values / total_investment + + # Expected return + portfolio_return = weights @ self.returns.values + + # Risk (variance) + portfolio_risk = cp.quad_form(weights, self.cov_matrix.values) + + # Constraints + constraints = [ + shares >= 0, # No short selling + total_investment <= self.budget, # Budget constraint + total_investment >= 0.9 * self.budget # Use at least 90% of budget + ] + + # Objective based on formulation + if max_risk is not None: + # Maximize return subject to risk constraint + objective = cp.Maximize(portfolio_return) + constraints.append(portfolio_risk <= max_risk) + elif min_return is not None: + # Minimize risk subject to return constraint + objective = cp.Minimize(portfolio_risk) + constraints.append(portfolio_return >= min_return) + else: + # Default: minimize risk - alpha * return + objective = cp.Minimize(portfolio_risk - self.alpha * portfolio_return) + + # Create and solve problem + problem = cp.Problem(objective, constraints) + problem.solve(solver=cp.GLPK_MI) # Mixed integer solver + + if problem.status == 'optimal': + shares_result = shares.value.astype(int) + total_cost = sum(shares_result * latest_prices) + + # Calculate actual weights and metrics + actual_weights = (shares_result * latest_prices) / total_cost + actual_return = np.dot(actual_weights, self.returns) + actual_risk = np.sqrt(np.dot(actual_weights.T, np.dot(self.cov_matrix, actual_weights))) + + return { + 'shares': dict(zip(self.stocks, shares_result)), + 'weights': dict(zip(self.stocks, actual_weights)), + 'expected_return': actual_return, + 'risk': actual_risk, + 'total_cost': total_cost + } + else: + print("Optimization failed:", problem.status) + return None + +# Example usage +if __name__ == "__main__": + # Example stocks + stocks = ['AAPL', 'MSFT', 'GOOGL', 'AMZN'] + budget = 10000 + + # Create optimizer + optimizer = ClassicalPortfolioOptimizer(stocks, budget, alpha=0.005) + + # Load data + print("Loading stock data...") + optimizer.load_data(start_date='2022-01-01', end_date='2023-12-31') + + # Optimize using scipy (continuous) + print("\n--- Scipy Optimization (Continuous Weights) ---") + result_scipy = optimizer.optimize_scipy() + if result_scipy: + print("Optimal shares:", result_scipy['shares']) + print(f"Expected return: {result_scipy['expected_return']:.2%}") + print(f"Risk (std dev): {result_scipy['risk']:.2%}") + print(f"Total cost: ${result_scipy['total_cost']:.2f}") + + # Optimize using cvxpy (integer shares) + print("\n--- CVXPY Optimization (Integer Shares) ---") + result_cvxpy = optimizer.optimize_cvxpy() + if result_cvxpy: + print("Optimal shares:", result_cvxpy['shares']) + print(f"Expected return: {result_cvxpy['expected_return']:.2%}") + print(f"Risk (std dev): {result_cvxpy['risk']:.2%}") + print(f"Total cost: ${result_cvxpy['total_cost']:.2f}") + + # Risk-bounded optimization + print("\n--- Risk-Bounded Optimization ---") + result_risk_bounded = optimizer.optimize_cvxpy(max_risk=0.20) + if result_risk_bounded: + print("Optimal shares:", result_risk_bounded['shares']) + print(f"Expected return: {result_risk_bounded['expected_return']:.2%}") + print(f"Risk (std dev): {result_risk_bounded['risk']:.2%}") + + # Return-bounded optimization + print("\n--- Return-Bounded Optimization ---") + result_return_bounded = optimizer.optimize_cvxpy(min_return=0.15) + if result_return_bounded: + print("Optimal shares:", result_return_bounded['shares']) + print(f"Expected return: {result_return_bounded['expected_return']:.2%}") + print(f"Risk (std dev): {result_return_bounded['risk']:.2%}") \ No newline at end of file diff --git a/requirements_classical.txt b/requirements_classical.txt new file mode 100644 index 0000000..a949bd8 --- /dev/null +++ b/requirements_classical.txt @@ -0,0 +1,9 @@ +# Requirements for classical portfolio optimization +# No D-Wave dependencies needed + +numpy>=1.21.0 +pandas>=1.3.0 +scipy>=1.7.0 +cvxpy>=1.2.0 +matplotlib>=3.3.4 +yfinance>=0.2.0 \ No newline at end of file