import copy 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 pandas as pd import numpy as np from analytics import backtest, build_macro from backtest import monte_carlo from config import DEFAULT_CONFIG from core_types import PortfolioState # ───────────────────────────────────────────── # 1. TIME-AGNOSTIC SCALING TESTS # ───────────────────────────────────────────── def test_trading_days_annualization_scaling(): """ Verifies that changing 'trading_days_per_year' correctly alters the mathematical annualization of returns and volatility in the backtester. """ # 100 days of synthetic returns at exactly 10 bps per day dates = pd.date_range("2023-01-01", periods=100, freq="D") returns_df = pd.DataFrame({'ASSET_A': np.repeat(0.001, 100)}, index=dates) spy_rets = pd.Series(np.repeat(0.001, 100), index=dates) weights = pd.Series({'ASSET_A': 1.0}) capital = 10000.0 rfr = 0.0 spread_map = {'ASSET_A': 0.0} state = PortfolioState.empty(['ASSET_A']) betas = pd.Series({'ASSET_A': 1.0}) # Run 1: US Equities (252 days) cfg_us = copy.deepcopy(DEFAULT_CONFIG) cfg_us['trading_days_per_year'] = 252 cfg_us['transaction_cost'] = 0.0 _, _, _, stats_us = backtest(returns_df, weights, capital, rfr, spy_rets, spread_map, cfg_us, state) # Run 2: Crypto (365 days) cfg_crypto = copy.deepcopy(DEFAULT_CONFIG) cfg_crypto['trading_days_per_year'] = 365 cfg_crypto['transaction_cost'] = 0.0 _, _, _, stats_crypto = backtest(returns_df, weights, capital, rfr, spy_rets, spread_map, cfg_crypto, state) # Assert: Because Crypto assumes a longer compounding year, its annualized return # extrapolated from the same 100-day sample MUST be mathematically higher. assert stats_crypto['ann_ret'] > stats_us['ann_ret'] def test_monte_carlo_path_horizon(): """ Verifies that the Monte Carlo engine dynamically extends its simulation paths based on the global trading days configuration. """ tickers = ["ASSET_A"] weights = pd.Series({'ASSET_A': 1.0}) exp_rets = pd.Series({'ASSET_A': 0.10}) cov_mat = pd.DataFrame([[0.04]], index=tickers, columns=tickers) # Run with Frankfurt calendar (256 days) for 2 years cfg_eu = copy.deepcopy(DEFAULT_CONFIG) cfg_eu['trading_days_per_year'] = 256 cfg_eu['monte_carlo_years'] = 2.0 _, stats = monte_carlo(weights, exp_rets, cov_mat, capital=10000.0, cfg=cfg_eu, seed=42) # Assert: 2 years * 256 days = 512 path points assert len(stats['dates']) == 512 # Verify the paths array matches the length assert len(stats[50]) == 512 # ───────────────────────────────────────────── # 2. DYNAMIC BENCHMARK TESTS # ───────────────────────────────────────────── def test_build_macro_dynamic_benchmarks(): """ Verifies that the macro-economic context builder correctly reads dynamic benchmarks (like European VIX variants) instead of hardcoding US indexes. """ cfg = copy.deepcopy(DEFAULT_CONFIG) # Switch to European benchmarks cfg['benchmarks'] = { "equity": "DAX", "volatility": "^V2TX", # Euro Stoxx 50 Volatility "risk_free": "^DE10Y" } # Mock data dictionaries matching the European tickers prices = {"^DE10Y": 2.50} raw = { "DAX": pd.Series(np.linspace(10000, 15000, 250)), # Trending up "^V2TX": pd.Series([25.0, 30.0]) } vix_raw = pd.Series([25.0, 35.0]) # Emulate a high volatility regime macro = build_macro( prices=prices, raw=raw, rfr=0.025, display_df=pd.DataFrame(), w_arr=np.array([]), vix_raw=vix_raw, cfg=cfg ) # Assert: It should successfully read the ^V2TX proxy and flag it as high volatility (> 20) assert macro['vix_val'] == 35.0 assert macro['vix_high'] is True # Assert: It should successfully read the DAX trend (which we mocked as a straight line up) assert macro['spy_trend'] == "BULL"