"""
V25 Short-Volume Feature Diagnostic: FINRA Short Ratio

Hypothesis (Boehmer et al. 2020): For a LONG ORB, higher short-volume ratio entering
a breakout day predicts weaker follow-through (informed bears leaning against the move).
Direction: LOWER short ratio → HIGHER r_multiple. Confirmed empirically here.

Method:
  1. Run V24 simulation over 200d window (baseline)
  2. For each completed trade, compute three short-volume features from FINRA CDN data:
       - short_ratio_prior_day : ratio from the most-recent prior trading day
       - short_ratio_avg_20d   : mean ratio over last 20 prior trading days
       - short_ratio_zscore_20d: (prior_day − mean_20d) / std_20d (spike detector)
  3. Report per-feature: Pearson vs r_multiple_at_exit, tercile WR/avg_R, coverage
  4. Pairwise correlation vs obv_slope_20 (redundancy check)

Gates (for promotion to Phase 2):
  G1: |Pearson| ≥ 0.07 on ≥ 120 trades (relaxed to 60 if window coverage < 80%)
  G2: |top − bottom tercile avg_R| ≥ 0.30R
  G3: |top − bottom tercile WR| ≥ 5pp
  G4: feature coverage ≥ 80% of trades with valid r_multiple
  G5: |ρ(short_feature, obv_slope_20)| < 0.70 (redundancy gate)

Data source: FINRA CDN daily files cached to data/cache/finra_daily/
  URL template: https://cdn.finra.org/equity/regsho/daily/CNMSshvol{yyyymmdd}.txt
"""
from __future__ import annotations

import concurrent.futures
import datetime as dt
import json
import math
import os
import statistics
import sys
from pathlib import Path

import requests
import yaml
from zoneinfo import ZoneInfo

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")))

from libs.common.time_utils import trading_days_between
from libs.intraday.domain import ORBStrategyParams
from libs.intraday.features import compute_obv_slope_approx, enrich_daily_bars
from libs.intraday.orb_simulator import ORBSimulationState, run_orb_simulation_with_state
from libs.intraday.screener import orb_pre_screen_candidates

# ── Config ──────────────────────────────────────────────────────────────────
V24_CONFIG = "configs/intraday/strategies/orb_gainers_v24_quality_overlay.yaml"
UNIVERSE_FILE = "configs/symbols_midlarge_snapshot_exact.yaml"
INTRADAY_CACHE_DIR = "data/cache/intraday"
FINRA_CACHE_DIR = "data/cache/finra_daily"
LOOKBACK_DAYS = 200
FEATURE_LOOKBACK_TRADING = 25   # need 20 prior short-vol days; add small buffer

FINRA_CDN_URL = "https://cdn.finra.org/equity/regsho/daily/CNMSshvol{yyyymmdd}.txt"

_ET = ZoneInfo("America/New_York")
_MKT_OPEN = dt.time(9, 30)
_MKT_CLOSE = dt.time(16, 0)


# ── FINRA Data Fetching ──────────────────────────────────────────────────────

def _fetch_finra_daily(date: dt.date) -> dict[str, float]:
    """Download and parse one FINRA daily file. Returns {ticker: short_ratio}."""
    cache_path = Path(FINRA_CACHE_DIR) / f"{date.isoformat()}.json"
    if cache_path.exists():
        try:
            with open(cache_path) as f:
                return json.load(f)
        except Exception:
            pass

    url = FINRA_CDN_URL.format(yyyymmdd=date.strftime("%Y%m%d"))
    try:
        resp = requests.get(url, timeout=15)
    except requests.RequestException:
        return {}
    if resp.status_code != 200:
        return {}

    lines = [line.strip() for line in resp.text.splitlines() if line.strip()]
    if not lines:
        return {}

    header = [p.strip() for p in lines[0].split("|")]
    try:
        sym_idx = header.index("Symbol")
        short_idx = header.index("ShortVolume")
        total_idx = header.index("TotalVolume")
    except ValueError:
        return {}

    ratios: dict[str, float] = {}
    for line in lines[1:]:
        parts = [p.strip() for p in line.split("|")]
        if len(parts) <= max(sym_idx, short_idx, total_idx):
            continue
        try:
            total = float(parts[total_idx])
            short = float(parts[short_idx])
        except ValueError:
            continue
        if total > 0:
            ratios[parts[sym_idx].upper()] = short / total

    Path(FINRA_CACHE_DIR).mkdir(parents=True, exist_ok=True)
    with open(cache_path, "w") as f:
        json.dump(ratios, f)
    return ratios


