Quant.Math.MovingAverages (quant v0.1.0-alpha.1)

Moving Average technical indicators.

This module implements various moving average calculations including:

  • Simple Moving Average (SMA)
  • Exponential Moving Average (EMA) - Coming soon
  • Weighted Moving Average (WMA) - Coming soon
  • Hull Moving Average (HMA) - Coming soon

All functions work with Explorer DataFrames and use NX tensors internally for high-performance calculations.

Summary

Functions

Add Double Exponential Moving Average (DEMA) to a DataFrame.

Add Exponential Moving Average (EMA) to a DataFrame.

Add Hull Moving Average (HMA) to a DataFrame.

Add Kaufman Adaptive Moving Average (KAMA) to a DataFrame.

Add Simple Moving Average (SMA) to a DataFrame.

Add Triple Exponential Moving Average (TEMA) to a DataFrame.

Add Weighted Moving Average (WMA) column to a DataFrame.

Get information about SMA/EMA results in a DataFrame.

Types

ema_opts()

@type ema_opts() :: [
  period: period_option(),
  alpha: float(),
  name: name_option(),
  nan_policy: nan_policy(),
  min_periods: pos_integer(),
  fillna: any()
]

name_option()

@type name_option() :: String.t() | atom()

nan_policy()

@type nan_policy() :: :drop | :fill_forward | :error

period_option()

@type period_option() :: pos_integer()

sma_opts()

@type sma_opts() :: [
  period: period_option(),
  name: name_option(),
  nan_policy: nan_policy(),
  min_periods: pos_integer(),
  fillna: any()
]

Functions

add_dema!(df, price_column \\ :close, options \\ [])

Add Double Exponential Moving Average (DEMA) to a DataFrame.

The Double Exponential Moving Average was developed by Patrick Mulloy to reduce the lag inherent in traditional exponential moving averages by applying double smoothing.

Algorithm

  1. Calculate EMA(period) of the price data (EMA1)
  2. Calculate EMA(period) of EMA1 (EMA2)
  3. DEMA = 2 × EMA1 - EMA2

Parameters

  • df - Explorer DataFrame containing price data
  • price_column - Name of the column containing prices (default: :close)
  • options - Keyword list with the following options:
    • :period - DEMA period (required, positive integer)
    • :column_name - Name for the DEMA column (default: "dema_N" where N is period)
    • :alpha - Optional smoothing factor. If not provided, uses 2/(period+1)
    • :validate - Whether to validate inputs (default: true)

Returns

  • DataFrame.t() - DataFrame with DEMA column added

Raises

Examples

