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)