import pytest import pandas as pd import numpy as np import copy import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from backtest import expanding_window_backtest from core_types import PortfolioState from solver import build_and_optimize def mock_expanding_window_backtest(returns_df, spy_rets, capital, rfr, cfg, model, allocation_engine, spread_map, initial_train_days=10, rebalance_freq=5, ff_df=None, yield_df=None): return expanding_window_backtest(returns_df, spy_rets, capital, rfr, cfg, model, allocation_engine, spread_map, initial_train_days, rebalance_freq, ff_df, yield_df) def test_delisting_terminal_returns(): """ Simulates a delisting event where a stock drops to 0.0, causing a -100% return and verifying the backtest correctly carries the total loss for that asset. """ dates = pd.date_range("2020-01-01", periods=30, freq="B") # Ticker A is stable. Ticker B drops to 0.0 at period 20. # We create raw prices to ensure compounding works exactly prices_A = np.linspace(100, 130, 30) prices_B = np.linspace(50, 60, 20).tolist() + [0.0] * 10 prices_df = pd.DataFrame({'A': prices_A, 'B': prices_B}, index=dates) # Calculate returns returns_df = prices_df.pct_change() # In core_engine.py we pad delisted tickers with 0.0 returns after the -1.0 # Let's manually do that here to simulate core_engine's behavior returns_df.loc[dates[20], 'B'] = -1.0 # The drop returns_df.loc[dates[21]:, 'B'] = 0.0 # The padding returns_df = returns_df.dropna() # Drop first day NaN spy_rets = pd.Series(0.001, index=returns_df.index) cfg = {"transaction_cost": 0.0, "trading_days_per_year": 252, "tax_enabled": False} spread_map = {"A": 0.0, "B": 0.0} # Run backtest # We use a dummy model that allocates 50/50 initially # Mocking build_and_optimize to return 50/50 class MockOptRes: weights = pd.Series({"A": 0.5, "B": 0.5, "CASH": 0.0}) import solver original_build = solver.build_and_optimize solver.build_and_optimize = lambda *args, **kwargs: MockOptRes() try: eq, spy_eq = mock_expanding_window_backtest( returns_df, spy_rets, capital=100_000.0, rfr=0.0, cfg=cfg, model=1, allocation_engine=1, spread_map=spread_map, initial_train_days=10, rebalance_freq=5 ) finally: solver.build_and_optimize = original_build assert eq is not None assert len(eq) > 0 # The portfolio had a 50% allocation to B at some point. # When B drops to 0.0 (return = -1.0), the portfolio should take a massive hit, but not go negative. # Ensure no NaNs or infs in the equity curve assert not eq.isna().any() assert not np.isinf(eq).any() # Portfolio shouldn't drop below 0 assert eq.min() >= 0.0