def build_short_volume_dict(trading_days: list[str], workers: int = 16) -> dict[str, dict[str, float]]:
    """Download FINRA CDN for each trading day → {ticker: {date: short_ratio}}.

    Dates are trading-day strings. Files are cached to FINRA_CACHE_DIR.
    """
    def _load(d_str: str) -> tuple[str, dict[str, float]]:
        d = dt.date.fromisoformat(d_str)
        return d_str, _fetch_finra_daily(d)

    print(f"  Fetching/loading {len(trading_days)} FINRA CDN files (parallel, cached)...")
    results: list[tuple[str, dict[str, float]]] = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        for i, (d_str, ratios) in enumerate(ex.map(_load, trading_days)):
            results.append((d_str, ratios))
            if (i + 1) % 25 == 0 or (i + 1) == len(trading_days):
                print(f"  {i+1}/{len(trading_days)} days loaded")

    # Invert: {ticker: {date: ratio}}
    by_ticker: dict[str, dict[str, float]] = {}
    for d_str, ratios in results:
        for ticker, ratio in ratios.items():
            if ticker not in by_ticker:
                by_ticker[ticker] = {}
            by_ticker[ticker][d_str] = ratio
    return by_ticker


# ── Short-Ratio Feature Computations ────────────────────────────────────────

def compute_short_ratio_prior_day(
    ticker: str,
    entry_date: str,
    short_vol: dict[str, dict[str, float]],
    trading_days: list[str],
) -> float | None:
    """Most-recent prior-day short ratio. Lookahead-free (strictly before entry_date)."""
    ticker_data = short_vol.get(ticker, {})
    if not ticker_data:
        return None
    if entry_date in trading_days:
        idx = trading_days.index(entry_date)
        for d in reversed(trading_days[:idx]):
            r = ticker_data.get(d)
            if r is not None:
                return r
    return None


def compute_short_ratio_avg_20d(
    ticker: str,
    entry_date: str,
    short_vol: dict[str, dict[str, float]],
    trading_days: list[str],
) -> float | None:
    """Mean short ratio over the last 20 prior trading days."""
    ticker_data = short_vol.get(ticker, {})
    if not ticker_data:
        return None
    if entry_date not in trading_days:
        return None
    idx = trading_days.index(entry_date)
    prior = trading_days[max(0, idx - 20):idx]
    vals = [ticker_data[d] for d in prior if d in ticker_data]
    if not vals:
        return None
    return sum(vals) / len(vals)


def compute_short_ratio_zscore_20d(
    ticker: str,
    entry_date: str,
    short_vol: dict[str, dict[str, float]],
    trading_days: list[str],
) -> float | None:
    """Z-score of prior-day short ratio vs 20d history."""
    prior_day = compute_short_ratio_prior_day(ticker, entry_date, short_vol, trading_days)
    avg_20d = compute_short_ratio_avg_20d(ticker, entry_date, short_vol, trading_days)
    if prior_day is None or avg_20d is None:
        return None
    if entry_date not in trading_days:
        return None
    idx = trading_days.index(entry_date)
    prior = trading_days[max(0, idx - 20):idx]
    ticker_data = short_vol.get(ticker, {})
    vals = [ticker_data[d] for d in prior if d in ticker_data]
    if len(vals) < 5:
        return None
    std = statistics.stdev(vals) if len(vals) >= 2 else 0.0
    if std <= 0:
        return 0.0
    return (prior_day - avg_20d) / std


# ── Daily Bar Builder (same as diag_orb_quality_features.py) ─────────────────

import pyarrow.parquet as pq


def _build_daily_bar_from_intraday(path: Path, date: str) -> dict | None:
    try:
        table = pq.read_table(str(path))
        rows = table.to_pydict()
    except Exception:
        return None
    opens, highs, lows, closes, vols = [], [], [], [], []
    for i, ts_raw in enumerate(rows.get("timestamp", [])):
        try:
            if ts_raw.endswith("Z"):
                ts_raw = ts_raw[:-1] + "+00:00"
            ts = dt.datetime.fromisoformat(ts_raw).astimezone(_ET)
        except Exception:
            continue
        if _MKT_OPEN <= ts.time() < _MKT_CLOSE:
            opens.append(float(rows["open"][i] or 0))
            highs.append(float(rows["high"][i] or 0))
            lows.append(float(rows["low"][i] or 0))
            closes.append(float(rows["close"][i] or 0))
            vols.append(float(rows["volume"][i] or 0))
    if not opens:
        return None
    return {
        "date": date, "open": opens[0], "high": max(highs),
        "low": min(lows), "close": closes[-1], "volume": sum(vols),
    }


