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

Momentum oscillators and technical indicators for financial time series analysis.

This module provides implementations of popular momentum indicators including:

  • MACD (Moving Average Convergence Divergence)
  • RSI (Relative Strength Index)

All functions are DataFrame-first and integrate seamlessly with Explorer DataFrames.

Summary

Functions

Add MACD (Moving Average Convergence Divergence) to a DataFrame.

Add RSI (Relative Strength Index) to a DataFrame.

Identify RSI overbought and oversold conditions.

Functions

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

Add MACD (Moving Average Convergence Divergence) to a DataFrame.

MACD is a trend-following momentum indicator that shows the relationship between two moving averages of a security's price. The MACD is calculated by subtracting the 26-period Exponential Moving Average (EMA) from the 12-period EMA.

Components

  • MACD Line: Fast EMA - Slow EMA
  • Signal Line: EMA of the MACD Line (typically 9-period)
  • Histogram: MACD Line - Signal Line

Parameters

  • dataframe - Explorer DataFrame with financial data
  • column - Column name to calculate MACD on (typically :close)
  • opts - Options keyword list:
    • :fast_period - Fast EMA period (default: 12)
    • :slow_period - Slow EMA period (default: 26)
    • :signal_period - Signal EMA period (default: 9)
    • :macd_column - MACD line column name (default: auto-generated)
    • :signal_column - Signal line column name (default: auto-generated)
    • :histogram_column - Histogram column name (default: auto-generated)

Examples

iex> df = Explorer.DataFrame.new(%{
...>   close: [10.0, 11.0, 12.0, 11.5, 13.0, 12.8, 14.0, 13.5, 15.0, 14.2,
...>           16.0, 15.5, 17.0, 16.8, 18.0, 17.5, 19.0, 18.2, 20.0, 19.5,
...>           21.0, 20.8, 22.0, 21.5, 23.0, 22.2, 24.0, 23.8, 25.0, 24.5]
...> })
iex> result = Quant.Math.Oscillators.add_macd!(df, :close)
iex> result |> Explorer.DataFrame.names() |> Enum.sort()
["close", "close_histogram_12_26_9", "close_macd_12_26", "close_signal_9"]

# Custom parameters
iex> df = Explorer.DataFrame.new(%{close: [1.0, 2.0, 3.0, 4.0, 5.0]})
iex> result = Quant.Math.Oscillators.add_macd!(df, :close, fast_period: 2, slow_period: 3, signal_period: 2)
iex> Explorer.DataFrame.names(result) |> Enum.member?("close_macd_2_3")
true

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

Add RSI (Relative Strength Index) to a DataFrame.

RSI is a momentum oscillator that measures the speed and change of price movements. It oscillates between 0 and 100 and is typically used to identify overbought and oversold conditions in a security.

Algorithm

RSI uses Wilder's smoothing method (different from standard EMA):

  • Calculate daily price changes (gains and losses)
  • Apply Wilder's smoothing to average gains and losses
  • RSI = 100 - (100 / (1 + RS)) where RS = Average Gain / Average Loss

Parameters

  • dataframe - Explorer DataFrame with financial data
  • column - Column name to calculate RSI on (typically :close)
  • opts - Options keyword list:
    • :period - RSI period (default: 14)
    • :column_name - Output column name (default: auto-generated)
    • :overbought - Overbought threshold for analysis (default: 70)
    • :oversold - Oversold threshold for analysis (default: 30)

Returns

DataFrame with additional RSI column

Examples

iex> df = Explorer.DataFrame.new(%{
...>   close: [44.0, 44.3, 44.1, 44.2, 44.5, 43.4, 44.0, 44.25, 44.8, 45.1,
...>           45.4, 45.8, 46.0, 45.9, 45.2, 44.8, 44.6, 44.4, 44.2, 44.0]
...> })
iex> result = Quant.Math.Oscillators.add_rsi!(df, :close)
iex> "close_rsi_14" in Explorer.DataFrame.names(result)
true

# Custom parameters
iex> df = Explorer.DataFrame.new(%{close: [10.0, 11.0, 12.0, 11.5, 13.0, 12.8, 14.0, 13.5, 15.0, 14.2]})
iex> result = Quant.Math.Oscillators.add_rsi!(df, :close, period: 5, column_name: "custom_rsi")
iex> "custom_rsi" in Explorer.DataFrame.names(result)
true

detect_macd_crossovers(dataframe, macd_column, signal_column, opts \\ [])

@spec detect_macd_crossovers(
  Explorer.DataFrame.t(),
  String.t(),
  String.t(),
  keyword()
) ::
  Explorer.DataFrame.t()

Detect MACD crossovers in a DataFrame.

Identifies bullish and bearish crossovers between MACD line and Signal line:

  • Bullish Crossover: MACD line crosses above Signal line (buy signal)
  • Bearish Crossover: MACD line crosses below Signal line (sell signal)

Parameters

  • dataframe - DataFrame with MACD and Signal columns
  • macd_column - MACD line column name
  • signal_column - Signal line column name
  • opts - Options:
    • :crossover_column - Output column name (default: "macd_crossover")

Returns

DataFrame with additional crossover column containing:

  • 1 for bullish crossover (MACD crosses above Signal)
  • -1 for bearish crossover (MACD crosses below Signal)
  • 0 for no crossover

Examples

iex> df = Explorer.DataFrame.new(%{
...>   macd: [0.1, 0.2, 0.15, -0.1, -0.2, 0.05, 0.15],
...>   signal: [0.05, 0.15, 0.25, 0.1, -0.05, -0.1, 0.05]
...> })
iex> result = Quant.Math.Oscillators.detect_macd_crossovers(df, "macd", "signal")
iex> crossovers = result |> Explorer.DataFrame.pull("macd_crossover") |> Explorer.Series.to_list()
iex> Enum.any?(crossovers, fn x -> x != 0 end)
true

rsi_signals(dataframe, rsi_column, opts \\ [])

Identify RSI overbought and oversold conditions.

Analyzes RSI values to identify potential trading signals based on traditional overbought (>70) and oversold (<30) levels.

Parameters

  • dataframe - DataFrame with RSI column
  • rsi_column - RSI column name
  • opts - Options:
    • :overbought - Overbought threshold (default: 70)
    • :oversold - Oversold threshold (default: 30)
    • :signal_column - Output column name (default: "rsi_signal")

Returns

DataFrame with additional signal column containing:

  • 1 for oversold condition (potential buy signal)
  • -1 for overbought condition (potential sell signal)
  • 0 for neutral condition

Examples

iex> df = Explorer.DataFrame.new(%{rsi: [20, 40, 60, 80, 50, 25, 75]})
iex> result = Quant.Math.Oscillators.rsi_signals(df, "rsi")
iex> signals = result |> Explorer.DataFrame.pull("rsi_signal") |> Explorer.Series.to_list()
iex> Enum.member?(signals, 1) and Enum.member?(signals, -1)
true