iex> df = Explorer.DataFrame.new(%{close: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
iex> result = Quant.Math.MovingAverages.add_dema!(df, :close, period: 4)
iex> "close_dema_4" in Map.keys(result.dtypes)
true

# DEMA reduces lag compared to traditional EMA
iex> df = Explorer.DataFrame.new(%{close: [10, 12, 11, 13, 12, 14, 13, 15, 14, 16]})
iex> result = Quant.Math.MovingAverages.add_dema!(df, :close, period: 4)
iex> dema_values = Explorer.DataFrame.to_columns(result)["close_dema_4"]
iex> is_list(dema_values) and length(dema_values) == 10
true

Mathematical Properties

  • Reduced Lag: Responds faster to price changes than traditional EMA
  • Double Smoothing: Uses two EMA calculations for improved trend following
  • Trend Sensitivity: More sensitive to recent price changes than single EMA
  • Overshooting: May overshoot in trending markets due to reduced lag

add_ema!(dataframe, column, opts \\ [])

Add Exponential Moving Average (EMA) to a DataFrame.

The Exponential Moving Average gives more weight to recent prices and responds more quickly to price changes than a simple moving average. The first EMA value is calculated as the SMA of the first period values.

Parameters

  • dataframe - The Explorer DataFrame
  • column - The column to calculate EMA for (atom)
  • opts - Options (keyword list)

Options

  • :period - Number of periods for the exponential moving average (default: 12)
  • :alpha - Smoothing factor (default: 2/(period+1))
  • :name - Name for the new column (default: "<column>ema<period>")
  • :nan_policy - How to handle NaN values (default: :drop)
  • :min_periods - Minimum periods required (default: same as period)
  • :fillna - Value to fill NaN results with (default: nil)

Examples

iex> df = Explorer.DataFrame.new(%{close: [1.0, 2.0, 3.0, 4.0, 5.0]})
iex> result = Quant.Math.MovingAverages.add_ema!(df, :close, period: 3)
iex> "close_ema_3" in Map.keys(result.dtypes)
true

iex> df = Explorer.DataFrame.new(%{close: [10.0, 12.0, 14.0, 16.0, 18.0]})
iex> result = Quant.Math.MovingAverages.add_ema!(df, :close, period: 2, name: "ema_2")
iex> "ema_2" in Map.keys(result.dtypes)
true

add_hma!(df, price_column \\ :close, options \\ [])

Add Hull Moving Average (HMA) to a DataFrame.

The Hull Moving Average was developed by Alan Hull to address the lag inherent in traditional moving averages. It uses weighted moving averages and a square root period to significantly reduce lag while maintaining smoothness.

Algorithm

  1. Calculate WMA(period/2) of the price data
  2. Calculate WMA(period) of the price data
  3. Calculate raw HMA: 2 × WMA(period/2) - WMA(period)
  4. Apply WMA(√period) to the raw HMA for final smoothing

Parameters

  • df - Explorer DataFrame containing price data
  • price_column - Name of the column containing prices (default: :close)
  • options - Keyword list with the following options:
    • :period - HMA period (required, positive integer)
    • :column_name - Name for the HMA column (default: :hma_N where N is period)
    • :validate - Whether to validate inputs (default: true)

Returns

  • DataFrame.t() - DataFrame with HMA column added

Raises

Examples

iex> df = Explorer.DataFrame.new(%{close: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
iex> result = Quant.Math.MovingAverages.add_hma!(df, :close, period: 4)
iex> "close_hma_4" in Map.keys(result.dtypes)
true

# Hull MA reduces lag compared to traditional moving averages
iex> df = Explorer.DataFrame.new(%{close: [10, 12, 11, 13, 12, 14, 13, 15, 14, 16]})
iex> result = Quant.Math.MovingAverages.add_hma!(df, :close, period: 4)
iex> hma_values = Explorer.DataFrame.to_columns(result)["close_hma_4"]
iex> is_list(hma_values) and length(hma_values) == 10
true

Mathematical Properties

  • Reduced Lag: Responds faster to price changes than SMA/EMA
  • Smoothness: Maintains smoothness despite reduced lag
  • Trend Following: Excellent for trend identification
  • Whipsaws: May produce more false signals in choppy markets

add_kama!(df, price_column \\ :close, options \\ [])

@spec add_kama!(Explorer.DataFrame.t(), atom(), keyword()) :: Explorer.DataFrame.t()

Add Kaufman Adaptive Moving Average (KAMA) to a DataFrame.

KAMA is an adaptive moving average that adjusts its smoothing based on market conditions. It uses an Efficiency Ratio to determine how much noise is in the price movement, applying more smoothing during choppy markets and less smoothing during trending markets.

Parameters

  • dataframe - The Explorer DataFrame
  • price_column - The column to calculate KAMA for (default: :close)
  • options - Options (keyword list)

Options

  • :period - Number of periods for efficiency ratio calculation (required, positive integer)
  • :fast_sc - Fast smoothing constant (default: 2, for fast EMA equivalent)
  • :slow_sc - Slow smoothing constant (default: 30, for slow EMA equivalent)
  • :column_name - Name for the KAMA column (default: "close_kama_N" where N is period)

Algorithm

  1. Efficiency Ratio (ER) = |Price Change| / Sum of |Daily Changes|
  2. Smoothing Constant (SC) = [ER × (Fastest SC - Slowest SC) + Slowest SC]²
  3. KAMA = Previous KAMA + SC × (Current Price - Previous KAMA)

The Efficiency Ratio ranges from 0 (very choppy) to 1 (perfectly trending). KAMA adapts between the fast and slow smoothing constants based on this ratio.

Examples

# Basic KAMA with 10-period efficiency ratio
df |> Quant.Math.add_kama!(:close, period: 10)

# KAMA with custom fast/slow parameters
df |> Quant.Math.add_kama!(:close, period: 14, fast_sc: 2, slow_sc: 30)

# KAMA with custom column name
df |> Quant.Math.add_kama!(:high, period: 20, column_name: "kama_high")

Returns

The DataFrame with the KAMA column added. Raises ArgumentError if parameters are invalid or if there's insufficient data.

add_sma!(dataframe, column, opts \\ [])

Add Simple Moving Average (SMA) to a DataFrame.

The Simple Moving Average is calculated as the arithmetic mean of values over a specified period. Values before the minimum required periods are set to NaN.

Parameters

  • dataframe - The Explorer DataFrame
  • column - The column to calculate SMA for (atom)
  • opts - Options (keyword list)

Options

  • :period - Number of periods for the moving average (default: 20)
  • :name - Name for the new column (default: "<column>sma<period>")
  • :nan_policy - How to handle NaN values (default: :drop)
  • :min_periods - Minimum periods required (default: same as period)
  • :fillna - Value to fill NaN results with (default: nil)

Examples

iex> df = Explorer.DataFrame.new(%{close: [1.0, 2.0, 3.0, 4.0, 5.0]})
iex> result = Quant.Math.MovingAverages.add_sma!(df, :close, period: 3)
iex> "close_sma_3" in Map.keys(result.dtypes)
true

iex> df = Explorer.DataFrame.new(%{close: [1.0, 2.0, 3.0, 4.0, 5.0]})
iex> result = Quant.Math.MovingAverages.add_sma!(df, :close, period: 2, name: "ma_2")
iex> "ma_2" in Map.keys(result.dtypes)
true

add_tema!(df, price_column \\ :close, options \\ [])

@spec add_tema!(Explorer.DataFrame.t(), atom(), keyword()) :: Explorer.DataFrame.t()

Add Triple Exponential Moving Average (TEMA) to a DataFrame.

The Triple Exponential Moving Average extends the DEMA concept by applying a third level of exponential smoothing, further reducing lag while maintaining smoothness. TEMA is calculated as:

EMA1 = EMA(price, period) EMA2 = EMA(EMA1, period) EMA3 = EMA(EMA2, period) TEMA = 3 EMA1 - 3 EMA2 + EMA3

This provides even faster response to price changes than DEMA while minimizing noise. TEMA requires approximately 3 * (period - 1) observations before producing valid values.

Parameters

  • dataframe - The Explorer DataFrame
  • price_column - The column to calculate TEMA for (default: :close)
  • options - Options (keyword list)

Options

  • :period - Number of periods for calculation (required, positive integer)
  • :alpha - Smoothing factor (optional, overrides period-based calculation)
  • :column_name - Name for the TEMA column (default: "tema_N" where N is period)

Examples

# Basic TEMA with 10-period
df |> Quant.Math.add_tema!(period: 10)

# TEMA with custom column name and alpha
df |> Quant.Math.add_tema!(:high, period: 20, column_name: "tema_high", alpha: 0.15)

Returns

The DataFrame with the TEMA column added. Raises ArgumentError if parameters are invalid or if there's insufficient data.

add_wma!(df, price_column \\ :close, options \\ [])

Add Weighted Moving Average (WMA) column to a DataFrame.

WMA gives more weight to recent prices with configurable weight vectors. Default uses linear weights: [1, 2, 3, ..., period] where recent prices have higher weights.

Parameters

  • df - Explorer DataFrame containing price data
  • price_column - Name of the column containing prices (default: :close)
  • options - Keyword list with the following options:
    • :period - WMA period (required, positive integer)
    • :column_name - Name for the WMA column (default: :wma_N where N is period)
    • :weights - Custom weight vector as list (default: linear [1, 2, 3, ..., period])
    • :validate - Whether to validate inputs (default: true)

Returns

  • {:ok, DataFrame.t()} - DataFrame with WMA column added
  • {:error, reason} - Error tuple if validation fails

Examples

iex> df = Explorer.DataFrame.new(%{close: [10, 12, 14, 16, 18, 20]})
iex> result = Quant.Math.MovingAverages.add_wma!(df, :close, period: 3)
iex> Explorer.DataFrame.to_columns(result)["close_wma_3"] |> Enum.take(-3) |> Enum.map(&Float.round(&1, 2))
[14.67, 16.67, 18.67]  # Weighted averages with linear weights

# Custom weights (equal weights = SMA)
iex> df = Explorer.DataFrame.new(%{close: [10, 12, 14, 16, 18, 20]})
iex> result = Quant.Math.MovingAverages.add_wma!(df, :close, period: 3, weights: [1, 1, 1])
iex> Explorer.DataFrame.to_columns(result)["close_wma_3"] |> Enum.take(-3)
[14.0, 16.0, 18.0]  # Same as SMA with equal weights

Algorithm

  • Linear weights (default): WMAt = (P_t×N + P(t-1)×(N-1) + ... + P_(t-N+1)×1) / (1+2+...+N)
  • Custom weights: WMAt = (P_t×W_N + P(t-1)×W(N-1) + ... + P(t-N+1)×W_1) / Σ(W_i)
  • Returns NaN for periods with insufficient data

analyze_ma_results!(dataframe, column)

@spec analyze_ma_results!(Explorer.DataFrame.t(), atom() | String.t()) :: map()

Get information about SMA/EMA results in a DataFrame.

This function helps users understand moving average results, especially regarding NaN values that appear before sufficient data points are available.

Parameters

  • dataframe - DataFrame containing moving average results
  • column - The moving average column to analyze (atom or string)

Returns

A map containing:

  • :total_rows - Total number of rows
  • :nan_count - Number of NaN values
  • :valid_count - Number of valid (non-NaN) values
  • :first_valid_index - Index of first valid value
  • :summary_stats - Min, max, mean of valid values

Examples

iex> df = Explorer.DataFrame.new(%{close: [1.0, 2.0, 3.0, 4.0, 5.0]})
iex> result = Quant.Math.MovingAverages.add_sma!(df, :close, period: 3)
iex> info = Quant.Math.MovingAverages.analyze_ma_results!(result, "close_sma_3")
iex> info.valid_count
3