def build_daily_bars(tickers: list[str], dates: list[str], workers: int = 8) -> dict[str, list[dict]]:
    root = Path(INTRADAY_CACHE_DIR)

    def _load(ticker: str) -> tuple[str, list[dict]]:
        d_path = root / ticker
        if not d_path.is_dir():
            return ticker, []
        bars: list[dict] = []
        for date in dates:
            p = d_path / f"{date}.parquet"
            if not p.exists():
                continue
            bar = _build_daily_bar_from_intraday(p, date)
            if bar and bar["close"] > 0:
                bars.append(bar)
        return ticker, bars

    result: dict[str, list[dict]] = {}
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
        for ticker, bars in ex.map(_load, tickers):
            if bars:
                result[ticker] = bars
    return result


def load_intraday_bulk(candidates: dict[str, list[str]]) -> dict[str, dict[str, list[dict]]]:
    import pandas as pd
    result: dict[str, dict[str, list[dict]]] = {}
    for date, tickers in candidates.items():
        day_bars: dict[str, list[dict]] = {}
        for ticker in tickers:
            p = Path(INTRADAY_CACHE_DIR) / ticker / f"{date}.parquet"
            if not p.exists():
                continue
            try:
                df = pd.read_parquet(str(p))
                if not df.empty and len(df) >= 5:
                    day_bars[ticker] = df.to_dict("records")
            except Exception:
                pass
        if day_bars:
            result[date] = day_bars
    return result


# ── Stats Helpers ─────────────────────────────────────────────────────────────

def pearson(xs: list[float], ys: list[float]) -> float | None:
    if len(xs) != len(ys) or len(xs) < 2:
        return None
    n = len(xs)
    xm = sum(xs) / n
    ym = sum(ys) / n
    num = sum((xs[i] - xm) * (ys[i] - ym) for i in range(n))
    dx = (sum((x - xm) ** 2 for x in xs)) ** 0.5
    dy = (sum((y - ym) ** 2 for y in ys)) ** 0.5
    if dx <= 0 or dy <= 0:
        return None
    return num / (dx * dy)


def tercile_stats(vals: list[float], outcomes_r: list[float]) -> dict:
    if len(vals) < 6:
        return {}
    pairs = sorted(zip(vals, outcomes_r), key=lambda p: p[0])
    n = len(pairs)
    t = n // 3

    def stats(pairs_sub):
        ys = [p[1] for p in pairs_sub]
        wins = [y for y in ys if y > 0]
        wr = len(wins) / len(ys) if ys else 0.0
        avg = sum(ys) / len(ys) if ys else 0.0
        return {"n": len(ys), "wr": wr, "avg_r": avg}

    return {
        "low": stats(pairs[:t]),
        "mid": stats(pairs[t:2 * t]),
        "high": stats(pairs[2 * t:]),
    }


# ── Main ─────────────────────────────────────────────────────────────────────

