"""Shared pytest fixtures. Every test gets an isolated ``tmp_path`` and we monkeypatch every path attribute in :mod:`scanner.paths` to point inside that directory. Since every consuming module reads paths via ``paths.X`` (rather than ``from .paths import X``) this is sufficient to isolate disk I/O per test - no module reload tricks required. """ from __future__ import annotations import os from datetime import datetime, timedelta import numpy as np import pandas as pd import pytest from scanner import paths @pytest.fixture(autouse=True) def isolated_paths(tmp_path, monkeypatch): """Redirect every persistent path under :mod:`scanner.paths` into ``tmp_path``.""" history_dir = tmp_path / "history" history_dir.mkdir() monkeypatch.setattr(paths, "TMP_DIR", str(tmp_path)) monkeypatch.setattr(paths, "HISTORY_DIR", str(history_dir)) monkeypatch.setattr(paths, "CACHE_PATH", str(tmp_path / "ohlcv.parquet")) monkeypatch.setattr(paths, "SECTOR_CACHE_PATH", str(tmp_path / "sectors.parquet")) monkeypatch.setattr(paths, "WATCHLIST_PATH", str(tmp_path / "watchlist.json")) monkeypatch.setattr(paths, "LEARNED_WEIGHTS_PATH", str(tmp_path / "learned.json")) monkeypatch.setattr(paths, "PERFORMANCE_LOG_PATH", str(tmp_path / "perf.parquet")) monkeypatch.setattr(paths, "RESULTS_CSV_PATH", str(tmp_path / "results.csv")) monkeypatch.setattr(paths, "L2_CACHE_PATH", str(tmp_path / "l2.parquet")) monkeypatch.setattr(paths, "OPTIONS_CACHE_PATH", str(tmp_path / "options.parquet")) monkeypatch.setattr(paths, "TICK_CACHE_DIR", str(tmp_path / "ticks")) monkeypatch.setattr(paths, "INTRADAY_CACHE_DIR", str(tmp_path / "intraday")) monkeypatch.setattr(paths, "STUB_DIR", str(tmp_path / "stubs")) yield # --------------------------------------------------------------------------- # Synthetic OHLCV helpers # --------------------------------------------------------------------------- def _business_days(n: int, end: datetime | None = None) -> list[datetime]: end = end or datetime(2026, 1, 30) out = [] d = end while len(out) < n: if d.weekday() < 5: out.append(d) d -= timedelta(days=1) return list(reversed(out)) def make_uptrend(n: int = 120, start_price: float = 50.0, daily_drift: float = 0.003, vol_base: int = 1_000_000, seed: int = 1) -> pd.DataFrame: """Generate a synthetic OHLCV frame with a clear up-trend and increasing on-balance volume (close > prev_close most days).""" rng = np.random.default_rng(seed) dates = _business_days(n) close = [start_price] for _ in range(1, n): ret = daily_drift + rng.normal(0, 0.008) close.append(close[-1] * (1.0 + ret)) close = np.array(close) open_ = close * (1 + rng.normal(0, 0.002, size=n)) high = np.maximum(open_, close) * (1 + np.abs(rng.normal(0, 0.005, size=n))) low = np.minimum(open_, close) * (1 - np.abs(rng.normal(0, 0.005, size=n))) vol = (vol_base * (1 + rng.normal(0, 0.2, size=n))).clip(1e4).astype(int) df = pd.DataFrame({"Date": dates, "Open": open_, "High": high, "Low": low, "Close": close, "Volume": vol}) return df def make_downtrend(**kwargs) -> pd.DataFrame: kwargs.setdefault("daily_drift", -0.003) kwargs.setdefault("seed", 2) return make_uptrend(**kwargs) def make_flat(**kwargs) -> pd.DataFrame: kwargs.setdefault("daily_drift", 0.0) kwargs.setdefault("seed", 3) return make_uptrend(**kwargs) @pytest.fixture def uptrend_frame() -> pd.DataFrame: return make_uptrend() @pytest.fixture def downtrend_frame() -> pd.DataFrame: return make_downtrend() @pytest.fixture def flat_frame() -> pd.DataFrame: return make_flat()