from __future__ import annotations import pandas as pd from web_core.constants import TREND_BEAR, TREND_BULL, TREND_NEUTRAL from web_core.strategy import classify_bars, detect_trends def make_df(rows: list[dict[str, float]]) -> pd.DataFrame: idx = pd.date_range("2025-01-01", periods=len(rows), freq="D") return pd.DataFrame(rows, index=idx) def test_classify_bars_real_bull_real_bear_and_fake() -> None: df = make_df( [ {"Open": 100, "High": 110, "Low": 95, "Close": 105, "Volume": 1000}, {"Open": 106, "High": 111, "Low": 102, "Close": 112, "Volume": 1000}, {"Open": 111, "High": 112, "Low": 100, "Close": 99, "Volume": 1000}, {"Open": 100, "High": 103, "Low": 97, "Close": 101, "Volume": 1000}, ] ) out = classify_bars( df, use_body_range=False, volume_filter_enabled=False, volume_sma_window=20, volume_multiplier=1.0, ) assert list(out["classification"]) == ["unclassified", "real_bull", "real_bear", "fake"] def test_classify_bars_with_body_range_ignores_wicks() -> None: df = make_df( [ {"Open": 100, "High": 130, "Low": 90, "Close": 105, "Volume": 1000}, {"Open": 106, "High": 109, "Low": 104, "Close": 108, "Volume": 1000}, ] ) wick_based = classify_bars( df, use_body_range=False, volume_filter_enabled=False, volume_sma_window=20, volume_multiplier=1.0, ) body_based = classify_bars( df, use_body_range=True, volume_filter_enabled=False, volume_sma_window=20, volume_multiplier=1.0, ) assert wick_based.iloc[1]["classification"] == "fake" assert body_based.iloc[1]["classification"] == "real_bull" def test_detect_trends_fake_bars_do_not_break_real_bar_sequence() -> None: df = make_df( [ {"Open": 100, "High": 110, "Low": 95, "Close": 105, "Volume": 1000}, {"Open": 106, "High": 112, "Low": 104, "Close": 113, "Volume": 1000}, {"Open": 113, "High": 115, "Low": 108, "Close": 112, "Volume": 1000}, {"Open": 112, "High": 120, "Low": 111, "Close": 121, "Volume": 1000}, ] ) classified = classify_bars( df, use_body_range=False, volume_filter_enabled=False, volume_sma_window=20, volume_multiplier=1.0, ) trended, events = detect_trends(classified) assert list(classified["classification"]) == ["unclassified", "real_bull", "fake", "real_bull"] assert trended.iloc[-1]["trend_state"] == TREND_BULL assert any(e.event == "Bullish trend started" for e in events) def test_detect_trends_reversal_requires_two_opposite_real_bars() -> None: df = make_df( [ {"Open": 100, "High": 110, "Low": 95, "Close": 105, "Volume": 1000}, {"Open": 106, "High": 112, "Low": 104, "Close": 113, "Volume": 1000}, {"Open": 113, "High": 120, "Low": 111, "Close": 121, "Volume": 1000}, {"Open": 120, "High": 121, "Low": 109, "Close": 108, "Volume": 1000}, {"Open": 108, "High": 109, "Low": 95, "Close": 94, "Volume": 1000}, ] ) classified = classify_bars( df, use_body_range=False, volume_filter_enabled=False, volume_sma_window=20, volume_multiplier=1.0, ) trended, events = detect_trends(classified) assert trended.iloc[2]["trend_state"] == TREND_BULL assert trended.iloc[3]["trend_state"] == TREND_BULL assert trended.iloc[4]["trend_state"] == TREND_BEAR assert any("Bearish reversal confirmed" in e.event for e in events) def test_detect_trends_remains_neutral_without_two_consecutive_real_bars() -> None: df = make_df( [ {"Open": 100, "High": 110, "Low": 95, "Close": 105, "Volume": 1000}, {"Open": 105, "High": 108, "Low": 102, "Close": 106, "Volume": 1000}, {"Open": 106, "High": 116, "Low": 100, "Close": 117, "Volume": 1000}, {"Open": 117, "High": 118, "Low": 111, "Close": 115, "Volume": 1000}, {"Open": 115, "High": 116, "Low": 104, "Close": 103, "Volume": 1000}, ] ) classified = classify_bars( df, use_body_range=False, volume_filter_enabled=False, volume_sma_window=20, volume_multiplier=1.0, ) trended, events = detect_trends(classified) assert trended.iloc[-1]["trend_state"] == TREND_NEUTRAL assert len(events) == 0