ZenQuant.Volatility (zen_quant v0.2.0)

Copy Markdown View Source

Volatility calculations for trading systems.

Pure functions for calculating historical (realized) volatility, comparing implied vs realized volatility, and computing volatility metrics.

Example

prices = [100, 102, 99, 103, 101, 105, 102]

ZenQuant.Volatility.realized(prices)
# => 0.023  (2.3% daily volatility)

ZenQuant.Volatility.realized(prices, annualize: true)
# => 0.365  (36.5% annualized)

API Functions

FunctionArityDescriptionParam Kinds
rolling3Calculate rolling volatility over a price series.prices: value, window: value
yang_zhang2Calculate Yang-Zhang volatility estimator from OHLC data.candles: exchange_data
garman_klass2Calculate Garman-Klass volatility estimator from OHLC data.candles: exchange_data
elevated?3Check if volatility regime is elevated.current_vol: value, baseline_vol: value
cone2Calculate volatility cone for term structure analysis.prices: value, periods: value
iv_vs_rv3Compare implied volatility to realized volatility.implied_vol: value, realized_vol: value
iv_rank2Calculate IV rank (normalized position in min-max range).current_iv: value, historical_ivs: value
iv_percentile2Calculate IV percentile relative to historical distribution.current_iv: value, historical_ivs: value
parkinson2Calculate Parkinson volatility estimator from high-low ranges.candles: exchange_data
realized2Calculate realized (historical) volatility from close-to-close returns.prices: value
normalize_candles1Normalize positional OHLCV rows into candle maps.rows: exchange_data

Summary

Types

OHLC candle for volatility estimation

IV comparison output format

Positional OHLCV row returned by Bourse 0.6.x

Volatility as decimal (e.g., 0.25 = 25%)

Functions

Calculate volatility cone for term structure analysis.

Check if volatility regime is elevated.

Calculate Garman-Klass volatility estimator from OHLC data.

Calculate IV percentile relative to historical distribution.

Calculate IV rank (normalized position in min-max range).

Compare implied volatility to realized volatility.

Normalize positional OHLCV rows into candle maps.

Calculate Parkinson volatility estimator from high-low ranges.

Calculate realized (historical) volatility from close-to-close returns.

Calculate rolling volatility over a price series.

Calculate Yang-Zhang volatility estimator from OHLC data.

Types

candle()

@type candle() :: %{
  optional(:timestamp) => number(),
  optional(:open) => number(),
  :high => number(),
  :low => number(),
  optional(:close) => number(),
  optional(:volume) => number()
}

OHLC candle for volatility estimation

iv_format()

@type iv_format() :: :ratio | :premium | :premium_pct

IV comparison output format

ohlcv_row()

@type ohlcv_row() :: [number()]

Positional OHLCV row returned by Bourse 0.6.x

volatility()

@type volatility() :: float()

Volatility as decimal (e.g., 0.25 = 25%)

Functions

cone(prices, periods)

@spec cone([number()], [pos_integer()]) :: %{
  required(pos_integer()) => volatility() | nil
}

Calculate volatility cone for term structure analysis.

Parameters

  • prices - List of prices (chronological order) (value)
  • periods - List of lookback periods to calculate (value)

Returns

Map of period => volatility value (or nil if insufficient data) (map)

Example

%{7 => 0.42, 30 => 0.36}
# descripex:contract
%{
  params: %{
    periods: %{
      description: "List of lookback periods to calculate",
      kind: :value
    },
    prices: %{description: "List of prices (chronological order)", kind: :value}
  },
  returns: %{
    type: :map,
    description: "Map of period => volatility value (or nil if insufficient data)"
  },
  returns_example: %{7 => 0.42, 30 => 0.36}
}

elevated?(current_vol, baseline_vol, threshold \\ 1.5)

@spec elevated?(number(), number(), number()) :: boolean()

Check if volatility regime is elevated.

Parameters

  • current_vol - Current volatility (value)
  • baseline_vol - Normal/baseline volatility (value)

Options

  • threshold - Multiple of baseline considered elevated (default: 1.5)

Returns

true if current_vol > baseline_vol * threshold (boolean)

Example

true
# descripex:contract
%{
  opts: %{
    threshold: %{
      default: 1.5,
      type: :number,
      description: "Multiple of baseline considered elevated"
    }
  },
  params: %{
    current_vol: %{description: "Current volatility", kind: :value},
    baseline_vol: %{description: "Normal/baseline volatility", kind: :value}
  },
  returns: %{
    type: :boolean,
    description: "true if current_vol > baseline_vol * threshold"
  },
  returns_example: true
}

garman_klass(candles, opts \\ [])

