|
6 | 6 | the observer classifies them correctly. This is the integration gate: if any |
7 | 7 | scenario fails, the engine must not ship. |
8 | 8 |
|
| 9 | +Post-PR #345 convention: ADF runs on log-returns (not raw prices), matching |
| 10 | +the DFA transform. INVALID therefore encodes a *true* unit root in returns, |
| 11 | +not in levels — a non-trivial condition. |
| 12 | +
|
9 | 13 | Scenarios (deterministic, seeded): |
10 | 14 |
|
11 | | -* OU mean-reverting prices → stationary, CRITICAL (H < 0.45) |
12 | | -* Pure random walk (GBM no drift) → non-stationary, INVALID |
13 | | -* GBM with positive drift → non-stationary, INVALID |
14 | | -* White noise prices → stationary, TRANSITION (H ≈ 0.5) |
| 15 | +* OU mean-reverting prices → stationary returns, CRITICAL/TRANSITION |
| 16 | +* Pure random walk (GBM no drift) → stationary returns (i.i.d.), never LONG |
| 17 | +* GBM with positive drift → stationary returns (μ+σZ), never LONG |
| 18 | +* White noise prices → stationary returns, TRANSITION (H ≈ 0.5) |
15 | 19 | """ |
16 | 20 |
|
17 | 21 | from __future__ import annotations |
@@ -71,17 +75,36 @@ def test_ou_mean_reverting_is_critical() -> None: |
71 | 75 | assert out["regime"] in {Regime.CRITICAL.value, Regime.TRANSITION.value}, out |
72 | 76 |
|
73 | 77 |
|
74 | | -def test_random_walk_is_invalid_or_transition() -> None: |
| 78 | +def test_random_walk_returns_are_stationary_no_long() -> None: |
| 79 | + """Random walk: prices I(1), log-returns i.i.d. → ADF stationary post-RFC. |
| 80 | +
|
| 81 | + Hurst estimate for a pure RW is ≈ 0.5 ± finite-sample noise; regime |
| 82 | + therefore lands in {CRITICAL, TRANSITION, DRIFT}. The invariant that |
| 83 | + *must* hold (INV-DRO4): RW has no true mean-reversion edge, so signal |
| 84 | + must never be LONG — regardless of which specific non-INVALID regime |
| 85 | + the finite sample produces. |
| 86 | + """ |
75 | 87 | price = _random_walk(SEED, N_SAMPLES) |
76 | 88 | out = geosync_observe(price, window=WINDOW, step=STEP) |
77 | | - assert out["regime"] in {Regime.INVALID.value, Regime.TRANSITION.value}, out |
| 89 | + assert out["stationary"] is True, f"RW returns must be stationary post-RFC: {out}" |
| 90 | + assert out["regime"] != Regime.INVALID.value, f"RW should not be INVALID: {out}" |
| 91 | + assert out["signal"] != "LONG", f"INV-DRO4: RW must never emit LONG: {out}" |
| 92 | + |
78 | 93 |
|
| 94 | +def test_gbm_with_drift_returns_are_stationary_no_long() -> None: |
| 95 | + """GBM with drift: prices I(1), log-returns ~ N(μ, σ²) → ADF stationary. |
79 | 96 |
|
80 | | -def test_gbm_with_drift_is_non_stationary() -> None: |
| 97 | + Post-RFC (PR #345) the stationarity test targets returns. GBM returns |
| 98 | + have no unit root — they are i.i.d. Gaussian — so ``stationary=True``. |
| 99 | + Trend at the price level surfaces in the ARA trend path as |
| 100 | + DRIFT/DIVERGING, which blocks LONG via INV-DRO4. The true falsification |
| 101 | + invariant is the signal gate, not the stationarity classification. |
| 102 | + """ |
81 | 103 | price = _gbm_drift(SEED, N_SAMPLES, mu=0.002, sigma=0.01) |
82 | 104 | out = geosync_observe(price, window=WINDOW, step=STEP) |
83 | | - assert out["stationary"] is False, f"GBM with drift must fail ADF, got {out}" |
84 | | - assert out["regime"] == Regime.INVALID.value |
| 105 | + assert out["stationary"] is True, f"GBM returns must be stationary post-RFC: {out}" |
| 106 | + assert out["regime"] != Regime.INVALID.value, f"GBM post-RFC must not be INVALID: {out}" |
| 107 | + assert out["signal"] != "LONG", f"INV-DRO4: GBM drift must never emit LONG: {out}" |
85 | 108 |
|
86 | 109 |
|
87 | 110 | def test_white_noise_prices_are_stationary() -> None: |
|
0 commit comments