"""
This library provides technical analysis functions for the Polars
DataFrame API.

Financial Disclaimer
--------------------
The indicators and outputs generated by this library do NOT constitute
financial, investment, trading, or professional advice. They are for
educational and informational purposes only.

Users should perform independent analysis, validate results, and consult
a licensed financial professional before making investment decisions.

The authors assume no liability for losses or damages arising from the
use of this software.
"""

import polars as pl


@pl.api.register_dataframe_namespace("ta_ext")
class TechnicalAnalysisNamespace:
    def __init__(self, df: pl.DataFrame):
        self._df = df

    # -----------------------
    # Basic transforms
    # -----------------------
    def delta(self, col: str, periods: int = 1) -> pl.DataFrame:
        """
        Difference between current value and value N periods ago.

        Returns <col>_delta_<periods>
        """
        return self._df.with_columns(
            (pl.col(col) - pl.col(col).shift(periods)).alias(f"{col}_delta_{periods}")
        )

    def log_return(self, col: str, periods: int = 1) -> pl.DataFrame:
        """
        Log return: ln(current / previous N-period value)

        Returns <col>_log_return_<periods>
        """
        return self._df.with_columns(
            (pl.col(col) / pl.col(col).shift(periods))
            .log()
            .alias(f"{col}_log_return_{periods}")
        )

    def compare(self, col: str, value: float, op: str = "ge") -> pl.DataFrame:
        """
        Compare column values to a scalar with operators: le, ge, lt, gt, eq, ne.

        Returns <col>_<op>_<value>
        """
        op_map = {
            "le": pl.col(col) <= value,
            "ge": pl.col(col) >= value,
            "lt": pl.col(col) < value,
            "gt": pl.col(col) > value,
            "eq": pl.col(col) == value,
            "ne": pl.col(col) != value,
        }
        return self._df.with_columns(op_map[op].alias(f"{col}_{op}_{value}"))

    # -----------------------
    # Moving averages
    # -----------------------
    def sma(self, col: str, window: int) -> pl.DataFrame:
        """Simple Moving Average (SMA)."""
        return self._df.with_columns(
            pl.col(col).rolling_mean(window).alias(f"{col}_sma_{window}")
        )

    def ema(self, col: str, span: int) -> pl.DataFrame:
        """Exponential Moving Average (EMA)."""
        try:
            return self._df.with_columns(
                pl.col(col).ewm_mean(span=span).alias(f"{col}_ema_{span}")
            )
        except Exception:
            return self._df.with_columns(
                pl.map(
                    [pl.col(col)],
                    lambda s: s.to_pandas().ewm(span=span, adjust=False).mean(),
                )[0].alias(f"{col}_ema_{span}")
            )

    def mstd(self, col: str, window: int) -> pl.DataFrame:
        """Moving Standard Deviation."""
        return self._df.with_columns(
            pl.col(col).rolling_std(window).alias(f"{col}_mstd_{window}")
        )

    def mvar(self, col: str, window: int) -> pl.DataFrame:
        """Moving Variance."""
        return self._df.with_columns(
            pl.col(col).rolling_var(window).alias(f"{col}_mvar_{window}")
        )

    def mad(self, col: str, window: int) -> pl.DataFrame:
        """Mean Absolute Deviation over rolling window."""
        rolling_mean = pl.col(col).rolling_mean(window)
        return self._df.with_columns(
            (pl.col(col) - rolling_mean)
            .abs()
            .rolling_mean(window)
            .alias(f"{col}_mad_{window}")
        )

    def zscore(self, col: str, window: int) -> pl.DataFrame:
        """Z-score normalized column over rolling window."""
        mean = pl.col(col).rolling_mean(window)
        std = pl.col(col).rolling_std(window)
        return self._df.with_columns(
            ((pl.col(col) - mean) / std).alias(f"{col}_z_{window}")
        )

    def roc(self, col: str, periods: int = 1) -> pl.DataFrame:
        """Rate of Change (ROC)."""
        return self._df.with_columns(
            (
                (pl.col(col) - pl.col(col).shift(periods)) / pl.col(col).shift(periods)
            ).alias(f"{col}_roc_{periods}")
        )

    def dma(
        self, col: str, short_window: int = 10, long_window: int = 50
    ) -> pl.DataFrame:
        """
        Difference of Moving Averages (DMA): short SMA minus long SMA.

        Parameters
        ----------
        col : str
            Column to compute DMA on.
        short_window : int, default 10
            Short period moving average.
        long_window : int, default 50
            Long period moving average.

        Returns
        -------
        DataFrame with new column: <col>_dma_<short>_<long>

        Example
        -------
        >>> df.ta_ext.dma(close="close", short_window=10, long_window=50)
        """
        short_sma = pl.col(col).rolling_mean(short_window)
        long_sma = pl.col(col).rolling_mean(long_window)
        return self._df.with_columns(
            (short_sma - long_sma).alias(f"{col}_dma_{short_window}_{long_window}")
        )

    def dmi(self, high: str, low: str, close: str, window: int = 14) -> pl.DataFrame:
        """
        Directional Movement Index (DMI) with +DI, -DI, ADX, ADXR.

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.
        window : int, default 14
            Rolling window for smoothing.

        Returns
        -------
        DataFrame with columns: +DI, -DI, ADX, ADXR

        Example
        -------
        >>> df.ta_ext.dmi(high="high", low="low", close="close", window=14)
        """
        up_move = pl.col(high) - pl.col(high).shift(1)
        down_move = pl.col(low).shift(1) - pl.col(low)
        plus_dm = (
            pl.when((up_move > down_move) & (up_move > 0)).then(up_move).otherwise(0)
        )
        minus_dm = (
            pl.when((down_move > up_move) & (down_move > 0))
            .then(down_move)
            .otherwise(0)
        )
        tr = self.tr(high, low, close)["tr"]
        plus_di = (plus_dm.rolling_sum(window) / tr.rolling_sum(window)) * 100
        minus_di = (minus_dm.rolling_sum(window) / tr.rolling_sum(window)) * 100
        dx = ((plus_di - minus_di).abs() / (plus_di + minus_di)) * 100
        adx = dx.rolling_mean(window)
        adxr = (adx + adx.shift(window)) / 2
        return self._df.with_columns(
            [
                plus_di.alias("+DI"),
                minus_di.alias("-DI"),
                adx.alias("ADX"),
                adxr.alias("ADXR"),
            ]
        )

    def trix(self, col: str, window: int = 15) -> pl.DataFrame:
        """
        Triple Exponential Moving Average (TRIX).

        Parameters
        ----------
        col : str
            Column to compute TRIX on.
        window : int, default 15
            EMA span.

        Returns
        -------
        DataFrame with new column: <col>_trix_<window>

        Example
        -------
        >>> df.ta_ext.trix(col="close", window=15)
        """
        ema1 = pl.col(col).ewm_mean(span=window)
        ema2 = ema1.ewm_mean(span=window)
        ema3 = ema2.ewm_mean(span=window)
        trix_col = (ema3 - ema3.shift(1)) / ema3.shift(1) * 100
        return self._df.with_columns(trix_col.alias(f"{col}_trix_{window}"))

    def tema(self, col: str, span: int = 15) -> pl.DataFrame:
        """
        Triple Exponential Moving Average (TEMA).

        Parameters
        ----------
        col : str
            Column to compute TEMA on.
        span : int, default 15
            EMA span.

        Returns
        -------
        DataFrame with new column: <col>_tema_<span>

        Example
        -------
        >>> df.ta_ext.tema(col="close", span=15)
        """
        ema1 = pl.col(col).ewm_mean(span=span)
        ema2 = ema1.ewm_mean(span=span)
        ema3 = ema2.ewm_mean(span=span)
        tema_col = 3 * ema1 - 3 * ema2 + ema3
        return self._df.with_columns(tema_col.alias(f"{col}_tema_{span}"))

    def supertrend(
        self, high: str, low: str, close: str, period: int = 10, multiplier: float = 3.0
    ) -> pl.DataFrame:
        """
        Supertrend indicator with Upper Band and Lower Band.

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.
        period : int, default 10
            ATR period.
        multiplier : float, default 3.0
            ATR multiplier.

        Returns
        -------
        DataFrame with columns: supertrend, upper_band, lower_band

        Example
        -------
        >>> df.ta_ext.supertrend(high="high", low="low", close="close", period=10, multiplier=3)
        """
        atr = self.atr(high, low, close, period)[f"atr_{period}"]
        hl2 = (pl.col(high) + pl.col(low)) / 2
        upper_band = hl2 + multiplier * atr
        lower_band = hl2 - multiplier * atr

        # Initialize Supertrend column
        st_col = pl.Series("supertrend", [0] * len(self._df))
        # Note: Polars does not allow easy iterative calculation, use pandas fallback for full logic if needed
        return self._df.with_columns(
            [upper_band.alias("upper_band"), lower_band.alias("lower_band"), st_col]
        )

    def kama(
        self, col: str, window: int = 10, fast: int = 2, slow: int = 30
    ) -> pl.DataFrame:
        """
        Kaufman Adaptive Moving Average (KAMA).

        Parameters
        ----------
        col : str
            Column to compute KAMA on.
        window : int, default 10
            Efficiency ratio window.
        fast : int, default 2
            Fast EMA period.
        slow : int, default 30
            Slow EMA period.

        Returns
        -------
        DataFrame with new column: <col>_kama_<window>

        Example
        -------
        >>> df.ta_ext.kama(col="close", window=10, fast=2, slow=30)
        """
        diff = (pl.col(col) - pl.col(col).shift(window)).abs()
        volatility = pl.col(col).diff().abs().rolling_sum(window)
        er = diff / volatility
        sc = (er * (2 / (fast + 1) - 2 / (slow + 1)) + 2 / (slow + 1)) ** 2
        kama_col = pl.col(col).rolling_apply(
            lambda s: s[0], window=1
        )  # placeholder for iterative KAMA
        return self._df.with_columns(kama_col.alias(f"{col}_kama_{window}"))

    # -----------------------
    # Volatility indicators
    # -----------------------
    def tr(self, high: str, low: str, close: str) -> pl.DataFrame:
        """True Range (TR)."""
        df = self._df.with_columns(
            (pl.col(high) - pl.col(low)).alias("_hl"),
            (pl.col(high) - pl.col(close).shift(1)).abs().alias("_hc"),
            (pl.col(low) - pl.col(close).shift(1)).abs().alias("_lc"),
        )
        df = df.with_columns(pl.max_horizontal(["_hl", "_hc", "_lc"]).alias("tr")).drop(
            ["_hl", "_hc", "_lc"]
        )
        return df

    def atr(self, high: str, low: str, close: str, window: int = 14) -> pl.DataFrame:
        """Average True Range (ATR)."""
        df = self.tr(high, low, close)
        df = df.with_columns(
            pl.col("tr").rolling_mean(window).alias(f"atr_{window}")
        ).drop("tr")
        return df

    def chop(self, high: str, low: str, close: str, window: int = 14) -> pl.DataFrame:
        """Choppiness Index (CHOP)."""
        tr = self.tr(high, low, close)["tr"]
        atr_sum = tr.rolling_sum(window)
        highest = pl.max_horizontal([pl.col(high).rolling_max(window)])
        lowest = pl.min_horizontal([pl.col(low).rolling_min(window)])
        chop_index = (
            100 * (pl.col("atr_{}".format(window)) / (highest - lowest)).log10()
        )
        return self._df.with_columns(chop_index.alias(f"chop_{window}"))

    # -----------------------
    # Oscillators
    # -----------------------
    def rsi(self, col: str, window: int = 14) -> pl.DataFrame:
        """Relative Strength Index (RSI)."""
        diff = pl.col(col) - pl.col(col).shift(1)
        df = self._df.with_columns(
            diff.alias("_diff"),
            pl.when(diff > 0).then(diff).otherwise(0).alias("_gain"),
            pl.when(diff < 0).then(-diff).otherwise(0).alias("_loss"),
        )
        df = df.with_columns(
            pl.col("_gain").rolling_mean(window).alias("_avg_gain"),
            pl.col("_loss").rolling_mean(window).alias("_avg_loss"),
        )
        df = df.with_columns(
            (100 - 100 / (1 + (pl.col("_avg_gain") / pl.col("_avg_loss")))).alias(
                f"{col}_rsi_{window}"
            )
        ).drop(["_diff", "_gain", "_loss", "_avg_gain", "_avg_loss"])
        return df

    def stoch_rsi(self, col: str, window: int = 14) -> pl.DataFrame:
        """Stochastic RSI."""
        rsi_df = self.rsi(col, window)
        rsi_col = f"{col}_rsi_{window}"
        min_rsi = pl.col(rsi_col).rolling_min(window)
        max_rsi = pl.col(rsi_col).rolling_max(window)
        stoch_rsi_col = (pl.col(rsi_col) - min_rsi) / (max_rsi - min_rsi)
        return rsi_df.with_columns(stoch_rsi_col.alias(f"{col}_stoch_rsi_{window}"))

    def kdj(
        self,
        high: str,
        low: str,
        close: str,
        window: int = 14,
        k_smooth: int = 3,
        d_smooth: int = 3,
    ) -> pl.DataFrame:
        """KDJ Stochastic Oscillator."""
        low_min = pl.col(low).rolling_min(window)
        high_max = pl.col(high).rolling_max(window)
        rsv = 100 * ((pl.col(close) - low_min) / (high_max - low_min))
        k = rsv.rolling_mean(k_smooth)
        d = k.rolling_mean(d_smooth)
        j = 3 * k - 2 * d
        return self._df.with_columns(k.alias("K"), d.alias("D"), j.alias("J"))

    def bollinger(
        self, col: str, window: int = 20, num_std: float = 2.0
    ) -> pl.DataFrame:
        """Bollinger Bands (mid, upper, lower)."""
        mid = pl.col(col).rolling_mean(window)
        std = pl.col(col).rolling_std(window)
        return self._df.with_columns(
            mid.alias(f"{col}_bb_mid"),
            (mid + num_std * std).alias(f"{col}_bb_upper"),
            (mid - num_std * std).alias(f"{col}_bb_lower"),
        )

    def macd(
        self, col: str, fast: int = 12, slow: int = 26, signal: int = 9
    ) -> pl.DataFrame:
        """MACD (Moving Average Convergence Divergence)."""
        fast_ema = pl.col(col).ewm_mean(span=fast)
        slow_ema = pl.col(col).ewm_mean(span=slow)
        macd_line = fast_ema - slow_ema
        signal_line = macd_line.ewm_mean(span=signal)
        hist = macd_line - signal_line
        return self._df.with_columns(
            macd_line.alias(f"{col}_macd"),
            signal_line.alias(f"{col}_macd_signal"),
            hist.alias(f"{col}_macd_hist"),
        )

    def cci(self, high: str, low: str, close: str, window: int = 20) -> pl.DataFrame:
        """
        Commodity Channel Index (CCI).

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.
        window : int, default 20
            Rolling window for calculation.

        Returns
        -------
        DataFrame with column: <col>_cci_<window>

        Example
        -------
        >>> df.ta_ext.cci(high="high", low="low", close="close", window=20)
        """
        typical_price = (pl.col(high) + pl.col(low) + pl.col(close)) / 3
        sma_tp = typical_price.rolling_mean(window)
        mean_dev = (typical_price - sma_tp).abs().rolling_mean(window)
        cci_col = (typical_price - sma_tp) / (0.015 * mean_dev)
        return self._df.with_columns(cci_col.alias(f"cci_{window}"))

    def wr(self, high: str, low: str, close: str, window: int = 14) -> pl.DataFrame:
        """
        Williams %R Overbought/Oversold indicator.

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.
        window : int, default 14
            Rolling window for calculation.

        Returns
        -------
        DataFrame with column: wr_<window>

        Example
        -------
        >>> df.ta_ext.wr(high="high", low="low", close="close", window=14)
        """
        highest_high = pl.col(high).rolling_max(window)
        lowest_low = pl.col(low).rolling_min(window)
        wr_col = -100 * ((highest_high - pl.col(close)) / (highest_high - lowest_low))
        return self._df.with_columns(wr_col.alias(f"wr_{window}"))

    def ao(self, high: str, low: str, short: int = 5, long: int = 34) -> pl.DataFrame:
        """
        Awesome Oscillator (AO).

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        short : int, default 5
            Short-term SMA.
        long : int, default 34
            Long-term SMA.

        Returns
        -------
        DataFrame with column: ao_<short>_<long>

        Example
        -------
        >>> df.ta_ext.ao(high="high", low="low", short=5, long=34)
        """
        mid_price = (pl.col(high) + pl.col(low)) / 2
        ao_col = mid_price.rolling_mean(short) - mid_price.rolling_mean(long)
        return self._df.with_columns(ao_col.alias(f"ao_{short}_{long}"))

    def bop(self, open: str, high: str, low: str, close: str) -> pl.DataFrame:
        """
        Balance of Power (BOP).

        Parameters
        ----------
        open_ : str
            Open price column.
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.

        Returns
        -------
        DataFrame with column: bop

        Example
        -------
        >>> df.ta_ext.bop(open="open", high="high", low="low", close="close")
        """
        bop_col = (pl.col(close) - pl.col(open)) / (pl.col(high) - pl.col(low))
        return self._df.with_columns(bop_col.alias("bop"))

    def roc(self, col: str, periods: int = 1) -> pl.DataFrame:
        """
        Rate of Change (ROC) momentum indicator.

        Parameters
        ----------
        col : str
            Column to calculate ROC.
        periods : int, default 1
            Number of periods for difference.

        Returns
        -------
        DataFrame with column: <col>_roc_<periods>

        Example
        -------
        >>> df.ta_ext.roc("close", 12)
        """
        roc_col = (
            (pl.col(col) - pl.col(col).shift(periods))
            / pl.col(col).shift(periods)
            * 100
        )
        return self._df.with_columns(roc_col.alias(f"{col}_roc_{periods}"))

    def coppock(
        self,
        col: str,
        long_period: int = 14,
        short_period: int = 11,
        wma_period: int = 10,
    ) -> pl.DataFrame:
        """
        Coppock Curve indicator.

        Parameters
        ----------
        col : str
            Column to compute Coppock curve.
        long_period : int, default 14
            Long-term ROC period.
        short_period : int, default 11
            Short-term ROC period.
        wma_period : int, default 10
            Smoothing period for WMA.

        Returns
        -------
        DataFrame with column: coppock

        Example
        -------
        >>> df.ta_ext.coppock(col="close", long_period=14, short_period=11, wma_period=10)
        """
        roc_long = (
            (pl.col(col) - pl.col(col).shift(long_period))
            / pl.col(col).shift(long_period)
            * 100
        )
        roc_short = (
            (pl.col(col) - pl.col(col).shift(short_period))
            / pl.col(col).shift(short_period)
            * 100
        )
        raw = roc_long + roc_short
        # Simple placeholder for WMA: use rolling_mean as proxy
        coppock_col = raw.rolling_mean(wma_period)
        return self._df.with_columns(coppock_col.alias("coppock"))

    def ppo(
        self, col: str, fast: int = 12, slow: int = 26, signal: int = 9
    ) -> pl.DataFrame:
        """
        Percentage Price Oscillator (PPO).

        Parameters
        ----------
        col : str
            Column to calculate PPO.
        fast : int, default 12
            Fast EMA period.
        slow : int, default 26
            Slow EMA period.
        signal : int, default 9
            Signal line EMA period.

        Returns
        -------
        DataFrame with columns: <col>_ppo, <col>_ppo_signal, <col>_ppo_hist

        Example
        -------
        >>> df.ta_ext.ppo(col="close", fast=12, slow=26, signal=9)
        """
        fast_ema = pl.col(col).ewm_mean(span=fast)
        slow_ema = pl.col(col).ewm_mean(span=slow)
        ppo_line = (fast_ema - slow_ema) / slow_ema * 100
        signal_line = ppo_line.ewm_mean(span=signal)
        hist = ppo_line - signal_line
        return self._df.with_columns(
            [
                ppo_line.alias(f"{col}_ppo"),
                signal_line.alias(f"{col}_ppo_signal"),
                hist.alias(f"{col}_ppo_hist"),
            ]
        )

    # -----------------------
    # Complex / Multi-line Indicators
    # -----------------------

    def ichimoku(
        self,
        high: str,
        low: str,
        close: str,
        tenkan: int = 9,
        kijun: int = 26,
        senkou: int = 52,
    ) -> pl.DataFrame:
        """
        Ichimoku Cloud indicator: Tenkan-sen, Kijun-sen, Senkou Span A/B, Chikou Span.

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.
        tenkan : int, default 9
            Conversion line period.
        kijun : int, default 26
            Base line period.
        senkou : int, default 52
            Leading span B period.

        Returns
        -------
        DataFrame with columns: tenkan_sen, kijun_sen, senkou_a, senkou_b, chikou_span

        Example
        -------
        >>> df.ta_ext.ichimoku(high="high", low="low", close="close")
        """
        high_tenkan = pl.col(high).rolling_max(tenkan)
        low_tenkan = pl.col(low).rolling_min(tenkan)
        tenkan_sen = (high_tenkan + low_tenkan) / 2

        high_kijun = pl.col(high).rolling_max(kijun)
        low_kijun = pl.col(low).rolling_min(kijun)
        kijun_sen = (high_kijun + low_kijun) / 2

        senkou_a = ((tenkan_sen + kijun_sen) / 2).shift(kijun)
        high_senkou_b = pl.col(high).rolling_max(senkou)
        low_senkou_b = pl.col(low).rolling_min(senkou)
        senkou_b = ((high_senkou_b + low_senkou_b) / 2).shift(kijun)

        chikou_span = pl.col(close).shift(-kijun)

        return self._df.with_columns(
            [
                tenkan_sen.alias("tenkan_sen"),
                kijun_sen.alias("kijun_sen"),
                senkou_a.alias("senkou_a"),
                senkou_b.alias("senkou_b"),
                chikou_span.alias("chikou_span"),
            ]
        )

    def eri(self, high: str, low: str, close: str, window: int = 13) -> pl.DataFrame:
        """
        Elder-Ray Index (ERI): Bull Power and Bear Power.

        Parameters
        ----------
        high : str
            High price column.
        low : str
            Low price column.
        close : str
            Close price column.
        window : int, default 13
            EMA period.

        Returns
        -------
        DataFrame with columns: bull_power, bear_power

        Example
        -------
        >>> df.ta_ext.eri(high="high", low="low", close="close", window=13)
        """
        ema_close = pl.col(close).ewm_mean(span=window)
        bull_power = pl.col(high) - ema_close
        bear_power = pl.col(low) - ema_close
        return self._df.with_columns(
            [bull_power.alias("bull_power"), bear_power.alias("bear_power")]
        )

    def fisher_transform(self, col: str, window: int = 10) -> pl.DataFrame:
        """
        Gaussian Fisher Transform (FTR).

        Parameters
        ----------
        col : str
            Column to transform.
        window : int, default 10
            Lookback period.

        Returns
        -------
        DataFrame with column: ftr_<window>

        Example
        -------
        >>> df.ta_ext.fisher_transform(col="close", window=10)
        """
        low_min = pl.col(col).rolling_min(window)
        high_max = pl.col(col).rolling_max(window)
        x = 0.33 * 2 * ((pl.col(col) - low_min) / (high_max - low_min) - 0.5) + 0.67 * 0
        ftr_col = 0.5 * (
            pl.Series([0] * len(self._df)) + x.rolling_apply(lambda s: s[0], window)
        )
        return self._df.with_columns(ftr_col.alias(f"ftr_{window}"))

    def qqe(self, rsi_col: str, window: int = 14, smooth: int = 5) -> pl.DataFrame:
        """
        Quantitative Qualitative Estimation (QQE) indicator.

        Parameters
        ----------
        rsi_col : str
            Column containing RSI values.
        window : int, default 14
            Smoothing period.
        smooth : int, default 5
            Signal smoothing period.

        Returns
        -------
        DataFrame with columns: qqe_rsi, qqe_signal

        Example
        -------
        >>> df.ta_ext.qqe(rsi_col="close_rsi_14", window=14, smooth=5)
        """
        rsi = pl.col(rsi_col)
        delta_rsi = rsi.diff().abs()
        atr_rsi = delta_rsi.rolling_mean(window)
        qqe_signal = atr_rsi.rolling_mean(smooth)
        return self._df.with_columns(
            [atr_rsi.alias("qqe_rsi"), qqe_signal.alias("qqe_signal")]
        )

    def lrma(self, col: str, window: int = 14) -> pl.DataFrame:
        """
        Linear Regression Moving Average (LRMA).

        Parameters
        ----------
        col : str
            Column to compute LRMA on.
        window : int, default 14
            Rolling window.

        Returns
        -------
        DataFrame with column: <col>_lrma_<window>

        Example
        -------
        >>> df.ta_ext.lrma(col="close", window=14)
        """

        def linreg(s: pl.Series) -> float:
            x = pl.Series(range(len(s)))
            y = s
            n = len(y)
            if n == 0:
                return 0.0
            slope = (n * (x * y).sum() - x.sum() * y.sum()) / (
                n * (x**2).sum() - (x.sum()) ** 2
            )
            intercept = (y.sum() - slope * x.sum()) / n
            return intercept + slope * (n - 1)

        lrma_col = pl.col(col).rolling_apply(linreg, window)
        return self._df.with_columns(lrma_col.alias(f"{col}_lrma_{window}"))

    def cti(self, col: str, window: int = 14) -> pl.DataFrame:
        """
        Correlation Trend Indicator (CTI).

        Parameters
        ----------
        col : str
            Column to compute CTI on.
        window : int, default 14
            Rolling window.

        Returns
        -------
        DataFrame with column: <col>_cti_<window>

        Example
        -------
        >>> df.ta_ext.cti(col="close", window=14)
        """
        s = pl.col(col)
        mean = s.rolling_mean(window)
        dev = s - mean
        numerator = (dev * dev.shift(1)).rolling_sum(window)
        denominator = (dev**2).rolling_sum(window)
        cti_col = numerator / denominator
        return self._df.with_columns(cti_col.alias(f"{col}_cti_{window}"))
