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