"""Run Length Momentum Analysis - OOD Robust Pattern Discovery.

This script analyzes run length (consecutive bars in same direction) patterns
in open deviation bar data and tests for OOD robustness.

Key Finding:
    P(reversal | position K) DECREASES with K (momentum effect)
    - At K=1: ~43-51% reversal probability
    - At K=7: ~31-38% reversal probability
    - Effect is OOD robust across 2022, 2023, 2024-H1, 2024-H2

Usage:
    uv run --python 3.13 python scripts/run_length_momentum_analysis.py

GitHub Issue: https://github.com/terrylica/opendeviationbar-py/issues/58
SRED-Type: applied-research
SRED-Claim: PATTERN-RESEARCH
"""

import sys
from dataclasses import dataclass

import numpy as np

sys.path.insert(0, "/Users/terryli/eon/opendeviationbar-py")

from opendeviationbar import get_open_deviation_bars


@dataclass
class RunLengthStats:
    """Statistics for run length analysis."""

    n_bars: int
    n_runs: int
    mean_run_length: float
    max_run_length: int
    reversal_probs: dict[int, float]  # P(reverse | position K)
    sample_sizes: dict[int, int]  # Sample size at each position


def compute_run_lengths(directions: list[str]) -> list[tuple[str, int]]:
    """Extract runs from direction sequence.

    Args:
        directions: List of 'U' (up) or 'D' (down) directions

    Returns:
        List of (direction, length) tuples
    """
    if not directions:
        return []

    runs = []
    current_dir = directions[0]
    current_len = 1

    for d in directions[1:]:
        if d == current_dir:
            current_len += 1
        else:
            runs.append((current_dir, current_len))
            current_dir = d
            current_len = 1
    runs.append((current_dir, current_len))

    return runs


def compute_reversal_probabilities(
    run_lengths: list[int], max_k: int = 10, min_samples: int = 30
) -> tuple[dict[int, float], dict[int, int]]:
    """Compute P(reverse | position K) for each position.

    This is survival analysis: given we've seen K consecutive bars,
    what's the probability of reversal vs continuation?

    Args:
        run_lengths: List of run lengths
        max_k: Maximum position to analyze
        min_samples: Minimum samples required for valid estimate

    Returns:
        Tuple of (reversal_probs, sample_sizes)
    """
    probs = {}
    samples = {}

    for k in range(1, max_k + 1):
        # Count runs that reached at least position K
        at_k = sum(1 for r in run_lengths if r >= k)
        # Count runs that continued to K+1
        at_k_plus_1 = sum(1 for r in run_lengths if r >= k + 1)

        samples[k] = at_k

        if at_k >= min_samples:
            # P(reverse at K) = 1 - P(survive to K+1 | at K)
            probs[k] = 1 - (at_k_plus_1 / at_k)

    return probs, samples


def analyze_period(
    symbol: str, start: str, end: str, threshold_decimal_bps: int = 100
) -> RunLengthStats:
    """Analyze run length patterns for a specific period.

    Args:
        symbol: Trading pair symbol
        start: Start date (YYYY-MM-DD)
        end: End date (YYYY-MM-DD)
        threshold_decimal_bps: Open deviation bar threshold in decimal basis points

    Returns:
        RunLengthStats for the period
    """
    df = get_open_deviation_bars(
        symbol, start, end, threshold_decimal_bps=threshold_decimal_bps
    )

    # Determine bar direction
    directions = [
        "U" if c > o else "D" for c, o in zip(df["Close"], df["Open"], strict=True)
    ]

    # Extract runs
    runs = compute_run_lengths(directions)
    up_runs = [r[1] for r in runs if r[0] == "U"]

    # Compute reversal probabilities
    probs, samples = compute_reversal_probabilities(up_runs)

    return RunLengthStats(
        n_bars=len(df),
        n_runs=len(up_runs),
        mean_run_length=np.mean(up_runs) if up_runs else 0.0,
        max_run_length=max(up_runs) if up_runs else 0,
        reversal_probs=probs,
        sample_sizes=samples,
    )


