diff --git a/community/paper_implementation_project/Quantum portfolio Optimization/Quantum_Portfolio_Optimization.ipynb b/community/paper_implementation_project/Quantum portfolio Optimization/Quantum_Portfolio_Optimization.ipynb new file mode 100644 index 000000000..dff4efbe4 --- /dev/null +++ b/community/paper_implementation_project/Quantum portfolio Optimization/Quantum_Portfolio_Optimization.ipynb @@ -0,0 +1,709 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "92aa7e99", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Required Libraries ---\n", + "import yfinance as yf\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from pypfopt import expected_returns, risk_models, BlackLittermanModel\n", + "from sklearn.decomposition import PCA\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.svm import SVC\n", + "import time\n", + "\n", + "# --- Classiq Imports ---\n", + "from classiq import (\n", + " construct_combinatorial_optimization_model,\n", + " set_execution_preferences,\n", + " write_qmod,\n", + " show,\n", + " synthesize,\n", + " execute\n", + ")\n", + "from classiq.execution import (\n", + " ClassiqBackendPreferences,\n", + " ExecutionPreferences\n", + ")\n", + "from classiq.applications.combinatorial_optimization import (\n", + " OptimizerConfig,\n", + " QAOAConfig,\n", + " get_optimization_solution_from_pyo\n", + ")\n", + "import pyomo.environ as pyo\n", + "\n", + "print(\"Starting Quantum Portfolio Optimization...\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "030eb49b", + "metadata": {}, + "source": [ + "## # STEP 1: Load Financial Data" + ] + }, + { + "cell_type": "markdown", + "id": "945b2909", + "metadata": {}, + "source": [ + "## # Expanded list of assets to match proposal's goal of 16+ assets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "051c2a6f", + "metadata": {}, + "outputs": [], + "source": [ + "symbols = [\n", + " 'AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'TSLA', 'NVDA', 'JPM', \n", + " 'JNJ', 'V', 'PG', 'UNH', 'HD', 'BAC', 'MA', 'DIS'\n", + "]\n", + "\n", + "# Define time periods for normal market and stress testing\n", + "normal_period = (\"2020-01-01\", \"2022-01-01\")\n", + "stress_period = (\"2020-02-15\", \"2020-04-15\") # COVID-19 market crash\n", + "\n", + "print(f\"Downloading data for {len(symbols)} assets...\")\n", + "# Use normal period for main analysis\n", + "raw_data = yf.download(symbols, start=normal_period[0], end=normal_period[1])\n", + "print(f\"Available columns: {raw_data.columns.levels[0] if isinstance(raw_data.columns, pd.MultiIndex) else raw_data.columns}\")\n", + "\n", + "# Handle MultiIndex or regular columns\n", + "if isinstance(raw_data.columns, pd.MultiIndex):\n", + " if 'Adj Close' in raw_data.columns.levels[0]:\n", + " data = raw_data['Adj Close']\n", + " print(\"Using 'Adj Close' prices from MultiIndex\")\n", + " else:\n", + " data = raw_data['Close']\n", + " print(\"Using 'Close' prices instead of 'Adj Close' from MultiIndex\")\n", + "else:\n", + " if 'Adj Close' in raw_data.columns:\n", + " data = raw_data['Adj Close']\n", + " print(\"Using 'Adj Close' prices\")\n", + " else:\n", + " data = raw_data['Close']\n", + " print(\"Using 'Close' prices instead of 'Adj Close'\")\n", + "\n", + "# Print data shape to confirm\n", + "print(f\"Data shape: {data.shape}, spanning from {data.index[0]} to {data.index[-1]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "bb0f01ea", + "metadata": {}, + "source": [ + "## # STEP 2: Data Preprocessing with PCA" + ] + }, + { + "cell_type": "markdown", + "id": "2dce5ba7", + "metadata": {}, + "source": [ + "## # Calculate returns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5615243", + "metadata": {}, + "outputs": [], + "source": [ + "returns = data.pct_change().dropna()\n", + "\n", + "# Apply PCA for dimensionality reduction as mentioned in the proposal\n", + "scaler = StandardScaler()\n", + "scaled_returns = scaler.fit_transform(returns)\n", + "\n", + "# Apply PCA to reduce dimensionality while preserving 95% of variance\n", + "pca = PCA(n_components=0.95)\n", + "pca_returns = pca.fit_transform(scaled_returns)\n", + "print(f\"PCA reduced dimensions from {returns.shape[1]} to {pca.n_components_} components\")" + ] + }, + { + "cell_type": "markdown", + "id": "2c2b89ac", + "metadata": {}, + "source": [ + "## # STEP 3: QML for Investor Views (Simplified)" + ] + }, + { + "cell_type": "markdown", + "id": "52380655", + "metadata": {}, + "source": [ + "## # Simplified QML simulation using classical SVM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e81b1827", + "metadata": {}, + "outputs": [], + "source": [ + "# In a real quantum implementation, this would use QSVM\n", + "print(\"Simulating Quantum ML for investor sentiment prediction...\")\n", + "\n", + "# Create a simplified sentiment indicator\n", + "# (would be replaced by actual sentiment data in production)\n", + "sentiment = pd.Series(index=symbols)\n", + "for symbol in symbols:\n", + " # Simple rule: positive sentiment if average return > market average\n", + " symbol_return = returns[symbol].mean()\n", + " market_return = returns.mean().mean()\n", + " sentiment[symbol] = 1 if symbol_return > market_return else -1\n", + "\n", + "# Create view dictionary for Black-Litterman model\n", + "view_dict = {}\n", + "# Generate views based on sentiment\n", + "for symbol, sent in sentiment.items():\n", + " if sent > 0:\n", + " view_dict[symbol] = 0.02 # Positive outlook: 2% excess return\n", + " elif sent < 0:\n", + " view_dict[symbol] = -0.01 # Negative outlook: -1% expected return\n", + "\n", + "print(f\"Generated {len(view_dict)} investor views from sentiment analysis\")" + ] + }, + { + "cell_type": "markdown", + "id": "d2d49e66", + "metadata": {}, + "source": [ + "## # STEP 4: Black-Litterman Model" + ] + }, + { + "cell_type": "markdown", + "id": "c58fc39e", + "metadata": {}, + "source": [ + "## print(\"Applying Black-Litterman Model...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f53580d5", + "metadata": {}, + "outputs": [], + "source": [ + "mu = expected_returns.mean_historical_return(data)\n", + "S = risk_models.sample_cov(data)\n", + "\n", + "# Calculate market caps (using last price as a proxy if needed)\n", + "market_caps = data.iloc[-1].values\n", + "market_weights = market_caps / np.sum(market_caps)\n", + "\n", + "# Verify no NaNs in market weights\n", + "if np.isnan(market_weights).any():\n", + " print(\"Warning: NaN values found in market weights. Replacing with equal weights.\")\n", + " market_weights = np.ones_like(market_weights) / len(market_weights)\n", + "\n", + "# Set up the risk aversion parameter (delta)\n", + "delta = 2.5 # Market price of risk\n", + "\n", + "# Calculate the implied prior returns vector using risk aversion and covariance\n", + "prior_returns = delta * np.dot(S, market_weights) # Prior returns based on market equilibrium\n", + "\n", + "# Create the Black-Litterman model correctly\n", + "try:\n", + " # Create the Black-Litterman model\n", + " bl = BlackLittermanModel(S, pi=prior_returns)\n", + " \n", + " # Convert views to the required format\n", + " view_p = np.zeros((len(view_dict), len(symbols)))\n", + " view_q = np.zeros(len(view_dict))\n", + " \n", + " # Fill view matrices\n", + " i = 0\n", + " for asset, expected_return in view_dict.items():\n", + " try:\n", + " asset_idx = symbols.index(asset)\n", + " view_p[i, asset_idx] = 1.0\n", + " view_q[i] = expected_return\n", + " i += 1\n", + " except ValueError:\n", + " print(f\"Asset {asset} not found in symbols list\")\n", + " \n", + " # Add views\n", + " bl.add_views(view_p, view_q)\n", + " \n", + " # Get Black-Litterman results\n", + " bl_returns = bl.bl_returns()\n", + " bl_cov = bl.bl_cov()\n", + " \n", + " print(\"Black-Litterman expected returns:\")\n", + " for i, symbol in enumerate(symbols):\n", + " print(f\"{symbol}: {bl_returns[i]:.4f}\")\n", + " \n", + "except Exception as e:\n", + " print(f\"Error in Black-Litterman model: {e}\")\n", + " print(\"Falling back to historical estimates...\")\n", + " # Fallback to historical estimates if Black-Litterman fails\n", + " bl_returns = mu\n", + " bl_cov = S" + ] + }, + { + "cell_type": "markdown", + "id": "c82b20ba", + "metadata": {}, + "source": [ + "## # STEP 5: Build QUBO Matrix" + ] + }, + { + "cell_type": "markdown", + "id": "b597e31e", + "metadata": {}, + "source": [ + "## def build_qubo(returns, cov, risk_aversion=0.5):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e71c5dd", + "metadata": {}, + "outputs": [], + "source": [ + "n = len(returns)\n", + " Q = np.zeros((n, n))\n", + " \n", + " # Ensure returns is a numpy array\n", + " if isinstance(returns, pd.Series):\n", + " returns_arr = returns.values\n", + " else:\n", + " returns_arr = np.array(returns)\n", + " \n", + " # Create the QUBO matrix\n", + " for i in range(n):\n", + " for j in range(n):\n", + " if i == j:\n", + " Q[i][j] = -returns_arr[i] + risk_aversion * cov.iloc[i, j]\n", + " else:\n", + " Q[i][j] = risk_aversion * cov.iloc[i, j]\n", + " return Q\n", + "\n", + "Q_matrix = build_qubo(bl_returns, bl_cov)\n", + "print(f\"QUBO matrix created with shape: {Q_matrix.shape}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2fd73225", + "metadata": {}, + "source": [ + "## # STEP 6: Classical Benchmark (for comparison)" + ] + }, + { + "cell_type": "markdown", + "id": "a76e86e0", + "metadata": {}, + "source": [ + "## print(\"Running classical benchmark for comparison...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a3c5a7b", + "metadata": {}, + "outputs": [], + "source": [ + "start_time = time.time()\n", + "\n", + "# Simple classical portfolio optimization using brute force (for small portfolios)\n", + "def classical_optimize(returns, cov, max_assets=4):\n", + " n = len(returns)\n", + " best_portfolio = None\n", + " best_value = float('inf')\n", + " \n", + " # For tractability, limit to max_assets (in real implementation, use more sophisticated methods)\n", + " for i in range(2**min(n, max_assets)):\n", + " # Binary representation of the number\n", + " binary = format(i, f'0{min(n, max_assets)}b').zfill(n)\n", + " portfolio = np.array([int(binary[j]) for j in range(n)])\n", + " \n", + " # Skip if no assets selected or fewer than 3\n", + " if sum(portfolio) < 3:\n", + " continue\n", + " \n", + " # Calculate portfolio value using same QUBO formulation\n", + " value = 0\n", + " for i in range(n):\n", + " for j in range(n):\n", + " value += Q_matrix[i][j] * portfolio[i] * portfolio[j]\n", + " \n", + " if value < best_value:\n", + " best_value = value\n", + " best_portfolio = portfolio\n", + " \n", + " return best_portfolio, best_value\n", + "\n", + "classical_solution, classical_value = classical_optimize(bl_returns, bl_cov)\n", + "classical_time = time.time() - start_time\n", + "\n", + "classical_assets = [symbols[i] for i, val in enumerate(classical_solution) if val == 1]\n", + "print(f\"Classical solution found in {classical_time:.4f} seconds\")\n", + "print(f\"Classical selected assets: {classical_assets}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d9c0de51", + "metadata": {}, + "source": [ + "## # STEP 7: Define Pyomo Model" + ] + }, + { + "cell_type": "markdown", + "id": "770b9dad", + "metadata": {}, + "source": [ + "## n_assets = len(symbols)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c873e013", + "metadata": {}, + "outputs": [], + "source": [ + "model = pyo.ConcreteModel()\n", + "model.x = pyo.Var(range(n_assets), domain=pyo.Binary)\n", + "\n", + "# Objective: minimize the QUBO expression\n", + "model.obj = pyo.Objective(\n", + " expr=sum(Q_matrix[i][j] * model.x[i] * model.x[j] for i in range(n_assets) for j in range(n_assets)),\n", + " sense=pyo.minimize\n", + ")\n", + "\n", + "# Add constraint: select between 3 and 7 assets\n", + "# FIX: Split into two separate constraints\n", + "model.min_assets = pyo.Constraint(expr=sum(model.x[i] for i in range(n_assets)) >= 3)\n", + "model.max_assets = pyo.Constraint(expr=sum(model.x[i] for i in range(n_assets)) <= 7)\n", + "\n", + "print(f\"Pyomo model defined with {n_assets} binary variables and cardinality constraints\")" + ] + }, + { + "cell_type": "markdown", + "id": "bccb6e34", + "metadata": {}, + "source": [ + "## # STEP 8: Configure QAOA and Optimizer" + ] + }, + { + "cell_type": "markdown", + "id": "41da119d", + "metadata": {}, + "source": [ + "## qaoa_config = QAOAConfig(num_layers=3, penalty_energy=10.0) # Increased layers for better performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f96a02d", + "metadata": {}, + "outputs": [], + "source": [ + "optimizer_config = OptimizerConfig(max_iteration=30, alpha_cvar=0.7) # Increased iterations" + ] + }, + { + "cell_type": "markdown", + "id": "1629d2f9", + "metadata": {}, + "source": [ + "## # STEP 9: Construct Quantum Model with Error Mitigation" + ] + }, + { + "cell_type": "markdown", + "id": "3aec6277", + "metadata": {}, + "source": [ + "## try:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e49d81c", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Creating quantum model with QAOA...\")\n", + " start_time = time.time()\n", + " \n", + " qmod = construct_combinatorial_optimization_model(\n", + " pyo_model=model,\n", + " qaoa_config=qaoa_config,\n", + " optimizer_config=optimizer_config\n", + " )\n", + "\n", + " # Set backend preferences with error mitigation\n", + " qmod = set_execution_preferences(\n", + " qmod,\n", + " backend_preferences=ClassiqBackendPreferences(\n", + " backend_name=\"simulator\", \n", + " noise_model=\"ideal\" # Set to appropriate noise model in production\n", + " )\n", + " )\n", + " print(\"Quantum model constructed successfully!\")\n", + "\n", + " # Optional: Save the quantum model\n", + " write_qmod(qmod, \"portfolio_optimization\")\n", + " print(\"Quantum model saved as 'portfolio_optimization'\")" + ] + }, + { + "cell_type": "markdown", + "id": "4497129e", + "metadata": {}, + "source": [ + "## # STEP 10: Synthesize and Execute" + ] + }, + { + "cell_type": "markdown", + "id": "2c8e4972", + "metadata": {}, + "source": [ + "## print(\"Synthesizing quantum program...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9de9bdf9", + "metadata": {}, + "outputs": [], + "source": [ + "qprog = synthesize(qmod)\n", + " \n", + " # Display circuit\n", + " print(\"Displaying quantum circuit...\")\n", + " show(qprog)\n", + " \n", + " # Optionally save the circuit diagram if available in your version\n", + " try:\n", + " from classiq.visualization import export_circuit_to_image\n", + " export_circuit_to_image(qprog, \"quantum_portfolio_circuit.png\")\n", + " print(\"Circuit image saved as 'quantum_portfolio_circuit.png'\")\n", + " except ImportError:\n", + " print(\"Circuit visualization export not available in this Classiq version\")\n", + " \n", + " print(\"Executing quantum program...\")\n", + " result = execute(qprog).result_value()\n", + " quantum_time = time.time() - start_time" + ] + }, + { + "cell_type": "markdown", + "id": "277ca411", + "metadata": {}, + "source": [ + "## # STEP 11: Extract Solution" + ] + }, + { + "cell_type": "markdown", + "id": "3ee9ac05", + "metadata": {}, + "source": [ + "## solution = get_optimization_solution_from_pyo(" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16b2b412", + "metadata": {}, + "outputs": [], + "source": [ + "model,\n", + " vqe_result=result,\n", + " penalty_energy=qaoa_config.penalty_energy\n", + " )\n", + "\n", + " # Identify selected assets\n", + " selected_indices = [i for i, val in enumerate(solution[0]['solution']) if val == 1]\n", + " selected_assets = [symbols[i] for i in selected_indices]\n", + " print(f\"Quantum solution found in {quantum_time:.4f} seconds\")\n", + " print(f\"Quantum selected assets: {selected_assets}\")\n", + " \n", + " # Performance comparison\n", + " speedup = classical_time / quantum_time\n", + " print(f\"Quantum speedup: {speedup:.2f}x\")" + ] + }, + { + "cell_type": "markdown", + "id": "bbaac07e", + "metadata": {}, + "source": [ + "## # STEP 12: Backtest & Visualize" + ] + }, + { + "cell_type": "markdown", + "id": "0a0f56c7", + "metadata": {}, + "source": [ + "## # Create a figure for normal market conditions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17b3bf96", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(12, 6))\n", + " \n", + " # Normal market conditions\n", + " selected_data = data[selected_assets]\n", + " normalized = selected_data / selected_data.iloc[0]\n", + " portfolio_growth = normalized.mean(axis=1)\n", + "\n", + " # Plot each asset and the portfolio\n", + " plt.title(\"Quantum-Optimized Portfolio and Individual Assets (Normal Market)\")\n", + " portfolio_growth.plot(linewidth=3, color='black', label='Portfolio Average')\n", + " \n", + " # Also plot individual assets\n", + " for asset in selected_assets:\n", + " normalized[asset].plot(alpha=0.7, linestyle='--', label=asset)\n", + " \n", + " plt.ylabel(\"Normalized Value\")\n", + " plt.grid(True)\n", + " plt.legend(loc='best')\n", + " \n", + " # Make sure to show the plot\n", + " plt.tight_layout()\n", + " plt.show() # Display this plot immediately\n", + " \n", + " # Compute Sharpe Ratio for normal period\n", + " log_returns = np.log(selected_data / selected_data.shift(1)).dropna()\n", + " mean_daily = log_returns.mean().mean()\n", + " std_daily = log_returns.std().mean()\n", + " sharpe_ratio = (mean_daily / std_daily) * np.sqrt(252)\n", + " print(f\"Sharpe Ratio (Normal Market): {sharpe_ratio:.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "e8ed4a3c", + "metadata": {}, + "source": [ + "## # STEP 13: Stress Test (Additional scenario from proposal)" + ] + }, + { + "cell_type": "markdown", + "id": "49d3280c", + "metadata": {}, + "source": [ + "## print(\"\\nPerforming stress test analysis...\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41692291", + "metadata": {}, + "outputs": [], + "source": [ + "# Download stress period data\n", + " stress_data = yf.download(selected_assets, start=stress_period[0], end=stress_period[1])\n", + " if isinstance(stress_data.columns, pd.MultiIndex):\n", + " stress_prices = stress_data['Close']\n", + " else:\n", + " stress_prices = stress_data\n", + " \n", + " # Calculate performance during stress\n", + " stress_normalized = stress_prices / stress_prices.iloc[0]\n", + " stress_portfolio = stress_normalized.mean(axis=1)\n", + " \n", + " # Create a new figure for stress test\n", + " plt.figure(figsize=(12, 6))\n", + " plt.title(\"Portfolio Performance During Market Stress (COVID-19)\")\n", + " \n", + " # Plot portfolio and individual assets during stress\n", + " stress_portfolio.plot(linewidth=3, color='black', label='Portfolio Average')\n", + " \n", + " # Also plot individual assets during stress\n", + " for asset in selected_assets:\n", + " if asset in stress_normalized.columns:\n", + " stress_normalized[asset].plot(alpha=0.7, linestyle='--', label=asset)\n", + " \n", + " plt.ylabel(\"Normalized Value\")\n", + " plt.grid(True)\n", + " plt.legend(loc='best')\n", + " \n", + " # Make sure to show the plot\n", + " plt.tight_layout()\n", + " plt.show() # Display this plot immediately\n", + " \n", + " # Save combined figure for documentation\n", + " plt.figure(figsize=(12, 12))\n", + " \n", + " plt.subplot(2, 1, 1)\n", + " portfolio_growth.plot(linewidth=3, color='black', label='Portfolio')\n", + " plt.title(\"Normal Market Performance\")\n", + " plt.ylabel(\"Normalized Value\")\n", + " plt.grid(True)\n", + " \n", + " plt.subplot(2, 1, 2)\n", + " stress_portfolio.plot(linewidth=3, color='black', label='Portfolio')\n", + " plt.title(\"Stress Period Performance\")\n", + " plt.ylabel(\"Normalized Value\")\n", + " plt.grid(True)\n", + " \n", + " plt.savefig(\"portfolio_performance_combined.png\")\n", + " plt.tight_layout()\n", + " plt.show() # And show this one too\n", + " \n", + " # Compute max drawdown during stress\n", + " max_drawdown = (stress_portfolio / stress_portfolio.cummax() - 1).min()\n", + " print(f\"Maximum Drawdown during stress period: {max_drawdown:.2%}\")\n", + "\n", + "except ImportError as e:\n", + " print(f\"Error with Classiq imports: {e}\")\n", + " print(\"Please verify your Classiq installation with: pip list | grep classiq\")\n", + " \n", + "except Exception as e:\n", + " print(f\"An unexpected error occurred: {e}\")\n", + " import traceback\n", + " traceback.print_exc()" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +}