portfolio-engine / tests /test_global.py
engineportf's picture
Initial Deployment from Local Engine
208fbf8 verified
Raw
History Blame Contribute Delete
4.62 kB
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"