#!/usr/bin/env python3
"""
Deterministic technical-indicator calculator from raw OHLCV bars -- no web
search, no LLM arithmetic. Built 2026-08-28 so the intraday watchlist-trigger
reconfirm flow (watchlist_trigger_reconfirm_prompt.md) can score a ticker
using only Questrade's own get_historical_data + get_quotes, instead of
waiting on a web search that may not return a fresh number that day. Also
usable as a fallback inside daily_scan_prompt.md when a web search genuinely
can't confirm a price/volume/ATR reading -- see CLAUDE.md's "Same-Day
Questrade Data Fallback" section for why this exists.

EMA is seeded with the SMA of the first `period` values (the standard
approach) rather than just the first data point, so a short bar history
doesn't bias the result the way a naive single-seed EMA would.

Input: JSON on stdin, a list of daily bars oldest-first, each
`{"date": "YYYY-MM-DD", "open": .., "high": .., "low": .., "close": ..,
"volume": ..}`, optionally followed by today's still-forming bar as the last
element (same shape, using the live quote's open/high/low/last/volume-so-far).
Questrade's get_historical_data caps at ~40 bars per call and truncates
silently -- call it repeatedly, walking `to` back to the earliest date
returned each time, and concatenate (oldest-first, de-duplicated by date)
before piping in here. Fewer than 60 bars total is not enough for a
trustworthy 200d EMA; this script still computes what it can but flags
`insufficient_history_for_ema200` so the caller doesn't quietly trust a
distorted number.

Run manually:
  python3 technical_indicators.py < bars.json
"""
import json
import sys


def sma(values, period):
    return sum(values[:period]) / period


def ema_series(values, period):
    """EMA seeded with the SMA of the first `period` values. Returns a list
    the same length as values, with the first (period-1) entries as None
    (not enough data yet to seed)."""
    if len(values) < period:
        return [None] * len(values)
    k = 2 / (period + 1)
    out = [None] * (period - 1)
    e = sma(values, period)
    out.append(e)
    for v in values[period:]:
        e = v * k + e * (1 - k)
        out.append(e)
    return out


def last_ema(values, period):
    series = ema_series(values, period)
    return series[-1] if series else None


def rsi(closes, period=14):
    if len(closes) < period + 1:
        return None
    gains = []
    losses = []
    for i in range(1, len(closes)):
        d = closes[i] - closes[i - 1]
        gains.append(max(d, 0.0))
        losses.append(max(-d, 0.0))
    avg_gain = sum(gains[:period]) / period
    avg_loss = sum(losses[:period]) / period
    for i in range(period, len(gains)):
        avg_gain = (avg_gain * (period - 1) + gains[i]) / period
        avg_loss = (avg_loss * (period - 1) + losses[i]) / period
    if avg_loss == 0:
        return 100.0
    rs = avg_gain / avg_loss
    return 100 - 100 / (1 + rs)


def macd_histogram(closes, fast=12, slow=26, signal_period=9):
    if len(closes) < slow + signal_period:
        return None, "insufficient_history (need >= {} bars, have {})".format(
            slow + signal_period, len(closes))
    ema_fast = ema_series(closes, fast)
    ema_slow = ema_series(closes, slow)
    macd_line = [
        (f - s) if (f is not None and s is not None) else None
        for f, s in zip(ema_fast, ema_slow)
    ]
    macd_valid = [m for m in macd_line if m is not None]
    if len(macd_valid) < signal_period:
        return None, "insufficient_history_for_signal_line"
    signal_line = ema_series(macd_valid, signal_period)
    hist = macd_valid[-1] - signal_line[-1]
    return hist, None


def atr(highs, lows, closes, period=14):
    if len(closes) < period + 1:
        return None
    trs = []
    for i in range(1, len(closes)):
        tr = max(
            highs[i] - lows[i],
            abs(highs[i] - closes[i - 1]),
            abs(lows[i] - closes[i - 1]),
        )
        trs.append(tr)
    a = sum(trs[:period]) / period
    for t in trs[period:]:
        a = (a * (period - 1) + t) / period
    return a


def compute(bars):
    closes = [b['close'] for b in bars]
    highs = [b['high'] for b in bars]
    lows = [b['low'] for b in bars]
    volumes = [b['volume'] for b in bars]

    n = len(bars)
    current_price = closes[-1]

    ema20 = last_ema(closes, 20)
    ema50 = last_ema(closes, 50)
    ema200 = last_ema(closes, 200)
    rsi14 = rsi(closes, 14)
    atr14 = atr(highs, lows, closes, 14)
    macd_hist, macd_error = macd_histogram(closes)

    # Volume average excludes the last (possibly still-forming) bar so a
    # partial today doesn't dilute its own comparison baseline.
    prior_volumes = volumes[:-1] if n > 1 else volumes
    avg_volume_20d = sum(prior_volumes[-20:]) / len(prior_volumes[-20:]) if prior_volumes else None
    volume_ratio_latest = (volumes[-1] / avg_volume_20d) if avg_volume_20d else None

    prior_highs = highs[:-1] if n > 1 else highs
    recent_high_20d = max(prior_highs[-20:]) if prior_highs else None

    result = {
        "bars_used": n,
        "current_price": current_price,
        "ema20": round(ema20, 2) if ema20 is not None else None,
        "ema50": round(ema50, 2) if ema50 is not None else None,
        "ema200": round(ema200, 2) if ema200 is not None else None,
        "insufficient_history_for_ema200": ema200 is None,
        "pct_above_ema20": round((current_price - ema20) / ema20 * 100, 2) if ema20 else None,
        "pct_above_ema200": round((current_price - ema200) / ema200 * 100, 2) if ema200 else None,
        "rsi14": round(rsi14, 2) if rsi14 is not None else None,
        "atr14": round(atr14, 4) if atr14 is not None else None,
        "macd_histogram": round(macd_hist, 4) if macd_hist is not None else None,
        "macd_error": macd_error,
        "avg_volume_20d": round(avg_volume_20d, 0) if avg_volume_20d else None,
        "latest_volume": volumes[-1],
        "volume_ratio_vs_20d_avg": round(volume_ratio_latest, 3) if volume_ratio_latest else None,
        "recent_high_20d_excl_latest": recent_high_20d,
        "price_above_recent_high_20d": (
            current_price > recent_high_20d if recent_high_20d is not None else None
        ),
    }
    return result


def _main():
    bars = json.load(sys.stdin)
    if not bars:
        print(json.dumps({"error": "no bars provided"}))
        sys.exit(1)
    print(json.dumps(compute(bars), indent=2))


if __name__ == '__main__':
    _main()
