Repository: dcapal/dcapal (language: rust)

Issue:
Add buy-sell optimizer



Discussion / hints:

Fix patch:
diff --git a/dcapal-frontend/src/components/allocationFlow/endStep/index.js b/dcapal-frontend/src/components/allocationFlow/endStep/index.js
index 49bb6fc0..7e831ac6 100644
--- a/dcapal-frontend/src/components/allocationFlow/endStep/index.js
+++ b/dcapal-frontend/src/components/allocationFlow/endStep/index.js
@@ -35,7 +35,7 @@ const buildCards = (assets, solution) => {
   return cards;
 };
 
-export const EndStep = ({ ...props }) => {
+export const EndStep = ({ useTaxEfficient }) => {
   const [solution, setSolution] = useState(null);
   const [isLoading, setIsLoading] = useState(true);
   const dispatch = useDispatch();
@@ -66,7 +66,7 @@ export const EndStep = ({ ...props }) => {
         {}
       );
 
-      const sol = await solver.makeAndSolve(budget, as);
+      const sol = await solver.makeAndSolve(budget, as, useTaxEfficient);
       await Thread.terminate(solver);
       return sol;
     };
diff --git a/dcapal-frontend/src/components/allocationFlow/index.js b/dcapal-frontend/src/components/allocationFlow/index.js
index e1e6aeb2..f45b8bc0 100644
--- a/dcapal-frontend/src/components/allocationFlow/index.js
+++ b/dcapal-frontend/src/components/allocationFlow/index.js
@@ -11,6 +11,8 @@ import { PortfolioStep } from "./portfolioStep";
 export const AllocationFlow = () => {
   const step = useSelector((state) => state.app.allocationFlowStep);
 
+  const [useTaxEfficient, setUseTaxEfficient] = useState(true);
+
   if (step <= Step.CCY) {
     return <CcyStep />;
   } else if (step === Step.IMPORT) {
@@ -18,8 +20,13 @@ export const AllocationFlow = () => {
   } else if (step === Step.PORTFOLIO) {
     return <PortfolioStep />;
   } else if (step === Step.INVEST) {
-    return <InvestStep />;
+    return (
+      <InvestStep
+        useTaxEfficient={useTaxEfficient}
+        setUseTaxEfficient={setUseTaxEfficient}
+      />
+    );
   } else if (step === Step.END) {
-    return <EndStep />;
+    return <EndStep useTaxEfficient={useTaxEfficient} />;
   }
 };
diff --git a/dcapal-frontend/src/components/allocationFlow/investStep.js b/dcapal-frontend/src/components/allocationFlow/investStep.js
index f394dc3f..52f4f8ff 100644
--- a/dcapal-frontend/src/components/allocationFlow/investStep.js
+++ b/dcapal-frontend/src/components/allocationFlow/investStep.js
@@ -5,9 +5,8 @@ import { setAllocationFlowStep, Step } from "../../app/appSlice";
 import { InputNumber, InputNumberType } from "../core/inputNumber";
 import { setBudget } from "./portfolioStep/portfolioSlice";
 
-export const InvestStep = ({ ...props }) => {
+export const InvestStep = ({ useTaxEfficient, setUseTaxEfficient }) => {
   const [cash, setCash] = useState(0);
-  const [useTaxEfficient, setUseTaxEfficient] = useState(true);
   const dispatch = useDispatch();
 
   const quoteCcy = useSelector((state) => state.pfolio.quoteCcy);
@@ -56,7 +55,6 @@ export const InvestStep = ({ ...props }) => {
               className="w-4 h-4 accent-neutral-500"
               checked={useTaxEfficient}
               onChange={onChangeTaxEfficient}
-              disabled={true}
             />
             <label htmlFor="#tax-efficient-checkbox" className="ml-2">
               Use <span className="font-medium">Tax Efficient</span> algorithm
diff --git a/dcapal-frontend/src/workers/solver.js b/dcapal-frontend/src/workers/solver.js
index d5ab8577..3efccf5b 100644
--- a/dcapal-frontend/src/workers/solver.js
+++ b/dcapal-frontend/src/workers/solver.js
@@ -2,7 +2,7 @@ import { import_wasm } from "../wasm/import_wasm";
 import { expose } from "threads";
 
 expose({
-  async makeAndSolve(budget, assets) {
+  async makeAndSolve(budget, assets, isBuyOnly) {
     const { Solver } = await import_wasm();
 
     if (!budget || budget < 0) return null;
@@ -13,6 +13,7 @@ expose({
       assets: {
         ...assets,
       },
+      is_buy_only: isBuyOnly,
     });
 
     return Solver.solve(handle);
diff --git a/dcapal-optimizer-wasm/src/lib.rs b/dcapal-optimizer-wasm/src/lib.rs
index 86b2ad48..bfacd5d3 100644
--- a/dcapal-optimizer-wasm/src/lib.rs
+++ b/dcapal-optimizer-wasm/src/lib.rs
@@ -88,6 +88,7 @@ pub struct JsSolution {
 pub struct JsProblemOptions {
     budget: f64,
     assets: HashMap<String, JsProblemAsset>,
+    is_buy_only: bool,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -134,6 +135,7 @@ impl TryFrom<JsProblemOptions> for ProblemOptions {
         Ok(ProblemOptions {
             budget: options.budget,
             assets,
+            is_buy_only: options.is_buy_only,
         })
     }
 }
diff --git a/dcapal-optimizer-wasm/src/optimize.rs b/dcapal-optimizer-wasm/src/optimize.rs
index e8b3c867..038302fd 100644
--- a/dcapal-optimizer-wasm/src/optimize.rs
+++ b/dcapal-optimizer-wasm/src/optimize.rs
@@ -6,6 +6,7 @@ use minilp::{ComparisonOp, OptimizationDirection, Variable};
 pub struct ProblemOptions {
     pub budget: f64,
     pub assets: HashMap<String, ProblemAsset>,
+    pub is_buy_only: bool,
 }
 
 #[derive(Debug, Clone)]
@@ -63,13 +64,17 @@ impl Problem {
             ComparisonOp::Eq,
             options.budget,
         );
-        //    a_i >= current_amount   -- No sell
-        for (aid, a_i) in &vars {
-            problem.add_constraint(
-                [(*a_i, 1.0)],
-                ComparisonOp::Ge,
-                options.assets.get(aid).unwrap().current_amount,
-            );
+
+        if options.is_buy_only {
+            // Subject to:
+            //    a_i >= current_amount   -- No sell
+            for (aid, a_i) in &vars {
+                problem.add_constraint(
+                    [(*a_i, 1.0)],
+                    ComparisonOp::Ge,
+                    options.assets.get(aid).unwrap().current_amount,
+                );
+            }
         }
 
         Problem { problem, vars }
@@ -80,9 +85,7 @@ impl Problem {
 mod tests {
     use super::*;
 
-    #[test]
-    fn it_solves_60_40_portfolio() -> anyhow::Result<()> {
-        // Given
+    fn build_60_40_portfolio_no_allocation(is_buy_only: bool) -> (Problem, Vec<String>) {
         let budget = 100.;
         let vwce = "VWCE".to_string();
         let aggh = "AGGH".to_string();
@@ -104,10 +107,23 @@ mod tests {
                 },
             ),
         ]);
-        let options = ProblemOptions { budget, assets };
+
+        let options = ProblemOptions {
+            budget,
+            assets,
+            is_buy_only,
+        };
+
+        (Problem::new(options), vec![vwce, aggh])
+    }
+
+    #[test]
+    fn it_solves_60_40_portfolio_buy_only() -> anyhow::Result<()> {
+        // Given
+        let (problem, assets) = build_60_40_portfolio_no_allocation(true);
+        let [vwce, aggh] = <[String; 2]>::try_from(assets).ok().unwrap();
 
         // When
-        let problem = Problem::new(options);
         let solution = problem.problem.solve()?;
 
         // Expect
@@ -117,8 +133,21 @@ mod tests {
     }
 
     #[test]
-    fn it_solves_60_40_unbalanced_portfolio() -> anyhow::Result<()> {
+    fn it_solves_60_40_portfolio_buy_and_sell() -> anyhow::Result<()> {
         // Given
+        let (problem, assets) = build_60_40_portfolio_no_allocation(false);
+        let [vwce, aggh] = <[String; 2]>::try_from(assets).ok().unwrap();
+
+        // When
+        let solution = problem.problem.solve()?;
+
+        // Expect
+        assert_eq!(solution[problem.vars[&vwce]], 60.);
+        assert_eq!(solution[problem.vars[&aggh]], 40.);
+        Ok(())
+    }
+
+    fn build_60_40_portfolio_unbalanced(is_buy_only: bool) -> (Problem, Vec<String>) {
         let budget = 100.;
         let vwce = "VWCE".to_string();
         let aggh = "AGGH".to_string();
@@ -140,10 +169,22 @@ mod tests {
                 },
             ),
         ]);
-        let options = ProblemOptions { budget, assets };
+        let options = ProblemOptions {
+            budget,
+            assets,
+            is_buy_only,
+        };
+
+        (Problem::new(options), vec![vwce, aggh])
+    }
+
+    #[test]
+    fn it_solves_60_40_unbalanced_portfolio_buy_only() -> anyhow::Result<()> {
+        // Given
+        let (problem, assets) = build_60_40_portfolio_unbalanced(true);
+        let [vwce, aggh] = <[String; 2]>::try_from(assets).ok().unwrap();
 
         // When
-        let problem = Problem::new(options);
         let solution = problem.problem.solve()?;
 
         // Expect
@@ -153,8 +194,21 @@ mod tests {
     }
 
     #[test]
-    fn it_solves_my_portfolio() -> anyhow::Result<()> {
+    fn it_solves_60_40_unbalanced_portfolio_buy_and_sell() -> anyhow::Result<()> {
         // Given
+        let (problem, assets) = build_60_40_portfolio_unbalanced(false);
+        let [vwce, aggh] = <[String; 2]>::try_from(assets).ok().unwrap();
+
+        // When
+        let solution = problem.problem.solve()?;
+
+        // Expect
+        assert_eq!(solution[problem.vars[&vwce]], 60.);
+        assert_eq!(solution[problem.vars[&aggh]], 40.);
+        Ok(())
+    }
+
+    fn build_three_assets_portfolio(is_buy_only: bool) -> (Problem, Vec<String>) {
         let budget = 7706.12;
         let vwce = "VWCE".to_string();
         let aggh = "AGGH".to_string();
@@ -185,10 +239,38 @@ mod tests {
                 },
             ),
         ]);
-        let options = ProblemOptions { budget, assets };
+        let options = ProblemOptions {
+            budget,
+            assets,
+            is_buy_only,
+        };
+
+        (Problem::new(options), vec![vwce, aggh, reit])
+    }
+
+    #[test]
+    fn it_solves_three_assets_portfolio_buy_only() -> anyhow::Result<()> {
+        // Given
+        let (problem, assets) = build_three_assets_portfolio(true);
+        let [vwce, aggh, reit] = <[String; 3]>::try_from(assets).ok().unwrap();
+
+        // When
+        let solution = problem.problem.solve()?;
+
+        // Expect
+        println!("{}: {}", &vwce, solution[problem.vars[&vwce]]);
+        println!("{}: {}", &aggh, solution[problem.vars[&aggh]]);
+        println!("{}: {}", &reit, solution[problem.vars[&reit]]);
+        Ok(())
+    }
+
+    #[test]
+    fn it_solves_three_assets_portfolio_buy_and_sell() -> anyhow::Result<()> {
+        // Given
+        let (problem, assets) = build_three_assets_portfolio(false);
+        let [vwce, aggh, reit] = <[String; 3]>::try_from(assets).ok().unwrap();
 
         // When
-        let problem = Problem::new(options);
         let solution = problem.problem.solve()?;
 
         // Expect


Test patch: