quantum-hybrid-portfolio / tests /test_methods.py
rocRevyAreGoals15's picture
Initial HF Space deployment
9e89154
Raw
History Blame Contribute Delete
5.43 kB
"""Tests for the methods package — portfolio optimization methods from research notebooks."""
import numpy as np
import pytest
from methods import (
equal_weight,
hrp_weights,
hybrid_pipeline_weights,
markowitz_max_sharpe,
min_variance,
qubo_sa_weights,
target_return_frontier,
vqe_weights,
)
from methods.hybrid_pipeline import HybridPipelineInfo
def _sample_data():
"""15-asset dataset matching notebook 05 (seeded for reproducibility)."""
np.random.seed(2025)
n = 15
mu = np.array(
[
0.18, 0.20, 0.15, 0.22, 0.17, 0.35,
0.12, 0.13, 0.16, 0.18,
0.08, 0.14, 0.10,
0.10, 0.09,
]
)
sigma_vec = np.array(
[
0.28, 0.25, 0.24, 0.30, 0.35, 0.42,
0.20, 0.22, 0.22, 0.24,
0.16, 0.18, 0.20,
0.22, 0.24,
]
)
corr = np.full((n, n), 0.30)
np.fill_diagonal(corr, 1.0)
for start, size in [(0, 6), (6, 4), (10, 3), (13, 2)]:
for i in range(start, start + size):
for j in range(start, start + size):
if i != j:
corr[i, j] = 0.68
noise = np.random.uniform(-0.04, 0.04, (n, n))
noise = (noise + noise.T) / 2
np.fill_diagonal(noise, 0)
corr = np.clip(corr + noise, -0.95, 0.95)
np.fill_diagonal(corr, 1.0)
Sigma = np.outer(sigma_vec, sigma_vec) * corr
eig = np.linalg.eigvalsh(Sigma)
if np.any(eig < 0):
Sigma += (-eig.min() + 1e-6) * np.eye(n)
return mu, Sigma
class TestWeightsSumToOne:
"""All methods must return weights summing to ~1."""
@pytest.fixture
def data(self):
return _sample_data()
def test_equal_weight(self, data):
mu, Sigma = data
w = equal_weight(mu, Sigma)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
def test_markowitz_max_sharpe(self, data):
mu, Sigma = data
w = markowitz_max_sharpe(mu, Sigma)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
def test_min_variance(self, data):
mu, Sigma = data
w = min_variance(mu, Sigma)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
def test_hrp_weights(self, data):
mu, Sigma = data
w = hrp_weights(mu, Sigma)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
def test_qubo_sa_weights(self, data):
mu, Sigma = data
w = qubo_sa_weights(mu, Sigma, K=6)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
assert np.sum(w > 0) == 6
def test_vqe_weights(self, data):
mu, Sigma = data
w = vqe_weights(mu, Sigma, n_restarts=3)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
def test_hybrid_pipeline_weights(self, data):
mu, Sigma = data
w, info = hybrid_pipeline_weights(mu, Sigma, K_screen=10, K_select=5)
assert np.isclose(w.sum(), 1.0)
assert len(w) == len(mu)
assert np.sum(w > 0) == 5
class TestTargetReturnFrontier:
"""target_return_frontier returns correct structure."""
def test_frontier_structure(self):
mu, Sigma = _sample_data()
frontier = target_return_frontier(mu, Sigma, n_points=5)
assert isinstance(frontier, list)
assert len(frontier) >= 1
for pt in frontier:
assert "target_return" in pt
assert "volatility" in pt
assert "sharpe" in pt
assert "weights" in pt
assert len(pt["weights"]) == len(mu)
assert np.isclose(sum(pt["weights"]), 1.0)
class TestHybridPipelineInfo:
"""hybrid_pipeline_weights returns (weights, info) with correct shapes."""
def test_info_keys(self):
mu, Sigma = _sample_data()
w, info = hybrid_pipeline_weights(mu, Sigma, K_screen=10, K_select=5)
assert len(info.stage1_screened_idx) == 10
assert info.stage1_ic is not None
assert len(info.stage2_selected_idx) == 5
assert hasattr(info, "stage2_qubo_obj")
assert hasattr(info, "stage3_sharpe")
def test_info_indices_valid(self):
mu, Sigma = _sample_data()
w, info = hybrid_pipeline_weights(mu, Sigma, K_screen=8, K_select=4)
assert all(isinstance(i, int) for i in info.stage1_screened_idx)
assert all(isinstance(i, int) for i in info.stage2_selected_idx)
assert all(0 <= i < len(mu) for i in info.stage1_screened_idx)
assert all(0 <= i < len(mu) for i in info.stage2_selected_idx)
class TestSmoke:
"""Basic smoke test: run each method on the 15-asset dataset."""
def test_all_methods_run(self):
mu, Sigma = _sample_data()
methods = [
lambda: equal_weight(mu, Sigma),
lambda: markowitz_max_sharpe(mu, Sigma),
lambda: min_variance(mu, Sigma),
lambda: hrp_weights(mu, Sigma),
lambda: qubo_sa_weights(mu, Sigma, K=6),
lambda: vqe_weights(mu, Sigma, n_restarts=2),
lambda: hybrid_pipeline_weights(mu, Sigma, K_screen=10, K_select=5)[0],
]
for fn in methods:
w = fn()
assert w is not None
assert isinstance(w, np.ndarray)
assert len(w) == 15
assert np.all(w >= -1e-6)
assert np.isclose(w.sum(), 1.0, rtol=1e-5)