# -*- coding: utf-8 -*- """RESEARCH HARD-STOP TEST: no-lookahead enforcement. A failing test here is a HARD STOP. Both SAX and ARIMA forecasts must be invariant to any change in bars AFTER the forecast origin, and must not carry actual (future) outcome information. """ import pytest from src.forecasting.target import ForecastContext from src.sax import SaxConfig, SaxAnalogForecaster from src.arima import ArimaConfig, ArimaModel from tests.helpers import make_series, clone_with_future @pytest.fixture def context(): return ForecastContext(symbol="XAUUSD", timeframe="H1", horizon=12, atr_period=20) def test_sax_forecast_unchanged_by_future(context): s = make_series(n=700, seed=3) origin = 400 model = SaxAnalogForecaster(SaxConfig(window_length=24, word_length=8, alphabet_size=5, min_analogs=5)) rec_a = model.forecast(s, origin, context) s_future = clone_with_future(s, origin, mut=100000.0) rec_b = model.forecast(s_future, origin, context) assert rec_a.to_dict() == rec_b.to_dict(), ( "SAX forecast changed when a FUTURE bar changed -> lookahead leak" ) def test_forecast_carries_no_actual_outcome(context): s = make_series(700, seed=4) model = SaxAnalogForecaster(SaxConfig(window_length=5, min_analogs=5)) rec = model.forecast(s, 400, context) assert rec.outcome_boundary is None or rec.outcome_boundary >= rec.forecast_origin # forecast must be frozen: no actual (future) outcome may be attached here assert rec.actual_forward_return is None assert rec.actual_forward_return_ATR is None def test_arima_forecast_unchanged_by_future(context): s = make_series(300, seed=5) origin = 220 model = ArimaModel(ArimaConfig(p=1, d=0, q=0, fit_window=150)) s_future = clone_with_future(s, origin, mut=5000.0) a = model.forecast(s, origin, context) b = model.forecast(s_future, origin, context) assert a.normalized_expected_return == b.normalized_expected_return assert a.arima_state == b.arima_state