core.test_solver
Tests for Hedge Portfolio Optimization Solver
This module tests src/core/solver.py which provides the optimization engine for finding optimal hedge portfolio quantities that minimize the difference between hedge profits and impermanent loss across price scenarios.
MATHEMATICAL BACKGROUND
The hedge optimization problem seeks to find option quantities that offset impermanent loss (IL) across different price scenarios. This is a constrained nonlinear optimization problem.
OPTIMIZATION OBJECTIVE
The solver minimizes the sum of absolute differences between hedge portfolio profit and impermanent loss magnitude:
minimize: Σᵢ |portfolio_profit[i] - |IL[i]||
Expanded: minimize: Σᵢ |Σⱼ (qⱼ × profit_matrix[j,i]) - |IL[i]||
Where: i = scenario index (price variations) j = strike index qⱼ = quantity of options at strike j profit_matrix[j,i] = put option profit at strike j, scenario i IL[i] = impermanent loss at scenario i (always ≤ 0)
OBJECTIVE FUNCTION RATIONALE
Why L1 norm (sum of absolute differences)?
- Robustness: Less sensitive to outliers than L2 (squared differences)
- Interpretability: Direct dollar interpretation of hedge shortfall
- Sparsity: Tends to produce sparser solutions (fewer active strikes)
Alternative objectives considered:
- Sum of squared differences: |portfolio_profit - |IL||² Pros: Differentiable, penalizes large mismatches more Cons: Over-penalizes tail scenarios, less interpretable
Worst-case minimization (minimax): max(|portfolio_profit[i] - |IL[i]||) Pros: Guarantees worst-case performance Cons: Ignores typical scenarios, harder to optimize
Expected value with probability weights: Σᵢ wᵢ|error[i]| Pros: Can incorporate market views Cons: Requires probability estimates
CONVERGENCE CRITERIA
The solver uses scipy.optimize.minimize with these convergence parameters:
| Parameter | Value | Description |
|---|---|---|
| ftol | 1e-9 | Function value tolerance |
| gtol | 1e-8 | Gradient tolerance |
| maxiter | 1000 | Maximum iterations (configurable) |
Convergence declared when:
- |f(x_k) - f(x_{k-1})| < ftol × max(1, |f(x_k)|), OR
- ||∇f(x_k)||∞ < gtol, OR
- Iterations exceed maxiter (may indicate non-convergence)
SOLVER METHOD COMPARISON
| Method | Algorithm | Bounds | Constraints | Speed | Robustness |
|---|---|---|---|---|---|
| SLSQP | SQP | ✓ | eq + ineq | Medium | Good |
| L-BFGS-B | Quasi-Newton | ✓ | ✗ | Fast | Moderate |
| trust-constr | Trust-region SQP | ✓ | eq + ineq | Slow | Excellent |
Default Choice: SLSQP
- Handles both equality and inequality constraints
- Good balance of speed and robustness
- Well-tested on financial optimization problems
EXCEL REPRODUCTION GUIDE
To verify calculations in Excel, follow these steps:
STEP 1: SET UP INPUT DATA
Create a spreadsheet with these named ranges:
Cell A1: "spot" Value: 3050
Cell A2: "p_min" Value: 2200
Cell A3: "p_max" Value: 3650
STEP 2: GENERATE STRIKES (Column B)
Starting at B5, list strikes from p_min to spot in 100 increments:
B5: 2200
B6: 2300
B7: 2400
...
B13: 3000
Or use formula: =SEQUENCE(ROUNDUP((A1-A2)/100,0)+1, 1, A2, 100)
STEP 3: GENERATE VARIATIONS (Row 4)
Starting at C4, list variations from -0.30 to +0.20 in 0.05 increments:
C4: -0.30
D4: -0.25
E4: -0.20
...
M4: +0.20
Or use formula: =SEQUENCE(1, 11, -0.3, 0.05)
STEP 4: BUILD PROFIT MATRIX (C5:M13)
Each cell calculates put option profit as % of spot.
Formula for cell C5 (and drag to fill matrix): =MAX($B5 - $A$1*(1+C$4), 0) / $A$1
This computes: max(strike - future_price, 0) / spot Where: future_price = spot × (1 + variation)
Example values at variation = -0.30 (column C): C5 (strike 2200): =MAX(2200 - 3050*0.70, 0) / 3050 = 0.0213 C6 (strike 2300): =MAX(2300 - 2135, 0) / 3050 = 0.0541 ...
STEP 5: INPUT IL VALUES (Row 14)
Below the profit matrix, enter IL values for each variation:
C14: -0.22 (IL at -30% variation)
D14: -0.18 (IL at -25% variation)
E14: -0.12 (IL at -20% variation)
...
M14: 0 (IL at +20% variation)
Note: IL values come from Uniswap IL calculations (see test_uniswap.py)
STEP 6: ADD QUANTITY CELLS (Column A)
Add decision variables (quantities) next to each strike:
A5: 0 (quantity for strike 2200)
A6: 0 (quantity for strike 2300)
...
A13: 0 (quantity for strike 3000)
STEP 7: CALCULATE PORTFOLIO PROFIT (Row 15)
For each variation, sum quantity × profit across all strikes:
C15: =SUMPRODUCT($A$5:$A$13, C5:C13)
This computes: Σⱼ (qⱼ × profit_matrix[j,i])
STEP 8: CALCULATE ABSOLUTE DIFFERENCES (Row 16)
Compute |portfolio_profit - |IL|| for each variation:
C16: =ABS(C15 - ABS(C14))
STEP 9: CALCULATE OBJECTIVE (Single Cell)
Sum all absolute differences:
O16: =SUM(C16:M16)
This is the value to minimize.
STEP 10: RUN EXCEL SOLVER
Go to Data → Solver (install Solver Add-in if needed)
Configure Solver:
- Set Objective: $O$16
- To: Min
- By Changing Variable Cells: $A$5:$A$13
- Subject to Constraints:
- $A$5:$A$13 >= -100 (lower bound)
- $A$5:$A$13 <= 100 (upper bound)
Select Solving Method: "GRG Nonlinear" (This is similar to scipy's L-BFGS-B)
Click "Solve"
EXPECTED EXCEL LAYOUT
A B C D E ... M
1 spot 3050
2 p_min 2200
3 p_max 3650
4 Strike -30% -25% -20% ... +20%
5 [q1] 2200 0.0213 0.0049 0.0000 ... 0.0000
6 [q2] 2300 0.0541 0.0213 0.0000 ... 0.0000
7 [q3] 2400 0.0869 0.0541 0.0213 ... 0.0000
...
13 [q9] 3000 0.2836 0.2508 0.2180 ... 0.0000
14 IL -0.22 -0.18 -0.12 ... 0.00
15 Profit [sum] [sum] [sum] ... [sum]
16 |Diff| [abs] [abs] [abs] ... [abs] → OBJECTIVE
VERIFICATION CHECKLIST
After running Excel Solver, verify:
- Objective value matches Python output (within tolerance ~0.01)
- Quantities have same sign and similar magnitude
- Portfolio profit at each scenario is close to |IL|
Common Differences:
- Excel GRG may find different local minimum than scipy SLSQP
- Floating point differences (~1e-6) are expected
- Different starting points may yield different solutions
ADVANCED: USING EXCEL FOR SENSITIVITY ANALYSIS
Add Data Table (What-If Analysis):
- Vary spot price and observe optimal quantities
Add Scenario Manager:
- Save multiple solutions for different market conditions
Create charts:
- Plot portfolio_profit vs |IL| across variations
- Visualize hedge effectiveness
Test Categories
Dynamic Strike Generation
- Tests for generate_dynamic_strikes() which creates strike prices
- Verifies Pmin inclusion, sorting, and strike step configuration
Dynamic Variation Generation
- Tests for generate_dynamic_variations() which creates price scenarios
- Verifies range extension (Pmin-10% to Pmax+10%) and 5% increments
Profit Calculations
- Tests for calc_strike_profit_percent() and calc_strike_distance_percent()
- Verifies ITM/OTM put payoff calculations
Profit Matrix
- Tests for calc_strike_profit_matrix() which builds the optimization input
- Verifies matrix dimensions and profit relationships
Optimizer
- Tests for optimize_hedge_quantities() with all three solver methods
- Verifies convergence, bounds, masks, and output structure
Solver Comparison
- Integration tests comparing L-BFGS-B, SLSQP, and trust-constr
- Verifies solvers reduce objective function vs initial values
Standard Test Parameters
SPOT = 3050 (Current ETH price in USDT)
P_MIN = 2200 (Lower price bound for LP position)
P_MAX = 3650 (Upper price bound for LP position)
Run with: uv run pytest tests/test_solver.py -v
Tests for generate_dynamic_strikes()
This function generates strike prices for the hedge portfolio based on:
- Current spot price
- Lower price bound (Pmin) from the LP position
- Strike step size (default 100)
- Number of additional strikes below Pmin
NUMERICAL EXAMPLE
Input: spot = 3050 p_min = 2200 strike_step = 100 num_additional = 3
Output: [2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, 2800, 2900, 3000]
The strikes include:
- Pmin (2200) always included
- Strikes from Pmin to spot in 100-point increments
- 3 additional strikes below Pmin (2100, 2000, 1900)
Pmin should always be included in strikes.
All strikes should be at or below spot price.
Strikes should be sorted ascending.
Custom strike step should be respected.
Should include strikes above Pmin.
Zero variation (current price) should always be included.
Pmin variation should be included.
Pmax variation should be included.
Variations should be sorted ascending.
Variations should extend 10% beyond Pmin/Pmax.
ITM put should have positive profit.
OTM put should have zero profit.
Deep ITM put should have large profit.
Strike distance should be percentage from spot.
Matrix should be [num_strikes × num_variations].
All profits should be non-negative (puts can't have negative payoff).
Higher strike should have more profit for same downside move.
Create a sample optimization problem.
SLSQP solver should converge.
L-BFGS-B solver should converge.
trust-constr solver should converge.
Output quantities should match number of strikes.
IL differences should be computed for all variations.
Fixed strikes should not change from initial value.
Include mask should filter which variations affect objective.
Quantity bounds should be respected.
Create a more realistic optimization problem.
All solvers should produce valid OptimizedHedge output.
Optimized quantities should have lower objective than zeros.