CoolFace
Apppublic

sridattapradeep/India_Equity_Scanner

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
test_smc_engine.py131 linesDownload Raw Back to tests
1"""2Regression tests for smc_engine accuracy fixes (2026-06-10 audit):3  - BOS/CHoCH labels must be assigned in chronological (event-time) order,4    not swing-detection order.5  - Premium/Discount classification must match the equilibrium band the6    function itself computes (ICT convention: premium = upper half).7  - detect_fvg returns mitigated FVGs too (consumers filter on .active).8  - _compute_indicators "sma_200_20d" is the value exactly 20 bars back.9"""10import numpy as np11import pandas as pd12import pytest13 14from scanner import _compute_indicators15from smc_engine import (16    SwingPoint,17    detect_fvg,18    detect_market_structure,19    detect_mss,20    detect_premium_discount,21)22 23 24def _df(close: np.ndarray, start="2025-01-01") -> pd.DataFrame:25    idx = pd.date_range(start, periods=len(close), freq="B")26    return pd.DataFrame(27        {"open": close, "high": close + 1, "low": close - 1,28         "close": close, "volume": 1e6},29        index=idx,30    )31 32 33# ── Structure labelling in time order ─────────────────────────────────────────34 35def _out_of_order_scenario():36    """Swing high at bar 10 is crossed AFTER swing low at bar 20 is crossed.37    Chronologically: bear break first (BOS — bias was neutral), then bull38    break (CHoCH — it flips the bear trend)."""39    n = 12040    close = np.full(n, 100.0)41    close[30] = 85.0          # crosses below swing low (90) at bar 3042    close[31:50] = 95.043    close[50:] = 115.0        # crosses above swing high (110) at bar 5044    df = _df(close)45    sh = [SwingPoint(price=110.0, bar_index=10,46                     timestamp=df.index[10].to_pydatetime(), kind="high")]47    sl = [SwingPoint(price=90.0, bar_index=20,48                     timestamp=df.index[20].to_pydatetime(), kind="low")]49    return df, sh, sl50 51 52def test_structure_labels_follow_event_time_order():53    df, sh, sl = _out_of_order_scenario()54    events = detect_market_structure(df, sh, sl)55    assert [(e.bar_index, e.direction, e.event_type) for e in events] == [56        (30, "bear", "BOS"),    # first event ever — bias neutral → BOS57        (50, "bull", "CHoCH"),  # flips the bear trend → CHoCH58    ]59 60 61def test_mss_labels_follow_event_time_order():62    df, sh, sl = _out_of_order_scenario()63    events = detect_mss(df, sh, sl)64    # ICT framework: a break against (or from) a non-aligned bias = MSS.65    assert [(e.bar_index, e.direction, e.event_type) for e in events] == [66        (30, "bear", "MSS"),   # dir was 0 (> -1) → MSS67        (50, "bull", "MSS"),   # dir was -1 (< 1) → MSS68    ]69 70 71def test_structure_events_sorted_chronologically():72    df, sh, sl = _out_of_order_scenario()73    events = detect_market_structure(df, sh, sl)74    bars = [e.bar_index for e in events]75    assert bars == sorted(bars)76 77 78# ── Premium / Discount classification ─────────────────────────────────────────79 80@pytest.mark.parametrize("pct_of_range,expected", [81    (0.99, "PREMIUM"),82    (0.80, "PREMIUM"),       # old code mislabelled this EQUILIBRIUM83    (0.55, "PREMIUM"),84    (0.50, "EQUILIBRIUM"),85    (0.48, "EQUILIBRIUM"),86    (0.40, "DISCOUNT"),87    (0.05, "DISCOUNT"),88])89def test_pd_zone_classification(pct_of_range, expected):90    lo, hi = 100.0, 200.091    price = lo + pct_of_range * (hi - lo)92    df = _df(np.full(60, price))93    zone = detect_premium_discount(df, swing_high=hi, swing_low=lo)94    assert zone.current_zone == expected95    # band boundaries themselves are unchanged96    assert zone.equilibrium_top == pytest.approx(lo + 0.525 * (hi - lo))97    assert zone.equilibrium_bottom == pytest.approx(lo + 0.475 * (hi - lo))98 99 100# ── FVG: mitigated gaps are returned (consumers filter on .active) ───────────101 102def test_detect_fvg_returns_mitigated_gaps():103    n = 40104    close = np.full(n, 100.0)105    df = _df(close)106    # Build a clean bull FVG at bar 21: low[21] > high[19] and close[20] > high[19]107    df.iloc[19, df.columns.get_loc("high")] = 101.0108    df.iloc[20, df.columns.get_loc("close")] = 104.0109    df.iloc[21, df.columns.get_loc("low")] = 103.0110    df.iloc[21, df.columns.get_loc("close")] = 105.0111    # Mitigate it: close drops below the gap bottom (101) later112    df.iloc[30, df.columns.get_loc("close")] = 99.0113 114    fvgs = detect_fvg(df, lookback_bars=40)115    bull = [f for f in fvgs if f.fvg_type == "bull"]116    assert bull, "bull FVG should be detected"117    assert any(not f.active for f in bull), "mitigated FVG must be returned"118 119 120# ── sma_200_20d off-by-one ────────────────────────────────────────────────────121 122def test_sma_200_20d_is_exactly_20_bars_back():123    # Strictly increasing close → sma_200 strictly increasing once defined.124    close = pd.Series(np.arange(1.0, 301.0))   # 300 bars125    df = _df(close.to_numpy())126    ind = _compute_indicators(df)127    sma200 = df["close"].rolling(200).mean()128    assert ind["sma_200_20d"] == pytest.approx(float(sma200.iloc[-21]))129    # sanity: it differs from the 19-bars-back value the old code used130    assert ind["sma_200_20d"] != pytest.approx(float(sma200.iloc[-20]))131