From fb44f5a9c1dd663ff03a27878631b638c968d366 Mon Sep 17 00:00:00 2001 From: Mihidum Gunasekara Date: Tue, 16 Jun 2026 09:19:06 -0500 Subject: [PATCH 1/5] stride formulation --- .gitignore | 2 + README.md | 33 ++++++++- data/larger_example.csv | 14 ++++ portfolio.py | 18 +++-- src/demo_enums.py | 2 + src/multi_period.py | 14 +++- src/single_period.py | 159 ++++++++++++++++++++++++++++++++++++++-- 7 files changed, 225 insertions(+), 17 deletions(-) create mode 100644 data/larger_example.csv diff --git a/.gitignore b/.gitignore index 822fbca..c6abeac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__ cache assets/__generated_theme.css portfolio.png +local_testing +.idea/ diff --git a/README.md b/README.md index a0ba998..c77ca80 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,31 @@ A default CQM run with a transaction cost factor of 1% can be done with the foll `python portfolio.py -m 'CQM' -t 0.01` +##### Stride Runs + +Similar to the CQM runs the user can run the same formulations in using the latest Stride non-linear solver. + +```python portfolio.py -m 'Stride'``` + +Same parameters as the CQM formulation can be used. The output of the default Stride run is printed on the console as follows. + +```bash +Stride run... +Minimize mean-variance expression + +Best feasible solution: +AAPL 65 +MSFT 9 +AAL 1 +WMT 1 + +Estimated Returns: 15.31 +Sales Revenue: 0.00 +Purchase Cost: 999.60 +Transaction Cost: 0.00 +Variance: 2464.38 +``` + ##### DQM Runs The user can select to build a disctrete quadratic model (DQM) using the following command: @@ -272,25 +297,25 @@ expression with an associated penalty coefficient γ. The DQM code contains to do a grid search in order to obtain the best values of the penalty coefficients γ and the risk-aversion coefficient α that result in the best objective value. -### CQM +### CQM and Stride ### Variables Each xi denotes the number of shares of stock i to be purchased. ### Constraints -All CQM formulations include the budget constraint described earlier. +All CQM and Stride formulations include the budget constraint described earlier. ##### Risk-Bounding Formulation This includes an upper bound on the risk as a quadratic constraint. -Such a constraint is supported natively by CQM. +Such a constraint is supported natively by CQM and Stride. ##### Return-Bounding Formulation This includes a lower bound on the returns as a linear inequality constraint. ### Objective -There are 3 CQM formulations, each with a different objective: +There are 3 formulations, each with a different objective: - The bi-objective formulation corresponds to the original problem formulation where a combination of variance and mean is minimized. - The risk-bounding formulation where only the returns are maximized. diff --git a/data/larger_example.csv b/data/larger_example.csv new file mode 100644 index 0000000..e4b788e --- /dev/null +++ b/data/larger_example.csv @@ -0,0 +1,14 @@ +Month,AAPL,MSFT,AAL,WMT,GOOGL,NFLX,EBAY,INTC,KMX,EA +10-Nov,9.541,19.953,10.521,41.735,167.98,435,177.24,232.61,244.56,89.025 +10-Dec,9.891,22.046,9.437,41.842,168.47,436.18,175,233.1,252.043,85.01 +11-Jan,10.405,21.904,9.352,43.502,166.68,434.38,174.48,233.32,257.99,84.91 +11-Feb,10.831,21.12,8.117,40.329,170.16,428.76,177.55,229.74,258.035,83.37 +11-Mar,10.687,20.174,8.211,40.667,173.242,421.17,172.78,229.98,264.51,84.28 +11-Apr,10.737,20.595,8.57,42.956,172.55,414.77,173.22,234.08,270,84.75 +11-May,10.666,20.003,8.579,43.429,172.32,409.24,173.69,233.885,256.01,85.83 +11-Jun,10.293,20.794,8.4,41.793,167.94,402.35,174.15,234.45,244.68,85.11 +11-Jul,11.974,21.914,5.883,41.455,173.73,406.42,176.7,236.18,217.125,88.92 +11-Aug,11.801,21.408,5.27,42.135,170.3,411.42,177.34,233.43,217.31,81.29 +11-Sep,11.693,20.032,5.185,41.113,175.94,416.02,181.38,231.6,218.9,85.95 +11-Oct,12.413,21.433,5.44,44.931,176.945,409.68,179.92,233.61,220.71,80.99 +11-Nov,11.72,20.743,4.45,46.658,180.155,403.32,177.92,228.7,221.59,86 diff --git a/portfolio.py b/portfolio.py index de835a8..fa5b866 100644 --- a/portfolio.py +++ b/portfolio.py @@ -87,9 +87,9 @@ "--model-type", default="CQM", multiple=False, - type=click.Choice(["CQM", "DQM"], case_sensitive=False), + type=click.Choice(["CQM", "DQM", "Stride"], case_sensitive=False), show_default=True, - help="Model type, CQM or DQM", + help="Model type, CQM, Stride or DQM", ) @click.option( "-r", @@ -142,19 +142,25 @@ def main( t_cost: float, ): - solver_type = SolverType.CQM if model_type == "CQM" else SolverType.DQM + if model_type == "CQM": + solver_type = SolverType.CQM + elif model_type == "Stride": + solver_type = SolverType.Stride + else: + solver_type = SolverType.DQM + #solver_type = SolverType.CQM if model_type == "CQM" else SolverType.DQM if (max_risk or min_return) and solver_type is SolverType.DQM: - raise Exception("The bound options require a CQM.") + raise Exception("The bound options require a CQM or Stride.") - if (gamma or bin_size) and solver_type is SolverType.CQM: + if (gamma or bin_size) and solver_type is (SolverType.CQM or SolverType.Stride): raise Exception("The option gamma or bin-size requires a DQM.") if num and not dates: raise Exception("User must provide dates with option 'num'.") if t_cost and solver_type is SolverType.DQM: - raise Exception("The transaction cost option requires a CQM. Set t_cost=0 for DQM.") + raise Exception("The transaction cost option requires a CQM or Stride. Set t_cost=0 for DQM.") if rebalance: print(f"\nRebalancing portfolio optimization run...") diff --git a/src/demo_enums.py b/src/demo_enums.py index 23ba628..5772127 100644 --- a/src/demo_enums.py +++ b/src/demo_enums.py @@ -18,12 +18,14 @@ class SolverType(Enum): CQM = 0 DQM = 1 + Stride = 2 @property def label(self): return { SolverType.CQM: "Quantum Hybrid (CQM)", SolverType.DQM: "Quantum Hybrid (DQM)", + SolverType.Stride: "Quantum Hybrid (Stride)", }[self] diff --git a/src/multi_period.py b/src/multi_period.py index a151a59..c8371f8 100644 --- a/src/multi_period.py +++ b/src/multi_period.py @@ -21,7 +21,7 @@ import matplotlib.pyplot as plt -from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler +from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler from src.single_period import SinglePeriod @@ -180,6 +180,7 @@ def initiate_run_update( self.sampler = { "CQM": LeapHybridCQMSampler(**self.sampler_args), "DQM": LeapHybridDQMSampler(**self.sampler_args), + "Stride": LeapHybridNLSampler(**self.sampler_args), } baseline_result, months, all_solutions, init_holdings, initial_budget = self.run_update( @@ -197,6 +198,7 @@ def initiate_run_update( self.sampler = {} self.sample_set["CQM"] = {} self.sample_set["DQM"] = {} + self.sample_set["Stride"] = {} return baseline_result, months, all_solutions, init_holdings @@ -301,6 +303,16 @@ def run_update( if self.model_type is SolverType.DQM: self.build_dqm() solution = self.solve_dqm() + elif self.model_type is SolverType.Stride: + # Set budget to 0 to enforce that portfolio is self-financing + if self.t_cost and not first_purchase: + self.budget = 0 + + solution = self.solve_stride( + max_risk=max_risk, min_return=min_return, init_holdings=init_holdings + ) + + init_holdings = solution["stocks"] else: # Set budget to 0 to enforce that portfolio is self-financing if self.t_cost and not first_purchase: diff --git a/src/single_period.py b/src/single_period.py index 27f0f69..5d69f9f 100644 --- a/src/single_period.py +++ b/src/single_period.py @@ -19,7 +19,10 @@ import numpy as np import pandas as pd from dimod import Binary, ConstrainedQuadraticModel, DiscreteQuadraticModel, Integer, quicksum -from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler +from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler +from dwave.optimization import Model +from dwave.optimization.mathematical import add, safe_divide +from dwave.optimization.symbols import IntegerVariable, BinaryVariable from src.demo_enums import SolverType from src.utils import get_baseline_data, get_requested_stocks, get_stock_data @@ -55,7 +58,7 @@ def __init__( otherwise, no grid search. file_path (str): Full path of CSV file containing stock data. dates (list of str): Pair of strings for start date and end date. - model_type (str): CQM or DQM. + model_type (str): CQM, Stride or DQM. time_limit (int): The time limit for the runs. alpha (float or int or list or tuple): Risk aversion coefficient. If alpha is a tuple/list and model is DQM, grid search will be done; @@ -105,7 +108,7 @@ def __init__( else: self.bin_size = 10 - self.model = {"CQM": None, "DQM": None} + self.model = {"CQM": None, "DQM": None, "Stride": None} self.sample_set = {} if sampler_args: @@ -116,6 +119,7 @@ def __init__( self.sampler = { "CQM": LeapHybridCQMSampler(**self.sampler_args), "DQM": LeapHybridDQMSampler(**self.sampler_args), + "Stride": LeapHybridNLSampler(**self.sampler_args), } self.solution = {} @@ -488,6 +492,8 @@ def compute_risk_and_returns(self, solution): def print_results(self, solution: dict): """Print results to the console given a solution dictionary.""" is_cqm_run = self.model_type is SolverType.CQM + is_dqm_run = self.model_type is SolverType.DQM + is_stride_run = self.model_type is SolverType.Stride if self.verbose and is_cqm_run: print( @@ -497,7 +503,7 @@ def print_results(self, solution: dict): sep="\n", ) - if not is_cqm_run: + if is_dqm_run: print(f"\nSolution for alpha = {solution['alpha']} and gamma = {solution['gamma']}:") print( @@ -507,16 +513,154 @@ def print_results(self, solution: dict): sep="\n", ) - if is_cqm_run: + if is_cqm_run or is_stride_run: print(f"Sales Revenue: {solution['sales']:.2f}") print(f"Purchase Cost: {solution['cost']:.2f}") - if is_cqm_run: + if is_cqm_run or is_stride_run: print(f"Transaction Cost: {solution['transaction cost']:.2f}") print(f"Variance: {solution['risk']}\n") + def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): + """Build and solve a Stride formulation. + This method allows the user a choice of 3 problem formulations: + 1) max return - alpha*risk (default formulation) + 2) max return s.t. risk <= max_risk + 3) min risk s.t. return >= min_return + + Args: + max_risk (int): Maximum risk for the risk bounding formulation. + min_return (int): Minimum return for the return bounding formulation. + init_holdings (float): Initial holdings, or initial portfolio state. + """ + # Instantiating the Stride object + stride_nl = Model() + + # Required constants + stock_prices = np.array(list(self.price)) + avg_returns = np.array(list(self.avg_monthly_returns)) + StockPrices = stride_nl.constant(stock_prices) + BudgetUpper = stride_nl.constant(self.budget) + BudgetLower = stride_nl.constant(0.997*self.budget) + cov = self.covariance_matrix.loc[self.stocks, self.stocks].to_numpy() + risk_coeff = cov * np.outer(stock_prices, stock_prices) + RiskCoeff = stride_nl.constant(risk_coeff) + AvgReturns = stride_nl.constant(avg_returns*stock_prices) + MaxRisk = stride_nl.constant(max_risk) + MinReturn = stride_nl.constant(min_return) + NegOne = stride_nl.constant(-1) + Alpha = stride_nl.constant(self.alpha) + Zero = stride_nl.constant(0) + TransCost = stride_nl.constant(self.t_cost) + One = stride_nl.constant(1) + Two = stride_nl.constant(2) + BudgetLowerWithTrans = stride_nl.constant(self.budget - 0.003 * self.init_budget) + + # Defining and adding variables to the Stride model + x = stride_nl.integer(len(self.stocks), lower_bound=0, upper_bound=list(self.max_num_shares)) + + # Defining risk expression + risk = [] + for (i1, s1), (i2, s2) in product(enumerate(self.stocks), enumerate(self.stocks)): + risk.append(RiskCoeff[i1, i2] * x[i1] * x[i2]) + + # Defining the returns expression + returns = AvgReturns * x + + # Adding budget and related constraints + if not init_holdings: + init_holdings = self.init_holdings + else: + self.init_holdings = init_holdings + + if not self.t_cost: + stock_purchases = x*StockPrices + stride_nl.add_constraint(stock_purchases.sum() <= BudgetUpper) + stride_nl.add_constraint(stock_purchases.sum() >= BudgetLower) + else: + # Modeling transaction cost + y = stride_nl.binary(len(self.stocks)) # dummy binary variable + x0 = stride_nl.constant(np.array(list(init_holdings.values()))) + lhs = [] + for i, s in enumerate(self.stocks): + curr_return = StockPrices[i] * (One - TransCost) * x[i] + prev_return = StockPrices[i] * (One - TransCost) * x0[i] + curr_cost = Two * TransCost * StockPrices[i] * x[i] * y[i] + prev_cost = Two * TransCost * StockPrices[i] * x0[i] * y[i] + lhs.append(curr_cost + curr_return - prev_cost - prev_return) + # Indicator linking constraints + stride_nl.add_constraint(x[i] - x0[i]*y[i] >= Zero) + stride_nl.add_constraint(x[i] - x[i]*y[i] <= x0[i]) + stride_nl.add_constraint(add(*lhs) <= BudgetUpper) + stride_nl.add_constraint(add(*lhs) >= BudgetLowerWithTrans) + + if max_risk: + print("Maximize returns s.t. an upper bound of risk") + # Adding maximum risk constraint + stride_nl.add_constraint(add(*risk) <= MaxRisk) + # Objective: maximize return + stride_nl.minimize(NegOne*returns.sum()) + elif min_return: + print("Minimize risk s.t. a lower bound of return") + # Adding minimum returns constraint + stride_nl.add_constraint(returns.sum() >= MinReturn) + # Objective: minimize risk + stride_nl.minimize(add(*risk)) + else: + print("Minimize mean-variance expression") + # Objective: minimize mean-variance expression + stride_nl.minimize(Alpha*add(*risk) - returns.sum()) + """ + Alternative objective function - minimize the ratio of risk per unit return. This can be also done in Stride. + obj_expr = safe_divide((Alpha * add(*risk)), returns.sum()) + stride_nl.minimize(obj_expr) + """ + + self.model["Stride"] = stride_nl + self.sample_set["Stride"] = self.sampler["Stride"].sample(stride_nl, label="Example - Portfolio Optimization", time_limit=self.time_limit) + stride_nl.lock() + solution = {} + if stride_nl.feasible(): + model_des = list(sym for sym in stride_nl.iter_decisions()) + x_var = None + y_var = None + for i in range(len(model_des)): + if type(model_des[i]) == IntegerVariable: + x_var = model_des[i] + elif type(model_des[i]) == BinaryVariable: + y_var = model_des[i] + """print(f"x variables: {x_var.state(0)}") + if y_var is not None: + print(f"y variables: {y_var.state(0)}")""" + solution["stocks"] = {s: int(x_var.state(0)[i]) for i, s in enumerate(self.stocks)} + solution["return"], solution["risk"] = self.compute_risk_and_returns(solution["stocks"]) + cost = sum( + [ + self.price[s] * max(0, solution["stocks"][s] - self.init_holdings[s]) + for s in self.stocks + ] + ) + sales = sum( + [ + self.price[s] * max(0, self.init_holdings[s] - solution["stocks"][s]) + for s in self.stocks + ] + ) + transaction = self.t_cost * (cost + sales) + solution.update( + { + "sales": sales, + "cost": cost, + "transaction cost": transaction, + } + ) + else: + raise Exception("No feasible solution could be found for this problem instance.") + + return solution + def run( self, min_return: float = 0, max_risk: float = 0, num: int = 0, init_holdings: float = None ): @@ -537,6 +681,9 @@ def run( solution = self.solve_cqm( min_return=min_return, max_risk=max_risk, init_holdings=init_holdings ) + elif self.model_type is SolverType.Stride: + print(f"\nStride run...") + solution = self.solve_stride(min_return=min_return, max_risk=max_risk, init_holdings=init_holdings) else: print(f"\nDQM run...") if len(self.alpha_list) > 1 or len(self.gamma_list) > 1: From 09bdd351a0446c3d198807920805d0a046ed1120 Mon Sep 17 00:00:00 2001 From: Mihidum Gunasekara Date: Tue, 16 Jun 2026 15:56:33 -0500 Subject: [PATCH 2/5] added unit tests for stride integration --- src/single_period.py | 8 ++------ tests/test_portfolio.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/single_period.py b/src/single_period.py index 5d69f9f..08371fe 100644 --- a/src/single_period.py +++ b/src/single_period.py @@ -548,8 +548,6 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): risk_coeff = cov * np.outer(stock_prices, stock_prices) RiskCoeff = stride_nl.constant(risk_coeff) AvgReturns = stride_nl.constant(avg_returns*stock_prices) - MaxRisk = stride_nl.constant(max_risk) - MinReturn = stride_nl.constant(min_return) NegOne = stride_nl.constant(-1) Alpha = stride_nl.constant(self.alpha) Zero = stride_nl.constant(0) @@ -599,12 +597,14 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): if max_risk: print("Maximize returns s.t. an upper bound of risk") # Adding maximum risk constraint + MaxRisk = stride_nl.constant(max_risk) stride_nl.add_constraint(add(*risk) <= MaxRisk) # Objective: maximize return stride_nl.minimize(NegOne*returns.sum()) elif min_return: print("Minimize risk s.t. a lower bound of return") # Adding minimum returns constraint + MinReturn = stride_nl.constant(min_return) stride_nl.add_constraint(returns.sum() >= MinReturn) # Objective: minimize risk stride_nl.minimize(add(*risk)) @@ -625,15 +625,11 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): if stride_nl.feasible(): model_des = list(sym for sym in stride_nl.iter_decisions()) x_var = None - y_var = None for i in range(len(model_des)): if type(model_des[i]) == IntegerVariable: x_var = model_des[i] elif type(model_des[i]) == BinaryVariable: y_var = model_des[i] - """print(f"x variables: {x_var.state(0)}") - if y_var is not None: - print(f"y variables: {y_var.state(0)}")""" solution["stocks"] = {s: int(x_var.state(0)[i]) for i, s in enumerate(self.stocks)} solution["return"], solution["risk"] = self.compute_risk_and_returns(solution["stocks"]) cost = sum( diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 7a5ddb3..85879e2 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -147,3 +147,20 @@ def test_dqm_integration(self): self.assertIn("purchase cost", output) self.assertIn("variance", output) self.assertNotIn("traceback", output) + + @unittest.skipIf(os.getenv("SKIP_INT_TESTS"), "Skipping integration test.") + def test_stride_integration(self): + """Test integration of portfolio script stride run.""" + + demo_file = os.path.join(project_dir, "portfolio.py") + + output = subprocess.check_output([sys.executable, demo_file] + ["-m", "Stride"]) + output = output.decode("utf-8") # Bytes to str + output = output.lower() + + self.assertIn("stride run", output) + self.assertIn("best feasible solution", output) + self.assertIn("estimated returns", output) + self.assertIn("purchase cost", output) + self.assertIn("variance", output) + self.assertNotIn("traceback", output) From ad27bf666fa4b22db9eaf6a256421faddc42ccf2 Mon Sep 17 00:00:00 2001 From: Mihidum Gunasekara Date: Wed, 17 Jun 2026 09:54:47 -0500 Subject: [PATCH 3/5] responding to changes --- README.md | 6 +- app.py | 13 ++-- data/larger_example.csv | 14 ----- demo_interface.py | 1 + portfolio.py | 6 +- src/demo_enums.py | 6 +- src/multi_period.py | 8 +-- src/single_period.py | 133 ++++++++++++++++++++-------------------- 8 files changed, 87 insertions(+), 100 deletions(-) delete mode 100644 data/larger_example.csv diff --git a/README.md b/README.md index c77ca80..8f3cb07 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Similar to the CQM runs the user can run the same formulations in using the late ```python portfolio.py -m 'Stride'``` -Same parameters as the CQM formulation can be used. The output of the default Stride run is printed on the console as follows. +The same parameters as the CQM formulation can be used. The output of the default Stride solver run is printed on the console as follows. ```bash Stride run... @@ -304,12 +304,12 @@ Each xi denotes the number of shares of stock i to be purchased. ### Constraints -All CQM and Stride formulations include the budget constraint described earlier. +All CQM and Stride solver formulations include the budget constraint described earlier. ##### Risk-Bounding Formulation This includes an upper bound on the risk as a quadratic constraint. -Such a constraint is supported natively by CQM and Stride. +Such a constraint is supported natively by the CQM and Stride solvers. ##### Return-Bounding Formulation This includes a lower bound on the returns as a linear inequality constraint. diff --git a/app.py b/app.py index a1a9753..8664a47 100644 --- a/app.py +++ b/app.py @@ -18,13 +18,6 @@ import dash import diskcache -from dash import DiskcacheManager - -from demo_configs import APP_TITLE, THEME_COLOR, THEME_COLOR_SECONDARY -from demo_interface import create_interface - -# Essential for initializing callbacks. Do not remove. -import demo_callbacks # Fix Dash long callbacks crashing on macOS 10.13+ (also potentially not working # on other POSIX systems), caused by https://bugs.python.org/issue33725 @@ -34,6 +27,12 @@ # the `multiprocessing` library, but its fork, `multiprocess` still hasn't caught up. # (see docs: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods) import multiprocess +from dash import DiskcacheManager + +# Essential for initializing callbacks. Do not remove. +import demo_callbacks +from demo_configs import APP_TITLE, THEME_COLOR, THEME_COLOR_SECONDARY +from demo_interface import create_interface if multiprocess.get_start_method(allow_none=True) is None: multiprocess.set_start_method("spawn") diff --git a/data/larger_example.csv b/data/larger_example.csv deleted file mode 100644 index e4b788e..0000000 --- a/data/larger_example.csv +++ /dev/null @@ -1,14 +0,0 @@ -Month,AAPL,MSFT,AAL,WMT,GOOGL,NFLX,EBAY,INTC,KMX,EA -10-Nov,9.541,19.953,10.521,41.735,167.98,435,177.24,232.61,244.56,89.025 -10-Dec,9.891,22.046,9.437,41.842,168.47,436.18,175,233.1,252.043,85.01 -11-Jan,10.405,21.904,9.352,43.502,166.68,434.38,174.48,233.32,257.99,84.91 -11-Feb,10.831,21.12,8.117,40.329,170.16,428.76,177.55,229.74,258.035,83.37 -11-Mar,10.687,20.174,8.211,40.667,173.242,421.17,172.78,229.98,264.51,84.28 -11-Apr,10.737,20.595,8.57,42.956,172.55,414.77,173.22,234.08,270,84.75 -11-May,10.666,20.003,8.579,43.429,172.32,409.24,173.69,233.885,256.01,85.83 -11-Jun,10.293,20.794,8.4,41.793,167.94,402.35,174.15,234.45,244.68,85.11 -11-Jul,11.974,21.914,5.883,41.455,173.73,406.42,176.7,236.18,217.125,88.92 -11-Aug,11.801,21.408,5.27,42.135,170.3,411.42,177.34,233.43,217.31,81.29 -11-Sep,11.693,20.032,5.185,41.113,175.94,416.02,181.38,231.6,218.9,85.95 -11-Oct,12.413,21.433,5.44,44.931,176.945,409.68,179.92,233.61,220.71,80.99 -11-Nov,11.72,20.743,4.45,46.658,180.155,403.32,177.92,228.7,221.59,86 diff --git a/demo_interface.py b/demo_interface.py index c226a6c..b702f56 100644 --- a/demo_interface.py +++ b/demo_interface.py @@ -13,6 +13,7 @@ # limitations under the License. """This file stores the HTML layout for the app.""" + from __future__ import annotations from datetime import date, timedelta diff --git a/portfolio.py b/portfolio.py index fa5b866..77bfa8f 100644 --- a/portfolio.py +++ b/portfolio.py @@ -148,7 +148,7 @@ def main( solver_type = SolverType.Stride else: solver_type = SolverType.DQM - #solver_type = SolverType.CQM if model_type == "CQM" else SolverType.DQM + # solver_type = SolverType.CQM if model_type == "CQM" else SolverType.DQM if (max_risk or min_return) and solver_type is SolverType.DQM: raise Exception("The bound options require a CQM or Stride.") @@ -160,7 +160,9 @@ def main( raise Exception("User must provide dates with option 'num'.") if t_cost and solver_type is SolverType.DQM: - raise Exception("The transaction cost option requires a CQM or Stride. Set t_cost=0 for DQM.") + raise Exception( + "The transaction cost option requires a CQM or Stride. Set t_cost=0 for DQM." + ) if rebalance: print(f"\nRebalancing portfolio optimization run...") diff --git a/src/demo_enums.py b/src/demo_enums.py index 5772127..9bd04eb 100644 --- a/src/demo_enums.py +++ b/src/demo_enums.py @@ -23,9 +23,9 @@ class SolverType(Enum): @property def label(self): return { - SolverType.CQM: "Quantum Hybrid (CQM)", - SolverType.DQM: "Quantum Hybrid (DQM)", - SolverType.Stride: "Quantum Hybrid (Stride)", + SolverType.CQM: "CQM Hybrid Solver", + SolverType.DQM: "DQM Hybrid Solver", + SolverType.Stride: "Stride Hybrid Solver", }[self] diff --git a/src/multi_period.py b/src/multi_period.py index c8371f8..9a1d042 100644 --- a/src/multi_period.py +++ b/src/multi_period.py @@ -13,16 +13,14 @@ # limitations under the License. from typing import Optional + +import matplotlib.pyplot as plt import numpy as np import pandas as pd +from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler from demo_configs import DATES_DEFAULT from src.demo_enums import SolverType - -import matplotlib.pyplot as plt - -from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler - from src.single_period import SinglePeriod diff --git a/src/single_period.py b/src/single_period.py index 08371fe..3ef5759 100644 --- a/src/single_period.py +++ b/src/single_period.py @@ -19,10 +19,10 @@ import numpy as np import pandas as pd from dimod import Binary, ConstrainedQuadraticModel, DiscreteQuadraticModel, Integer, quicksum -from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler from dwave.optimization import Model from dwave.optimization.mathematical import add, safe_divide -from dwave.optimization.symbols import IntegerVariable, BinaryVariable +from dwave.optimization.symbols import BinaryVariable, IntegerVariable +from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler from src.demo_enums import SolverType from src.utils import get_baseline_data, get_requested_stocks, get_stock_data @@ -471,58 +471,6 @@ def dqm_grid_search(self): print(f"DQM Grid Search Completed: alpha={self.alpha}, gamma={self.gamma}.-") - def compute_risk_and_returns(self, solution): - """Compute the risk and return values of solution.""" - variance = 0.0 - for s1, s2 in product(solution, solution): - variance += ( - solution[s1] - * self.price[s1] - * solution[s2] - * self.price[s2] - * self.covariance_matrix[s1][s2] - ) - - est_return = 0 - for stock in solution: - est_return += solution[stock] * self.price[stock] * self.avg_monthly_returns[stock] - - return round(est_return, 2), round(variance, 2) - - def print_results(self, solution: dict): - """Print results to the console given a solution dictionary.""" - is_cqm_run = self.model_type is SolverType.CQM - is_dqm_run = self.model_type is SolverType.DQM - is_stride_run = self.model_type is SolverType.Stride - - if self.verbose and is_cqm_run: - print( - f"Number of feasible solutions: {solution['number feasible']} out of {solution['number sampled']} sampled.", - f"\nBest energy: {solution['best energy']:.2f}", - f"Best energy (feasible): {solution['energy']:.2f}", - sep="\n", - ) - - if is_dqm_run: - print(f"\nSolution for alpha = {solution['alpha']} and gamma = {solution['gamma']}:") - - print( - f"\nBest feasible solution:", - "\n".join(f"{k}\t{v:>3}" for k, v in solution["stocks"].items()), - f"\nEstimated Returns: {solution['return']}", - sep="\n", - ) - - if is_cqm_run or is_stride_run: - print(f"Sales Revenue: {solution['sales']:.2f}") - - print(f"Purchase Cost: {solution['cost']:.2f}") - - if is_cqm_run or is_stride_run: - print(f"Transaction Cost: {solution['transaction cost']:.2f}") - - print(f"Variance: {solution['risk']}\n") - def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): """Build and solve a Stride formulation. This method allows the user a choice of 3 problem formulations: @@ -543,11 +491,11 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): avg_returns = np.array(list(self.avg_monthly_returns)) StockPrices = stride_nl.constant(stock_prices) BudgetUpper = stride_nl.constant(self.budget) - BudgetLower = stride_nl.constant(0.997*self.budget) + BudgetLower = stride_nl.constant(0.997 * self.budget) cov = self.covariance_matrix.loc[self.stocks, self.stocks].to_numpy() risk_coeff = cov * np.outer(stock_prices, stock_prices) RiskCoeff = stride_nl.constant(risk_coeff) - AvgReturns = stride_nl.constant(avg_returns*stock_prices) + AvgReturns = stride_nl.constant(avg_returns * stock_prices) NegOne = stride_nl.constant(-1) Alpha = stride_nl.constant(self.alpha) Zero = stride_nl.constant(0) @@ -557,7 +505,9 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): BudgetLowerWithTrans = stride_nl.constant(self.budget - 0.003 * self.init_budget) # Defining and adding variables to the Stride model - x = stride_nl.integer(len(self.stocks), lower_bound=0, upper_bound=list(self.max_num_shares)) + x = stride_nl.integer( + len(self.stocks), lower_bound=0, upper_bound=list(self.max_num_shares) + ) # Defining risk expression risk = [] @@ -574,7 +524,7 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): self.init_holdings = init_holdings if not self.t_cost: - stock_purchases = x*StockPrices + stock_purchases = x * StockPrices stride_nl.add_constraint(stock_purchases.sum() <= BudgetUpper) stride_nl.add_constraint(stock_purchases.sum() >= BudgetLower) else: @@ -589,8 +539,8 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): prev_cost = Two * TransCost * StockPrices[i] * x0[i] * y[i] lhs.append(curr_cost + curr_return - prev_cost - prev_return) # Indicator linking constraints - stride_nl.add_constraint(x[i] - x0[i]*y[i] >= Zero) - stride_nl.add_constraint(x[i] - x[i]*y[i] <= x0[i]) + stride_nl.add_constraint(x[i] - x0[i] * y[i] >= Zero) + stride_nl.add_constraint(x[i] - x[i] * y[i] <= x0[i]) stride_nl.add_constraint(add(*lhs) <= BudgetUpper) stride_nl.add_constraint(add(*lhs) >= BudgetLowerWithTrans) @@ -600,7 +550,7 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): MaxRisk = stride_nl.constant(max_risk) stride_nl.add_constraint(add(*risk) <= MaxRisk) # Objective: maximize return - stride_nl.minimize(NegOne*returns.sum()) + stride_nl.minimize(NegOne * returns.sum()) elif min_return: print("Minimize risk s.t. a lower bound of return") # Adding minimum returns constraint @@ -611,7 +561,7 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): else: print("Minimize mean-variance expression") # Objective: minimize mean-variance expression - stride_nl.minimize(Alpha*add(*risk) - returns.sum()) + stride_nl.minimize(Alpha * add(*risk) - returns.sum()) """ Alternative objective function - minimize the ratio of risk per unit return. This can be also done in Stride. obj_expr = safe_divide((Alpha * add(*risk)), returns.sum()) @@ -619,7 +569,9 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): """ self.model["Stride"] = stride_nl - self.sample_set["Stride"] = self.sampler["Stride"].sample(stride_nl, label="Example - Portfolio Optimization", time_limit=self.time_limit) + self.sample_set["Stride"] = self.sampler["Stride"].sample( + stride_nl, label="Example - Portfolio Optimization", time_limit=self.time_limit + ) stride_nl.lock() solution = {} if stride_nl.feasible(): @@ -628,8 +580,6 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): for i in range(len(model_des)): if type(model_des[i]) == IntegerVariable: x_var = model_des[i] - elif type(model_des[i]) == BinaryVariable: - y_var = model_des[i] solution["stocks"] = {s: int(x_var.state(0)[i]) for i, s in enumerate(self.stocks)} solution["return"], solution["risk"] = self.compute_risk_and_returns(solution["stocks"]) cost = sum( @@ -657,6 +607,55 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): return solution + def compute_risk_and_returns(self, solution): + """Compute the risk and return values of solution.""" + variance = 0.0 + for s1, s2 in product(solution, solution): + variance += ( + solution[s1] + * self.price[s1] + * solution[s2] + * self.price[s2] + * self.covariance_matrix[s1][s2] + ) + + est_return = 0 + for stock in solution: + est_return += solution[stock] * self.price[stock] * self.avg_monthly_returns[stock] + + return round(est_return, 2), round(variance, 2) + + def print_results(self, solution: dict): + """Print results to the console given a solution dictionary.""" + is_cqm_run = self.model_type is SolverType.CQM + is_dqm_run = self.model_type is SolverType.DQM + + if self.verbose and is_cqm_run: + print( + f"Number of feasible solutions: {solution['number feasible']} out of {solution['number sampled']} sampled.", + f"\nBest energy: {solution['best energy']:.2f}", + f"Best energy (feasible): {solution['energy']:.2f}", + sep="\n", + ) + + if is_dqm_run: + print(f"\nSolution for alpha = {solution['alpha']} and gamma = {solution['gamma']}:") + + print( + f"\nBest feasible solution:", + "\n".join(f"{k}\t{v:>3}" for k, v in solution["stocks"].items()), + f"\nEstimated Returns: {solution['return']}", + sep="\n", + ) + + print(f"Purchase Cost: {solution['cost']:.2f}") + + if not is_dqm_run: + print(f"Sales Revenue: {solution['sales']:.2f}") + print(f"Transaction Cost: {solution['transaction cost']:.2f}") + + print(f"Variance: {solution['risk']}\n") + def run( self, min_return: float = 0, max_risk: float = 0, num: int = 0, init_holdings: float = None ): @@ -679,7 +678,9 @@ def run( ) elif self.model_type is SolverType.Stride: print(f"\nStride run...") - solution = self.solve_stride(min_return=min_return, max_risk=max_risk, init_holdings=init_holdings) + solution = self.solve_stride( + min_return=min_return, max_risk=max_risk, init_holdings=init_holdings + ) else: print(f"\nDQM run...") if len(self.alpha_list) > 1 or len(self.gamma_list) > 1: From 8841bc6e687b009566d3ebffad288a2ee594d971 Mon Sep 17 00:00:00 2001 From: Mihidum Gunasekara Date: Thu, 18 Jun 2026 10:29:35 -0500 Subject: [PATCH 4/5] Responding to PR comments --- .gitignore | 1 - README.md | 2 +- portfolio.py | 1 - src/single_period.py | 166 ++++++++++++++++++++++------------------ tests/test_portfolio.py | 22 ++++++ 5 files changed, 115 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index c6abeac..6ef82b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ __pycache__ cache assets/__generated_theme.css portfolio.png -local_testing .idea/ diff --git a/README.md b/README.md index 8f3cb07..72cf097 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ A default CQM run with a transaction cost factor of 1% can be done with the foll ##### Stride Runs -Similar to the CQM runs the user can run the same formulations in using the latest Stride non-linear solver. +Similar to the CQM runs, the user can run the same formulations in using the Stride™ hybrid solver. ```python portfolio.py -m 'Stride'``` diff --git a/portfolio.py b/portfolio.py index 77bfa8f..62ff355 100644 --- a/portfolio.py +++ b/portfolio.py @@ -148,7 +148,6 @@ def main( solver_type = SolverType.Stride else: solver_type = SolverType.DQM - # solver_type = SolverType.CQM if model_type == "CQM" else SolverType.DQM if (max_risk or min_return) and solver_type is SolverType.DQM: raise Exception("The bound options require a CQM or Stride.") diff --git a/src/single_period.py b/src/single_period.py index 3ef5759..bb52962 100644 --- a/src/single_period.py +++ b/src/single_period.py @@ -471,8 +471,8 @@ def dqm_grid_search(self): print(f"DQM Grid Search Completed: alpha={self.alpha}, gamma={self.gamma}.-") - def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): - """Build and solve a Stride formulation. + def build_stride(self, max_risk=None, min_return=None, init_holdings=None): + """Build a Stride formulation. This method allows the user a choice of 3 problem formulations: 1) max return - alpha*risk (default formulation) 2) max return s.t. risk <= max_risk @@ -484,38 +484,40 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): init_holdings (float): Initial holdings, or initial portfolio state. """ # Instantiating the Stride object - stride_nl = Model() + model = Model() # Required constants stock_prices = np.array(list(self.price)) avg_returns = np.array(list(self.avg_monthly_returns)) - StockPrices = stride_nl.constant(stock_prices) - BudgetUpper = stride_nl.constant(self.budget) - BudgetLower = stride_nl.constant(0.997 * self.budget) + const_stock_prices = model.constant(stock_prices) + const_budget_upper = model.constant(self.budget) + const_budget_lower = model.constant(0.997 * self.budget) cov = self.covariance_matrix.loc[self.stocks, self.stocks].to_numpy() risk_coeff = cov * np.outer(stock_prices, stock_prices) - RiskCoeff = stride_nl.constant(risk_coeff) - AvgReturns = stride_nl.constant(avg_returns * stock_prices) - NegOne = stride_nl.constant(-1) - Alpha = stride_nl.constant(self.alpha) - Zero = stride_nl.constant(0) - TransCost = stride_nl.constant(self.t_cost) - One = stride_nl.constant(1) - Two = stride_nl.constant(2) - BudgetLowerWithTrans = stride_nl.constant(self.budget - 0.003 * self.init_budget) + const_risk_coeff = model.constant(risk_coeff) + const_avg_returns = model.constant(avg_returns * stock_prices) + const_neg_one = model.constant(-1) + const_alpha = model.constant(self.alpha) + const_zero = model.constant(0) + const_trans_cost = model.constant(self.t_cost) + const_one = model.constant(1) + const_two = model.constant(2) + const_budget_lower_with_trans = model.constant(self.budget - 0.003 * self.init_budget) + + num_stocks = len(self.stocks) # Defining and adding variables to the Stride model - x = stride_nl.integer( - len(self.stocks), lower_bound=0, upper_bound=list(self.max_num_shares) + x = model.integer( + num_stocks, lower_bound=0, upper_bound=list(self.max_num_shares) ) # Defining risk expression risk = [] - for (i1, s1), (i2, s2) in product(enumerate(self.stocks), enumerate(self.stocks)): - risk.append(RiskCoeff[i1, i2] * x[i1] * x[i2]) + for i1, i2 in product(range(num_stocks), range(num_stocks)): + risk.append(const_risk_coeff[i1, i2] * x[i1] * x[i2]) # Defining the returns expression - returns = AvgReturns * x + returns = const_avg_returns * x # Adding budget and related constraints if not init_holdings: @@ -524,87 +526,103 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): self.init_holdings = init_holdings if not self.t_cost: - stock_purchases = x * StockPrices - stride_nl.add_constraint(stock_purchases.sum() <= BudgetUpper) - stride_nl.add_constraint(stock_purchases.sum() >= BudgetLower) + stock_purchases = x * const_stock_prices + model.add_constraint(stock_purchases.sum() <= const_budget_upper) + model.add_constraint(stock_purchases.sum() >= const_budget_lower) else: # Modeling transaction cost - y = stride_nl.binary(len(self.stocks)) # dummy binary variable - x0 = stride_nl.constant(np.array(list(init_holdings.values()))) + y = model.binary(num_stocks) # dummy binary variable + x0 = model.constant(np.array(list(init_holdings.values()))) lhs = [] - for i, s in enumerate(self.stocks): - curr_return = StockPrices[i] * (One - TransCost) * x[i] - prev_return = StockPrices[i] * (One - TransCost) * x0[i] - curr_cost = Two * TransCost * StockPrices[i] * x[i] * y[i] - prev_cost = Two * TransCost * StockPrices[i] * x0[i] * y[i] + for i in range(num_stocks): + curr_return = const_stock_prices[i] * (const_one - const_trans_cost) * x[i] + prev_return = const_stock_prices[i] * (const_one - const_trans_cost) * x0[i] + curr_cost = const_two * const_trans_cost * const_stock_prices[i] * x[i] * y[i] + prev_cost = const_two * const_trans_cost * const_stock_prices[i] * x0[i] * y[i] lhs.append(curr_cost + curr_return - prev_cost - prev_return) # Indicator linking constraints - stride_nl.add_constraint(x[i] - x0[i] * y[i] >= Zero) - stride_nl.add_constraint(x[i] - x[i] * y[i] <= x0[i]) - stride_nl.add_constraint(add(*lhs) <= BudgetUpper) - stride_nl.add_constraint(add(*lhs) >= BudgetLowerWithTrans) + model.add_constraint(x[i] - x0[i] * y[i] >= const_zero) + model.add_constraint(x[i] - x[i] * y[i] <= x0[i]) + model.add_constraint(add(*lhs) <= const_budget_upper) + model.add_constraint(add(*lhs) >= const_budget_lower_with_trans) if max_risk: print("Maximize returns s.t. an upper bound of risk") # Adding maximum risk constraint - MaxRisk = stride_nl.constant(max_risk) - stride_nl.add_constraint(add(*risk) <= MaxRisk) + const_max_risk = model.constant(max_risk) + model.add_constraint(add(*risk) <= const_max_risk) # Objective: maximize return - stride_nl.minimize(NegOne * returns.sum()) + model.minimize(const_neg_one * returns.sum()) elif min_return: print("Minimize risk s.t. a lower bound of return") # Adding minimum returns constraint - MinReturn = stride_nl.constant(min_return) - stride_nl.add_constraint(returns.sum() >= MinReturn) + const_min_return = model.constant(min_return) + model.add_constraint(returns.sum() >= const_min_return) # Objective: minimize risk - stride_nl.minimize(add(*risk)) + model.minimize(add(*risk)) else: print("Minimize mean-variance expression") # Objective: minimize mean-variance expression - stride_nl.minimize(Alpha * add(*risk) - returns.sum()) + model.minimize(const_alpha * add(*risk) - returns.sum()) """ Alternative objective function - minimize the ratio of risk per unit return. This can be also done in Stride. obj_expr = safe_divide((Alpha * add(*risk)), returns.sum()) - stride_nl.minimize(obj_expr) + model.minimize(obj_expr) """ + self.model["Stride"] = model + + def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): + """Solve the Stride formulation. + This method allows the user a choice of 3 problem formulations: + 1) max return - alpha*risk (default formulation) + 2) max return s.t. risk <= max_risk + 3) min risk s.t. return >= min_return + + Args: + max_risk (int): Maximum risk for the risk bounding formulation. + min_return (int): Minimum return for the return bounding formulation. + init_holdings (float): Initial holdings, or initial portfolio state. + """ + + self.build_stride(max_risk, min_return, init_holdings) - self.model["Stride"] = stride_nl self.sample_set["Stride"] = self.sampler["Stride"].sample( - stride_nl, label="Example - Portfolio Optimization", time_limit=self.time_limit + self.model["Stride"], label="Example - Portfolio Optimization", time_limit=self.time_limit ) - stride_nl.lock() + self.model["Stride"].lock() solution = {} - if stride_nl.feasible(): - model_des = list(sym for sym in stride_nl.iter_decisions()) - x_var = None - for i in range(len(model_des)): - if type(model_des[i]) == IntegerVariable: - x_var = model_des[i] - solution["stocks"] = {s: int(x_var.state(0)[i]) for i, s in enumerate(self.stocks)} - solution["return"], solution["risk"] = self.compute_risk_and_returns(solution["stocks"]) - cost = sum( - [ - self.price[s] * max(0, solution["stocks"][s] - self.init_holdings[s]) - for s in self.stocks - ] - ) - sales = sum( - [ - self.price[s] * max(0, self.init_holdings[s] - solution["stocks"][s]) - for s in self.stocks - ] - ) - transaction = self.t_cost * (cost + sales) - solution.update( - { - "sales": sales, - "cost": cost, - "transaction cost": transaction, - } - ) - else: + + if not self.model["Stride"].feasible(): raise Exception("No feasible solution could be found for this problem instance.") + model_des = list(sym for sym in self.model["Stride"].iter_decisions()) + x_var = None + for i in range(len(model_des)): + if type(model_des[i]) == IntegerVariable: + x_var = model_des[i] + solution["stocks"] = {s: int(x_var.state(0)[i]) for i, s in enumerate(self.stocks)} + solution["return"], solution["risk"] = self.compute_risk_and_returns(solution["stocks"]) + cost = sum( + [ + self.price[s] * max(0, solution["stocks"][s] - self.init_holdings[s]) + for s in self.stocks + ] + ) + sales = sum( + [ + self.price[s] * max(0, self.init_holdings[s] - solution["stocks"][s]) + for s in self.stocks + ] + ) + transaction = self.t_cost * (cost + sales) + solution.update( + { + "sales": sales, + "cost": cost, + "transaction cost": transaction, + } + ) + return solution def compute_risk_and_returns(self, solution): diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 85879e2..1dc2991 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -111,6 +111,28 @@ def test_build_cqm_no_transaction(self): self.assertEqual(len(test_portfolio.model["CQM"].variables), 4) self.assertEqual(len(test_portfolio.model["CQM"].constraints), 4) + def test_build_stride(self): + test_portfolio = SinglePeriod(model_type="Stride", stocks=DEFAULT_STOCKS) + + data = { + "IBM": [93.043, 84.585, 111.453, 99.525, 95.819], + "WMT": [51.826, 52.823, 56.477, 49.805, 50.287], + } + + idx = ["Nov-00", "Dec-00", "Jan-01", "Feb-01", "Mar-01"] + + df = pd.DataFrame(data, index=idx) + + test_portfolio.load_data(df=df) + test_portfolio.build_stride() + test_portfolio.model["Stride"].lock() + + num_variables = test_portfolio.model["Stride"].num_decisions() + num_constraints = test_portfolio.model["Stride"].num_constraints() + + self.assertEqual(num_variables, 2) + self.assertEqual(num_constraints, 6) + class TestIntegration(unittest.TestCase): @unittest.skipIf(os.getenv("SKIP_INT_TESTS"), "Skipping integration test.") From 37b398fe59a8cf5be74f238335009a407edb4cfc Mon Sep 17 00:00:00 2001 From: Mihidum Gunasekara Date: Wed, 24 Jun 2026 12:49:07 -0500 Subject: [PATCH 5/5] responding to PR comments --- .gitignore | 1 - README.md | 2 +- app.py | 13 +++++++------ demo_interface.py | 1 - portfolio.py | 2 +- src/multi_period.py | 8 +++++--- src/single_period.py | 27 +++++++++++++++++++-------- 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 6ef82b0..822fbca 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ __pycache__ cache assets/__generated_theme.css portfolio.png -.idea/ diff --git a/README.md b/README.md index 72cf097..ce19bd9 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ A default CQM run with a transaction cost factor of 1% can be done with the foll ##### Stride Runs -Similar to the CQM runs, the user can run the same formulations in using the Stride™ hybrid solver. +Similar to the CQM runs, the user can run the same problem using the Stride™ hybrid solver. ```python portfolio.py -m 'Stride'``` diff --git a/app.py b/app.py index 8664a47..a1a9753 100644 --- a/app.py +++ b/app.py @@ -18,6 +18,13 @@ import dash import diskcache +from dash import DiskcacheManager + +from demo_configs import APP_TITLE, THEME_COLOR, THEME_COLOR_SECONDARY +from demo_interface import create_interface + +# Essential for initializing callbacks. Do not remove. +import demo_callbacks # Fix Dash long callbacks crashing on macOS 10.13+ (also potentially not working # on other POSIX systems), caused by https://bugs.python.org/issue33725 @@ -27,12 +34,6 @@ # the `multiprocessing` library, but its fork, `multiprocess` still hasn't caught up. # (see docs: https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods) import multiprocess -from dash import DiskcacheManager - -# Essential for initializing callbacks. Do not remove. -import demo_callbacks -from demo_configs import APP_TITLE, THEME_COLOR, THEME_COLOR_SECONDARY -from demo_interface import create_interface if multiprocess.get_start_method(allow_none=True) is None: multiprocess.set_start_method("spawn") diff --git a/demo_interface.py b/demo_interface.py index b702f56..c226a6c 100644 --- a/demo_interface.py +++ b/demo_interface.py @@ -13,7 +13,6 @@ # limitations under the License. """This file stores the HTML layout for the app.""" - from __future__ import annotations from datetime import date, timedelta diff --git a/portfolio.py b/portfolio.py index 62ff355..1a007a1 100644 --- a/portfolio.py +++ b/portfolio.py @@ -152,7 +152,7 @@ def main( if (max_risk or min_return) and solver_type is SolverType.DQM: raise Exception("The bound options require a CQM or Stride.") - if (gamma or bin_size) and solver_type is (SolverType.CQM or SolverType.Stride): + if (gamma or bin_size) and solver_type in (SolverType.CQM, SolverType.Stride): raise Exception("The option gamma or bin-size requires a DQM.") if num and not dates: diff --git a/src/multi_period.py b/src/multi_period.py index 9a1d042..c8371f8 100644 --- a/src/multi_period.py +++ b/src/multi_period.py @@ -13,14 +13,16 @@ # limitations under the License. from typing import Optional - -import matplotlib.pyplot as plt import numpy as np import pandas as pd -from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler from demo_configs import DATES_DEFAULT from src.demo_enums import SolverType + +import matplotlib.pyplot as plt + +from dwave.system import LeapHybridCQMSampler, LeapHybridDQMSampler, LeapHybridNLSampler + from src.single_period import SinglePeriod diff --git a/src/single_period.py b/src/single_period.py index bb52962..c1d2aab 100644 --- a/src/single_period.py +++ b/src/single_period.py @@ -471,8 +471,9 @@ def dqm_grid_search(self): print(f"DQM Grid Search Completed: alpha={self.alpha}, gamma={self.gamma}.-") - def build_stride(self, max_risk=None, min_return=None, init_holdings=None): + def build_stride(self, max_risk=None, min_return=None, init_holdings=None) -> None: """Build a Stride formulation. + This method allows the user a choice of 3 problem formulations: 1) max return - alpha*risk (default formulation) 2) max return s.t. risk <= max_risk @@ -487,8 +488,8 @@ def build_stride(self, max_risk=None, min_return=None, init_holdings=None): model = Model() # Required constants - stock_prices = np.array(list(self.price)) - avg_returns = np.array(list(self.avg_monthly_returns)) + stock_prices = np.asarray(self.price) + avg_returns = np.asarray(self.avg_monthly_returns) const_stock_prices = model.constant(stock_prices) const_budget_upper = model.constant(self.budget) const_budget_lower = model.constant(0.997 * self.budget) @@ -571,8 +572,9 @@ def build_stride(self, max_risk=None, min_return=None, init_holdings=None): """ self.model["Stride"] = model - def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): + def solve_stride(self, max_risk=None, min_return=None, init_holdings=None) -> dict: """Solve the Stride formulation. + This method allows the user a choice of 3 problem formulations: 1) max return - alpha*risk (default formulation) 2) max return s.t. risk <= max_risk @@ -582,6 +584,13 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): max_risk (int): Maximum risk for the risk bounding formulation. min_return (int): Minimum return for the return bounding formulation. init_holdings (float): Initial holdings, or initial portfolio state. + + Returns: + A dictionary containing the portfolio solution metrics: + + - sales: Portfolio sales value. + - cost: Portfolio cost value. + - transaction cost: Transaction cost incurred. """ self.build_stride(max_risk, min_return, init_holdings) @@ -596,10 +605,12 @@ def solve_stride(self, max_risk=None, min_return=None, init_holdings=None): raise Exception("No feasible solution could be found for this problem instance.") model_des = list(sym for sym in self.model["Stride"].iter_decisions()) - x_var = None - for i in range(len(model_des)): - if type(model_des[i]) == IntegerVariable: - x_var = model_des[i] + for i, var in enumerate(model_des): + if isinstance(var, IntegerVariable): + x_var = var + break + else: + x_var = None solution["stocks"] = {s: int(x_var.state(0)[i]) for i, s in enumerate(self.stocks)} solution["return"], solution["risk"] = self.compute_risk_and_returns(solution["stocks"]) cost = sum(