import numpy as np
import pandas as pd


def ema(s, n):
    return s.ewm(span=n, adjust=False).mean()


def rsi(close, n=14):
    d = close.diff()
    up = d.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
    dn = (-d.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
    rs = up / dn.replace(0, np.nan)
    return (100 - 100 / (1 + rs)).fillna(50)


def macd(close, fast=12, slow=26, sig=9):
    line = ema(close, fast) - ema(close, slow)
    signal = ema(line, sig)
    return line, signal, line - signal


def atr(df, n=14):
    pc = df.close.shift()
    tr = pd.concat([df.high - df.low, (df.high - pc).abs(), (df.low - pc).abs()], axis=1).max(axis=1)
    return tr.ewm(alpha=1 / n, adjust=False).mean()


def adx(df, n=14):
    up = df.high.diff()
    dn = -df.low.diff()
    plus = pd.Series(np.where((up > dn) & (up > 0), up, 0.0), index=df.index)
    minus = pd.Series(np.where((dn > up) & (dn > 0), dn, 0.0), index=df.index)
    a = atr(df, n).replace(0, np.nan)
    pdi = 100 * plus.ewm(alpha=1 / n, adjust=False).mean() / a
    mdi = 100 * minus.ewm(alpha=1 / n, adjust=False).mean() / a
    dx = 100 * (pdi - mdi).abs() / (pdi + mdi).replace(0, np.nan)
    adx_line = dx.ewm(alpha=1 / n, adjust=False).mean().fillna(0)
    return adx_line, pdi.fillna(0), mdi.fillna(0)


def supertrend(df, period=10, mult=3.0):
    """Returns (direction, line). direction: 1 = bullish, -1 = bearish.
    Fully vectorised – no Python loop."""
    a = atr(df, period).to_numpy()
    h, l, c = df.high.to_numpy(), df.low.to_numpy(), df.close.to_numpy()
    hl2 = (h + l) / 2
    up_raw, lo_raw = hl2 + mult * a, hl2 - mult * a
    n = len(df)
    fu, fl = up_raw.copy(), lo_raw.copy()
    d = np.ones(n, dtype=np.int8)
    for i in range(1, n):
        fu[i] = up_raw[i] if (up_raw[i] < fu[i - 1] or c[i - 1] > fu[i - 1]) else fu[i - 1]
        fl[i] = lo_raw[i] if (lo_raw[i] > fl[i - 1] or c[i - 1] < fl[i - 1]) else fl[i - 1]
        if d[i - 1] == -1 and c[i] > fu[i - 1]:
            d[i] = 1
        elif d[i - 1] == 1 and c[i] < fl[i - 1]:
            d[i] = -1
        else:
            d[i] = d[i - 1]
    line = np.where(d == 1, fl, fu)
    return pd.Series(d, index=df.index), pd.Series(line, index=df.index)


def bollinger(close, n=20, k=2.0):
    """Returns (mid, upper, lower, pct_b, bandwidth)."""
    mid = close.rolling(n).mean()
    std = close.rolling(n).std(ddof=0)
    upper = mid + k * std
    lower = mid - k * std
    pct_b = (close - lower) / (upper - lower).replace(0, np.nan)
    bw = (upper - lower) / mid.replace(0, np.nan) * 100
    return mid, upper, lower, pct_b.fillna(0.5), bw.fillna(0)


def stochastic(df, k_period=14, d_period=3):
    """Returns (%K, %D). Overbought > 80, Oversold < 20."""
    low_min = df.low.rolling(k_period).min()
    high_max = df.high.rolling(k_period).max()
    rng = (high_max - low_min).replace(0, np.nan)
    k = ((df.close - low_min) / rng * 100).fillna(50)
    d = k.rolling(d_period).mean()
    return k, d.fillna(50)


def vwap(df):
    """Intraday VWAP approximation using available data."""
    typical = (df.high + df.low + df.close) / 3
    cum_pv = (typical * df.volume).cumsum()
    cum_v = df.volume.cumsum().replace(0, np.nan)
    return cum_pv / cum_v