@spec garman_klass(
  [candle()],
  keyword()
) :: volatility() | nil | {:error, :invalid_prices}

Calculate Garman-Klass volatility estimator from OHLC data.

Parameters

  • candles - List of candles with :open, :high, :low, :close fields (exchange_data)

Options

  • annualize - Whether to annualize (default: false)
  • trading_days - Trading days per year (default: 365)

Returns

Garman-Klass volatility, nil if insufficient data, or {:error, :invalid_prices} (float)

Example

0.1095

Errors

  • :invalid_prices
# descripex:contract
%{
  opts: %{
    annualize: %{
      default: false,
      type: :boolean,
      description: "Whether to annualize"
    },
    trading_days: %{
      default: 365,
      type: :integer,
      description: "Trading days per year"
    }
  },
  params: %{
    candles: %{
      description: "List of candles with :open, :high, :low, :close fields",
      source: "fetch_ohlcv(symbol)",
      kind: :exchange_data
    }
  },
  errors: [:invalid_prices],
  returns: %{
    type: :float,
    description: "Garman-Klass volatility, nil if insufficient data, or {:error, :invalid_prices}"
  },
  returns_example: 0.1095
}

iv_percentile(current_iv, historical_ivs)

@spec iv_percentile(number(), [number()]) :: float() | nil

Calculate IV percentile relative to historical distribution.

Parameters

  • current_iv - Current implied volatility (value)
  • historical_ivs - List of historical IV values (value)

Returns

Percentile (0-100) of historical values below current, or nil if empty (float)

Example

0.1095
# descripex:contract
%{
  params: %{
    current_iv: %{description: "Current implied volatility", kind: :value},
    historical_ivs: %{description: "List of historical IV values", kind: :value}
  },
  returns: %{
    type: :float,
    description: "Percentile (0-100) of historical values below current, or nil if empty"
  },
  returns_example: 0.1095
}

iv_rank(current_iv, historical_ivs)

@spec iv_rank(number(), [number()]) :: float() | nil

Calculate IV rank (normalized position in min-max range).

Parameters

  • current_iv - Current implied volatility (value)
  • historical_ivs - List of historical IV values (value)

Returns

Rank (0-100) showing position in min-max range, or nil if insufficient data (float)

Example

0.1095
# descripex:contract
%{
  params: %{
    current_iv: %{description: "Current implied volatility", kind: :value},
    historical_ivs: %{description: "List of historical IV values", kind: :value}
  },
  returns: %{
    type: :float,
    description: "Rank (0-100) showing position in min-max range, or nil if insufficient data"
  },
  returns_example: 0.1095
}

iv_vs_rv(implied_vol, realized_vol, format \\ :ratio)

@spec iv_vs_rv(volatility(), volatility(), iv_format()) :: float() | nil

Compare implied volatility to realized volatility.

Parameters

  • implied_vol - Current implied volatility (value)
  • realized_vol - Realized volatility over same period (value)

Options

  • format - :ratio (IV/RV), :premium (IV-RV), or :premium_pct ((IV-RV)/RV*100) (default: :ratio)

Returns

Comparison result in specified format, or nil if RV is zero (float)

Example

0.1095
# descripex:contract
%{
  opts: %{
    format: %{
      default: :ratio,
      type: :atom,
      description: ":ratio (IV/RV), :premium (IV-RV), or :premium_pct ((IV-RV)/RV*100)"
    }
  },
  params: %{
    implied_vol: %{description: "Current implied volatility", kind: :value},
    realized_vol: %{
      description: "Realized volatility over same period",
      kind: :value
    }
  },
  returns: %{
    type: :float,
    description: "Comparison result in specified format, or nil if RV is zero"
  },
  returns_example: 0.1095
}

normalize_candles(rows)

@spec normalize_candles([ohlcv_row()]) :: [candle()]

Normalize positional OHLCV rows into candle maps.

Parameters

  • rows - List of [timestamp, open, high, low, close, volume] rows (exchange_data)

Returns

List of candle maps with atom keys (list)

Example

[
  %{
    close: 103.0,
    high: 105.0,
    low: 98.0,
    open: 100.0,
    timestamp: 1700000000000,
    volume: 42.0
  }
]
# descripex:contract
%{
  params: %{
    rows: %{
      description: "List of [timestamp, open, high, low, close, volume] rows",
      source: "fetch_ohlcv(symbol)",
      kind: :exchange_data
    }
  },
  returns: %{type: :list, description: "List of candle maps with atom keys"},
  returns_example: [
    %{
      close: 103.0,
      high: 105.0,
      low: 98.0,
      open: 100.0,
      timestamp: 1700000000000,
      volume: 42.0
    }
  ]
}

