from __future__ import annotations

import numpy as np
import pandas as pd


def add_technical_features(group: pd.DataFrame) -> pd.DataFrame:
    result = group.copy()
    close = result["adjusted_close"].fillna(result["close"])
    delta = close.diff()
    gain = delta.clip(lower=0).ewm(alpha=1 / 14, adjust=False, min_periods=14).mean()
    loss = (-delta.clip(upper=0)).ewm(alpha=1 / 14, adjust=False, min_periods=14).mean()
    rs = gain / loss.replace(0, np.nan)
    result["rsi_14"] = 100 - 100 / (1 + rs)
    ema12 = close.ewm(span=12, adjust=False, min_periods=12).mean()
    ema26 = close.ewm(span=26, adjust=False, min_periods=26).mean()
    result["macd"] = ema12 - ema26
    result["macd_signal"] = result["macd"].ewm(span=9, adjust=False, min_periods=9).mean()
    result["macd_histogram"] = result["macd"] - result["macd_signal"]
    result["roc_10"] = close.pct_change(10, fill_method=None)
    low14 = result["low"].rolling(14, min_periods=14).min()
    high14 = result["high"].rolling(14, min_periods=14).max()
    result["stochastic_k_14"] = 100 * (close - low14) / (high14 - low14).replace(0, np.nan)
    result["stochastic_d_3"] = result["stochastic_k_14"].rolling(3, min_periods=3).mean()
    previous_close = result["close"].shift(1)
    true_range = pd.concat([
        result["high"] - result["low"],
        (result["high"] - previous_close).abs(),
        (result["low"] - previous_close).abs(),
    ], axis=1).max(axis=1)
    result["atr_14"] = true_range.rolling(14, min_periods=14).mean()
    result["atr_14_pct"] = result["atr_14"] / close
    result["daily_range"] = result["high"] - result["low"]
    result["daily_range_pct"] = result["daily_range"] / close
    daily_return = close.pct_change(fill_method=None)
    for window in (5, 10, 20):
        result[f"volatility_{window}d"] = daily_return.rolling(window, min_periods=window).std(ddof=0)
    middle = close.rolling(20, min_periods=20).mean()
    std = close.rolling(20, min_periods=20).std(ddof=0)
    upper, lower = middle + 2 * std, middle - 2 * std
    result["bollinger_percent_b"] = (close - lower) / (upper - lower).replace(0, np.nan)
    result["bollinger_bandwidth"] = (upper - lower) / middle
    return result