def main() -> None:
    print("=== V25 Short-Volume Feature Diagnostic ===\n")

    # 1. Load V24 config
    with open(V24_CONFIG) as f:
        raw = yaml.safe_load(f)
    params = ORBStrategyParams(**raw["orb_strategy"])
    print(f"V24 config loaded. weight_obv_slope={params.weight_obv_slope}")

    # 2. Determine 200d trading window
    today = dt.date(2026, 4, 21)
    all_td = trading_days_between(today - dt.timedelta(days=400), today)
    trading_days_list = [d.isoformat() for d in all_td[-LOOKBACK_DAYS:]]
    print(f"Window: {trading_days_list[0]} → {trading_days_list[-1]} ({len(trading_days_list)} trading days)")

    # Extended range for daily bars (need feature_lookback for prev_bars)
    extended_td = [d.isoformat() for d in all_td[-(LOOKBACK_DAYS + FEATURE_LOOKBACK_TRADING + 10):]]
    first_cal = dt.date.fromisoformat(extended_td[0])
    last_cal = dt.date.fromisoformat(trading_days_list[-1])
    needed_dates: list[str] = []
    d = first_cal
    while d <= last_cal:
        needed_dates.append(d.isoformat())
        d += dt.timedelta(days=1)

    # 3. Load universe
    with open(UNIVERSE_FILE) as f:
        udata = yaml.safe_load(f)
    universe = udata.get("symbols", udata) if isinstance(udata, dict) else udata
    if "QQQ" not in universe:
        universe = list(universe) + ["QQQ"]
    print(f"Universe: {len(universe)} tickers")

    # 4. Build daily bars
    print(f"\nBuilding daily bars ({len(needed_dates)} calendar days)...")
    daily_bars = build_daily_bars(universe, needed_dates)
    print(f"Built daily bars for {len(daily_bars)} tickers")

    # 5. Enrichment
    print("Computing enrichment...")
    enrichment = enrich_daily_bars(daily_bars, trading_days_list)
    print(f"Enrichment for {len(enrichment)} tickers")

    # 6. Pre-screen + load intraday
    candidates = orb_pre_screen_candidates(
        daily_bars, trading_days_list, enrichment,
        min_price=params.min_price,
        min_atr=params.min_atr_14,
        min_avg_dollar_vol=params.min_avg_dollar_volume,
        max_per_day=None,
    )
    total_pairs = sum(len(v) for v in candidates.values())
    print(f"Pre-screened: {total_pairs} ticker-days")
    print("Loading intraday bars...")
    all_intraday = load_intraday_bulk(candidates)
    intraday_pairs = sum(len(v) for v in all_intraday.values())
    print(f"Loaded: {intraday_pairs} ticker-days")

    # 7. Run V24 simulation
    print("\nRunning V24 simulation...")
    state = ORBSimulationState(equity=params.initial_capital)
    day_results, _ = run_orb_simulation_with_state(
        all_intraday, trading_days_list, params, enrichment, state=state,
    )
    all_trades = [t for dr in day_results for t in dr.trades]
    trades_with_r = [t for t in all_trades if getattr(t, "r_multiple_at_exit", None) is not None]
    print(f"Total trades: {len(all_trades)}, with r_multiple: {len(trades_with_r)}")

    if len(trades_with_r) < 20:
        print("ABORT: fewer than 20 trades with r_multiple — insufficient sample")
        return

    # 8. Load FINRA short-volume data (CDN, cached)
    # Need data for extended window to compute 20d lookback before first trade
    extended_short_days = [d.isoformat() for d in all_td[-(LOOKBACK_DAYS + 30):]]
    print(f"\nLoading FINRA short-volume data for {len(extended_short_days)} trading days...")
    short_vol = build_short_volume_dict(extended_short_days)
    print(f"Short-vol dict: {len(short_vol)} tickers with data")

    # Check coverage for unique trade tickers
    trade_tickers_uniq = {t.ticker for t in trades_with_r}
    covered_tickers = {t for t in trade_tickers_uniq if t in short_vol}
    print(f"Trade tickers: {len(trade_tickers_uniq)} unique, {len(covered_tickers)} have short-vol data")

    # 9. Build daily bars lookup for OBV (pairwise correlation)
    sorted_daily: dict[str, list[dict]] = {
        t: sorted(bars, key=lambda b: b["date"])
        for t, bars in daily_bars.items()
    }

    # 10. Compute features per trade
    print("\nComputing features per trade...")
    annotated: list[dict] = []
    missing = {"prior_day": 0, "avg_20d": 0, "zscore_20d": 0, "obv_slope": 0}

    for trade in trades_with_r:
        ticker = trade.ticker
        date = trade.date
        r = float(trade.r_multiple_at_exit)

        # Short-ratio features
        f_prior = compute_short_ratio_prior_day(ticker, date, short_vol, extended_short_days)
        f_avg = compute_short_ratio_avg_20d(ticker, date, short_vol, extended_short_days)
        f_zscore = compute_short_ratio_zscore_20d(ticker, date, short_vol, extended_short_days)

        # OBV slope (for pairwise correlation)
        bars_t = sorted_daily.get(ticker, [])
        prev_bars = [b for b in bars_t if b["date"][:10] < date]
        obv = compute_obv_slope_approx(prev_bars, lookback=20)

        if f_prior is None:
            missing["prior_day"] += 1
        if f_avg is None:
            missing["avg_20d"] += 1
        if f_zscore is None:
            missing["zscore_20d"] += 1
        if obv is None:
            missing["obv_slope"] += 1

        annotated.append({
            "ticker": ticker, "date": date, "r": r,
            "prior_day": f_prior,
            "avg_20d": f_avg,
            "zscore_20d": f_zscore,
            "obv_slope": obv,
            "win": r > 0,
        })

    total = len(annotated)
    print(f"Annotated: {total} trades")
    print(f"Missing: prior_day={missing['prior_day']}, avg_20d={missing['avg_20d']}, "
          f"zscore_20d={missing['zscore_20d']}, obv_slope={missing['obv_slope']}")

    # 11. Report per-feature
    feature_defs = [
        ("short_ratio_prior_day", "prior_day",
         "Lower = fewer informed shorts = cleaner breakout (Boehmer 2020)", "low"),
        ("short_ratio_avg_20d", "avg_20d",
         "20d avg short ratio baseline", "low"),
        ("short_ratio_zscore_20d", "zscore_20d",
         "Spike detector: high z = unusual short surge before breakout", "low"),
    ]

    print("\n" + "=" * 90)
    print("FEATURE ANALYSIS — V24 200d trade set")
    print("=" * 90)

    results = {}
    for feat_name, feat_key, description, best_tercile in feature_defs:
        valid = [(t[feat_key], t["r"]) for t in annotated if t[feat_key] is not None]
        if len(valid) < 20:
            print(f"\n{feat_name}: SKIP — only {len(valid)} valid trades (need ≥20)")
            results[feat_name] = None
            continue

        vals = [v[0] for v in valid]
        rs = [v[1] for v in valid]
        wins = [v for v in valid if v[1] > 0]
        overall_wr = len(wins) / len(valid)

        rho = pearson(vals, rs)
        tstat = tercile_stats(vals, rs)
        coverage_pct = len(valid) / total

        print(f"\n{'─'*60}")
        print(f"FEATURE: {feat_name}")
        print(f"  Description: {description}")
        print(f"  n={len(valid)}/{total} ({coverage_pct*100:.0f}% coverage), overall WR={overall_wr*100:.1f}%")
        if rho is not None:
            print(f"  Pearson(feature, r_multiple) = {rho:.4f}")
        else:
            print("  Pearson = n/a")

        # Determine which end is "high performance"
        worst_tercile = "high" if best_tercile == "low" else "low"

        if tstat:
            h = tstat["high"]
            m = tstat["mid"]
            lo = tstat["low"]
            print(f"  Tercile breakdown (low→high feature value):")
            print(f"    Bottom: n={lo['n']}, WR={lo['wr']*100:.1f}%, avg_R={lo['avg_r']:+.3f}")
            print(f"    Middle: n={m['n']}, WR={m['wr']*100:.1f}%, avg_R={m['avg_r']:+.3f}")
            print(f"    Top:    n={h['n']}, WR={h['wr']*100:.1f}%, avg_R={h['avg_r']:+.3f}")

            best = tstat[best_tercile]
            worst = tstat[worst_tercile]
            rho_abs = abs(rho) if rho is not None else 0.0

            # Adjust minimum trades gate based on coverage
            min_trades = 120 if coverage_pct >= 0.80 else 60
            g1 = rho_abs >= 0.07 and len(valid) >= min_trades
            g2 = best["avg_r"] - worst["avg_r"] >= 0.30
            g3 = best["wr"] >= worst["wr"] + 0.05
            g4 = coverage_pct >= 0.50  # at least 50% of trades covered

            print(f"  Gates (best='{best_tercile}' tercile):")
            print(f"    G1 |Pearson|≥0.07 + n≥{min_trades}: {rho_abs:.4f}, n={len(valid)}  → {'PASS ✓' if g1 else 'FAIL ✗'}")
            print(f"    G2 avg_R gap ≥ 0.30R: {best['avg_r'] - worst['avg_r']:+.3f}  → {'PASS ✓' if g2 else 'FAIL ✗'}")
            print(f"    G3 WR gap ≥ 5pp: {(best['wr'] - worst['wr'])*100:+.1f}pp  → {'PASS ✓' if g3 else 'FAIL ✗'}")
            print(f"    G4 coverage ≥ 50%: {coverage_pct*100:.0f}%  → {'PASS ✓' if g4 else 'FAIL ✗'}")
            overall_pass = g1 and g2 and g3 and g4
            print(f"  VERDICT: {'ALL GATES PASS → PROCEED TO PHASE 2' if overall_pass else f'FAIL ({sum([not g1, not g2, not g3, not g4])} gate(s) failed)'}")
            results[feat_name] = {
                "pass": overall_pass, "pearson": rho, "stats": tstat,
                "n": len(valid), "coverage": coverage_pct, "best_tercile": best_tercile,
            }

    # 12. Pairwise correlations
    print(f"\n{'─'*60}")
    print("PAIRWISE CORRELATIONS (short features vs obv_slope_20):")
    for feat_key, feat_name_short in [("prior_day", "prior_day"), ("avg_20d", "avg_20d"), ("zscore_20d", "zscore_20d")]:
        combined = [(t[feat_key], t["obv_slope"]) for t in annotated
                    if t[feat_key] is not None and t["obv_slope"] is not None]
        if len(combined) >= 10:
            fvals = [c[0] for c in combined]
            ovals = [c[1] for c in combined]
            rho_x = pearson(fvals, ovals)
            print(f"  ρ({feat_name_short}, obv_slope): {rho_x:.4f}" if rho_x is not None else f"  ρ({feat_name_short}, obv_slope): n/a")
            if rho_x is not None:
                g5_pass = abs(rho_x) < 0.70
                if feat_name_short in [k.split("short_ratio_")[-1] if k.startswith("short_ratio_") else k for k in results]:
                    print(f"    G5 |ρ| < 0.70: {'PASS ✓' if g5_pass else 'FAIL (redundant with OBV) ✗'}")

    # Inter-feature correlations
    for f1, f2 in [("prior_day", "avg_20d"), ("prior_day", "zscore_20d"), ("avg_20d", "zscore_20d")]:
        combined = [(t[f1], t[f2]) for t in annotated if t[f1] is not None and t[f2] is not None]
        if len(combined) >= 10:
            rho_x = pearson([c[0] for c in combined], [c[1] for c in combined])
            print(f"  ρ({f1}, {f2}): {rho_x:.4f}" if rho_x is not None else f"  ρ({f1}, {f2}): n/a")

    # 13. Summary
    passing = [name for name, r in results.items() if r is not None and r["pass"]]
    failed = [name for name, r in results.items() if r is not None and not r["pass"]]
    skipped = [name for name, r in results.items() if r is None]

    print(f"\n{'='*90}")
    print("SUMMARY")
    print(f"{'='*90}")
    print(f"Features passing all gates: {passing if passing else 'NONE'}")
    print(f"Features failing gates: {failed if failed else 'NONE'}")
    print(f"Features skipped (insufficient data): {skipped if skipped else 'NONE'}")

    if passing:
        # Pick strongest by |Pearson|
        best = max(passing, key=lambda n: abs(results[n]["pearson"] or 0))
        pearson_val = results[best]["pearson"]
        obv_pearson_ref = 0.2349  # V24's OBV Pearson from Phase 1
        weight_magnitude = round(0.05 * min(1.0, abs(pearson_val) / obv_pearson_ref), 2)
        weight_magnitude = max(weight_magnitude, 0.02)

        # Sign: "low" best tercile means NEGATIVE weight (lower ratio = better)
        best_direction = results[best]["best_tercile"]
        weight_sign = -1 if best_direction == "low" else +1

        print(f"\nVERDICT: PROCEED TO PHASE 2")
        print(f"  Best feature: {best}")
        print(f"  Pearson: {pearson_val:.4f}")
        print(f"  Direction: '{best_direction}' tercile is best → weight_short_ratio = {weight_sign * weight_magnitude:+.3f}")
        print(f"  Suggested weight_short_ratio in V25 config: {weight_sign * weight_magnitude:+.3f}")
        print(f"  (Magnitude: 0.05 × |{abs(pearson_val):.4f}| / 0.23 = {weight_magnitude:.3f}, floor 0.02)")
        print(f"\n  → Wire '{best}' as 'short_ratio_signal' in enrich_daily_bars")
        print(f"  → Add weight_short_ratio = {weight_sign * weight_magnitude:+.3f} to V25 config (parent V24)")
    else:
        print(f"\nVERDICT: ABORT — FINRA short-volume axis null on V24 200d trade set")
        print("  → V24 remains champion. Short-volume axis exhausted on this set.")
        print("  → Next Ralph iteration: RSI-14, BB %B, or gravitational pull from libs/features/market_features.py")


if __name__ == "__main__":
    main()