parkinson(candles, opts \\ [])

@spec parkinson(
  [candle()],
  keyword()
) :: volatility() | nil | {:error, :invalid_prices}

Calculate Parkinson volatility estimator from high-low ranges.

Parameters

  • candles - List of candles with :high and :low fields (exchange_data)

Options

  • annualize - Whether to annualize (default: false)
  • trading_days - Trading days per year (default: 365)

Returns

Parkinson volatility estimate, nil if insufficient data, or {:error, :invalid_prices} (float)

Example

0.1095

Errors

  • :invalid_prices
# descripex:contract
%{
  opts: %{
    annualize: %{
      default: false,
      type: :boolean,
      description: "Whether to annualize"
    },
    trading_days: %{
      default: 365,
      type: :integer,
      description: "Trading days per year"
    }
  },
  params: %{
    candles: %{
      description: "List of candles with :high and :low fields",
      source: "fetch_ohlcv(symbol)",
      kind: :exchange_data
    }
  },
  errors: [:invalid_prices],
  returns: %{
    type: :float,
    description: "Parkinson volatility estimate, nil if insufficient data, or {:error, :invalid_prices}"
  },
  returns_example: 0.1095
}

realized(prices, opts \\ [])

@spec realized(
  [number()],
  keyword()
) :: volatility() | nil | {:error, :invalid_prices}

Calculate realized (historical) volatility from close-to-close returns.

Parameters

  • prices - List of prices (chronological order, oldest first) (value)

Options

  • annualize - Whether to annualize the result (default: false)
  • trading_days - Trading days per year for annualization (default: 365)

Returns

Standard deviation of returns, nil if insufficient data, or {:error, :invalid_prices} (float)

Example

0.1095

Errors

  • :invalid_prices
# descripex:contract
%{
  opts: %{
    annualize: %{
      default: false,
      type: :boolean,
      description: "Whether to annualize the result"
    },
    trading_days: %{
      default: 365,
      type: :integer,
      description: "Trading days per year for annualization"
    }
  },
  params: %{
    prices: %{
      description: "List of prices (chronological order, oldest first)",
      kind: :value
    }
  },
  errors: [:invalid_prices],
  returns: %{
    type: :float,
    description: "Standard deviation of returns, nil if insufficient data, or {:error, :invalid_prices}"
  },
  returns_example: 0.1095
}

rolling(prices, window, opts \\ [])

@spec rolling([number()], pos_integer(), keyword()) :: [volatility()]

Calculate rolling volatility over a price series.

Parameters

  • prices - List of prices (chronological order) (value)
  • window - Rolling window size (> 2) (value)

Options

  • annualize - Whether to annualize (default: false)
  • trading_days - Trading days per year (default: 365)

Returns

List of volatility values, empty if insufficient data (list)

Example

[0.38, 0.41]
# descripex:contract
%{
  opts: %{
    annualize: %{
      default: false,
      type: :boolean,
      description: "Whether to annualize"
    },
    trading_days: %{
      default: 365,
      type: :integer,
      description: "Trading days per year"
    }
  },
  params: %{
    window: %{description: "Rolling window size (> 2)", kind: :value},
    prices: %{description: "List of prices (chronological order)", kind: :value}
  },
  returns: %{
    type: :list,
    description: "List of volatility values, empty if insufficient data"
  },
  returns_example: [0.38, 0.41]
}

yang_zhang(candles, opts \\ [])

@spec yang_zhang(
  [candle()],
  keyword()
) :: volatility() | nil | {:error, :invalid_prices}

Calculate Yang-Zhang volatility estimator from OHLC data.

Parameters

  • candles - List of consecutive candles with :open, :high, :low, :close fields (exchange_data)

Options

  • annualize - Whether to annualize (default: false)
  • trading_days - Trading days per year (default: 365)

Returns

Yang-Zhang volatility, nil if insufficient data, or {:error, :invalid_prices} (float)

Example

0.1095

Errors

  • :invalid_prices
# descripex:contract
%{
  opts: %{
    annualize: %{
      default: false,
      type: :boolean,
      description: "Whether to annualize"
    },
    trading_days: %{
      default: 365,
      type: :integer,
      description: "Trading days per year"
    }
  },
  params: %{
    candles: %{
      description: "List of consecutive candles with :open, :high, :low, :close fields",
      source: "fetch_ohlcv(symbol)",
      kind: :exchange_data
    }
  },
  errors: [:invalid_prices],
  returns: %{
    type: :float,
    description: "Yang-Zhang volatility, nil if insufficient data, or {:error, :invalid_prices}"
  },
  returns_example: 0.1095
}