portfolio-engine / tests /test_bl_multi_view.py
engineportf's picture
Initial Deployment from Local Engine
208fbf8 verified
Raw
History Blame Contribute Delete
5.43 kB
import sys
import os
# Bulletproof pathing: Force Python to look in both the current folder AND the parent folder
_this_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _this_dir)
sys.path.insert(0, os.path.dirname(_this_dir))
import pytest
import pandas as pd
import numpy as np
from bl_bridge import compute_bl_posterior
# ─────────────────────────────────────────────
# TEST FIXTURES & DATA SETUP
# ─────────────────────────────────────────────
@pytest.fixture
def bl_data():
"""Provides a clean, mathematical sandbox for Black-Litterman tests."""
tickers = ["AAPL", "TSLA"]
# Prior: Structural model (e.g., CAPM) expects 5% return for both
prior_rets = pd.Series([0.05, 0.05], index=tickers)
# Covariance: Simple diagonal matrix to isolate view effects
cov_mat = pd.DataFrame([[0.04, 0.00],
[0.00, 0.04]], index=tickers, columns=tickers)
return tickers, prior_rets, cov_mat
# ─────────────────────────────────────────────
# 1. BASELINE & EDGE CASE TESTS
# ─────────────────────────────────────────────
def test_bl_empty_views_returns_prior(bl_data):
"""If no views are provided, the posterior should exactly match the prior."""
tickers, prior_rets, cov_mat = bl_data
# Act: Pass an empty view_sets list
posterior = compute_bl_posterior(prior_rets, [], cov_mat, tau=0.05, silent=True)
# Assert
np.testing.assert_allclose(posterior.values, prior_rets.values, atol=1e-6)
# ─────────────────────────────────────────────
# 2. MULTI-VIEW PRECISION BLENDING TESTS
# ─────────────────────────────────────────────
def test_bl_equal_precision_averaging(bl_data):
"""
If two views have the exact same uncertainty, their effective view
should be the exact mathematical average of the two.
"""
tickers, prior_rets, cov_mat = bl_data
# ML expects 10%, BSTS expects 20%. Both are equally confident.
ml_views = pd.Series([0.10, 0.10], index=tickers)
ml_omega = pd.Series([0.02, 0.02], index=tickers)
bs_views = pd.Series([0.20, 0.20], index=tickers)
bs_omega = pd.Series([0.02, 0.02], index=tickers)
view_sets = [(ml_views, ml_omega), (bs_views, bs_omega)]
# Act: Compute multi-view posterior
multi_posterior = compute_bl_posterior(prior_rets, view_sets, cov_mat, tau=0.05, silent=True)
# Act: Compute single-view posterior using the theoretical combined math
combined_views = pd.Series([0.15, 0.15], index=tickers)
combined_omega = pd.Series([0.01, 0.01], index=tickers)
single_posterior = compute_bl_posterior(prior_rets, [(combined_views, combined_omega)], cov_mat, tau=0.05, silent=True)
# Assert: The multi-view engine must exactly match the theoretical combined math
np.testing.assert_allclose(multi_posterior.values, single_posterior.values, atol=1e-6)
def test_bl_infinite_uncertainty_rejection(bl_data):
"""
If one model outputs an infinite/massive uncertainty (e.g., BSTS failed),
the BL bridge should effectively ignore it and output the same posterior as if
only the confident model was provided.
"""
tickers, prior_rets, cov_mat = bl_data
# ML is confident (Omega = 0.01)
ml_views = pd.Series([0.15, 0.15], index=tickers)
ml_omega = pd.Series([0.01, 0.01], index=tickers)
# BSTS predicts a massive crash, but is purely noise (Omega = 1000.0)
bs_views = pd.Series([-0.50, -0.50], index=tickers)
bs_omega = pd.Series([1000.0, 1000.0], index=tickers)
# Act 1: Compute posterior with BOTH views
multi_posterior = compute_bl_posterior(
prior_rets,
[(ml_views, ml_omega), (bs_views, bs_omega)],
cov_mat, tau=0.05, silent=True
)
# Act 2: Compute posterior with ONLY the ML view
single_posterior = compute_bl_posterior(
prior_rets,
[(ml_views, ml_omega)],
cov_mat, tau=0.05, silent=True
)
# Assert
np.testing.assert_allclose(multi_posterior.values, single_posterior.values, atol=1e-4)
def test_bl_conflicting_views_domination(bl_data):
"""
If Model 1 is slightly confident and Model 2 is hyper-confident,
the final posterior should be pulled heavily towards Model 2.
"""
tickers, prior_rets, cov_mat = bl_data
# ML expects 0%, slightly uncertain
ml_views = pd.Series([0.00, 0.00], index=tickers)
ml_omega = pd.Series([0.10, 0.10], index=tickers)
# BSTS expects 20%, hyper confident
bs_views = pd.Series([0.20, 0.20], index=tickers)
bs_omega = pd.Series([1e-5, 1e-5], index=tickers)
view_sets = [(ml_views, ml_omega), (bs_views, bs_omega)]
posterior = compute_bl_posterior(prior_rets, view_sets, cov_mat, tau=0.05, silent=True)
assert np.all(posterior.values > 0.10)
assert np.all(posterior.values < 0.15)