def test_momentum_effect(
    probs: dict[int, float], k_start: int = 1, k_end: int = 7
) -> tuple[float, bool]:
    """Test if momentum effect exists: P(reverse|K=1) > P(reverse|K=7).

    Args:
        probs: Dictionary of reversal probabilities by position
        k_start: Start position for comparison
        k_end: End position for comparison

    Returns:
        Tuple of (difference, holds)
    """
    if k_start not in probs or k_end not in probs:
        return 0.0, False

    diff = probs[k_start] - probs[k_end]
    return diff, diff > 0


def main() -> None:
    """Run OOD robustness analysis for run length momentum."""
    print("=" * 70)
    print("RUN LENGTH MOMENTUM ANALYSIS - OOD Robustness Test")
    print("=" * 70)
    print("\nHypothesis: P(reverse) decreases with run length (momentum effect)")
    print("Implication: Longer runs are MORE likely to continue")
    print()

    # Define test periods
    periods = [
        ("2022-01-01", "2022-12-31", "2022"),
        ("2023-01-01", "2023-12-31", "2023"),
        ("2024-01-01", "2024-06-30", "2024-H1"),
        ("2024-07-01", "2024-12-31", "2024-H2"),
    ]

    # Analyze each period
    results: dict[str, RunLengthStats] = {}
    for start, end, label in periods:
        print(f"Fetching {label}...")
        stats = analyze_period("BTCUSDT", start, end)
        results[label] = stats
        print(
            f"  {stats.n_bars:,} bars, {stats.n_runs:,} up-runs, mean length {stats.mean_run_length:.2f}"
        )

    # Print reversal probability table
    print("\n" + "=" * 70)
    print("P(reverse) by position across periods")
    print("=" * 70)
    print(f"\n{'Position':>8}", end="")
    for label in results:
        print(f" {label:>10}", end="")
    print(f" {'StdDev':>10} {'Consistent?':>12}")
    print("-" * 70)

    for k in range(1, 9):
        print(f"{k:>8}", end="")
        values = []
        for _label, stats in results.items():
            if k in stats.reversal_probs:
                print(f" {stats.reversal_probs[k]:>10.4f}", end="")
                values.append(stats.reversal_probs[k])
            else:
                print(f" {'N/A':>10}", end="")

        if len(values) >= 3:
            std = np.std(values)
            consistent = "YES" if std < 0.05 else "MAYBE" if std < 0.08 else "NO"
            print(f" {std:>10.4f} {consistent:>12}")
        else:
            print()

    # Test momentum effect
    print("\n" + "=" * 70)
    print("MOMENTUM TEST: Is P(reverse) at K=1 > P(reverse) at K=7?")
    print("=" * 70)

    momentum_holds = 0
    total_tests = 0
    for label, stats in results.items():
        diff, holds = test_momentum_effect(stats.reversal_probs)
        if 1 in stats.reversal_probs and 7 in stats.reversal_probs:
            total_tests += 1
            if holds:
                momentum_holds += 1
            status = "YES" if holds else "NO"
            print(
                f"{label}: P(1)={stats.reversal_probs[1]:.4f}, "
                f"P(7)={stats.reversal_probs[7]:.4f}, "
                f"diff={diff:+.4f} → {status}"
            )

    print(f"\nMomentum effect holds in {momentum_holds}/{total_tests} periods")
    if momentum_holds == total_tests:
        print("\n*** MOMENTUM EFFECT IS OOD ROBUST ***")
    else:
        print("\n*** MOMENTUM EFFECT IS NOT ROBUST ***")

    # Print interpretation
    print("\n" + "=" * 70)
    print("INTERPRETATION")
    print("=" * 70)
    print("""
Key Finding:
  The probability of reversal DECREASES as a run extends.
  At position 1: ~43-51% chance of reversal
  At position 7: ~31-38% chance of reversal

This is NOT memoryless (geometric distribution would have constant P).
This indicates exploitable momentum structure.

Trading Implication:
  If we're 7 bars into a run, there's ~35% chance of reversal.
  If we're 1 bar into a run, there's ~47% chance of reversal.
  Longer runs are statistically more likely to continue.

Caveats:
  - This is conditional probability, not predictive edge by itself
  - Need to factor in risk/reward of entries at different positions
  - Transaction costs may erode edge
  - Further testing needed on other symbols and thresholds
""")


if __name__ == "__main__":
    main()
