Spaces:
Running
Running
| 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 | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) |