Skip to content

Volatility Analysis Examples

Examples of using volatility indicators like ATR and Bollinger Bands for risk management and trading.

ATR-Based Position Sizing

Use Average True Range to calculate appropriate position sizes.

require 'sqa/tai'

def calculate_position_size(high, low, close, account_value, risk_per_trade_pct: 1.0, atr_multiplier: 2.0)
  # Calculate ATR
  atr = SQA::TAI.atr(high, low, close, period: 14)
  current_atr = atr.last
  current_price = close.last

  # Calculate risk amount
  risk_amount = account_value * (risk_per_trade_pct / 100.0)

  # Stop distance based on ATR
  stop_distance = current_atr * atr_multiplier

  # Calculate position size
  shares = (risk_amount / stop_distance).floor

  # Calculate actual investment
  investment = shares * current_price

  # Calculate percentage of account
  account_pct = (investment / account_value * 100).round(2)

  {
    shares: shares,
    price: current_price.round(2),
    investment: investment.round(2),
    account_pct: account_pct,
    atr: current_atr.round(2),
    stop_distance: stop_distance.round(2),
    stop_price: (current_price - stop_distance).round(2),
    risk_amount: risk_amount.round(2)
  }
end

# Example usage
length = 30
high = Array.new(length) { |i| 100 + i * 0.5 + rand(1.0..3.0) }
low = high.map { |h| h - rand(2.0..4.0) }
close = low.zip(high).map { |l, h| l + (h - l) * rand(0.4..0.6) }

position = calculate_position_size(high, low, close, 100_000, risk_per_trade_pct: 1.0)

puts "ATR-Based Position Sizing:"
puts "=" * 50
puts "Account Value: $100,000"
puts "Risk per Trade: 1% ($#{position[:risk_amount]})"
puts "\nPosition Details:"
puts "Shares: #{position[:shares]}"
puts "Entry Price: $#{position[:price]}"
puts "Investment: $#{position[:investment]} (#{position[:account_pct]}% of account)"
puts "\nRisk Management:"
puts "ATR: $#{position[:atr]}"
puts "Stop Distance: $#{position[:stop_distance]} (2× ATR)"
puts "Stop Loss Price: $#{position[:stop_price]}"

Bollinger Bands Mean Reversion

Trade the extremes of Bollinger Bands for mean reversion.

require 'sqa/tai'

class BollingerBandsStrategy
  def initialize(period: 20, std_dev: 2.0)
    @period = period
    @std_dev = std_dev
  end

  def analyze(prices)
    upper, middle, lower = SQA::TAI.bbands(
      prices,
      period: @period,
      nbdev_up: @std_dev,
      nbdev_down: @std_dev
    )

    current_price = prices.last
    upper_val = upper.last
    middle_val = middle.last
    lower_val = lower.last

    # Calculate %B (position within bands)
    percent_b = ((current_price - lower_val) / (upper_val - lower_val) * 100).round(2)

    # Calculate bandwidth
    bandwidth = ((upper_val - lower_val) / middle_val * 100).round(2)

    # Generate signal
    signal = if current_price <= lower_val
      :buy  # Price at/below lower band - oversold
    elsif current_price >= upper_val
      :sell  # Price at/above upper band - overbought
    elsif percent_b >= 45 && percent_b <= 55
      :close  # Near middle band - take profits
    else
      :hold
    end

    {
      signal: signal,
      price: current_price.round(2),
      upper: upper_val.round(2),
      middle: middle_val.round(2),
      lower: lower_val.round(2),
      percent_b: percent_b,
      bandwidth: bandwidth
    }
  end

  def backtest(prices, initial_capital: 10_000)
    capital = initial_capital
    position = 0
    entry_price = 0
    trades = []

    ([@period, prices.length - 1].max..prices.length - 1).each do |i|
      current_prices = prices[0..i]
      analysis = analyze(current_prices)

      case analysis[:signal]
      when :buy
        if position == 0
          shares = (capital / analysis[:price]).floor
          position = shares
          entry_price = analysis[:price]
          capital -= shares * analysis[:price]

          trades << {
            action: :buy,
            price: analysis[:price],
            shares: shares,
            percent_b: analysis[:percent_b]
          }
        end
      when :sell, :close
        if position > 0
          exit_value = position * analysis[:price]
          profit = (analysis[:price] - entry_price) * position
          capital += exit_value

          trades << {
            action: :sell,
            price: analysis[:price],
            shares: position,
            profit: profit.round(2),
            percent_b: analysis[:percent_b]
          }

          position = 0
        end
      end
    end

    # Close open position
    if position > 0
      capital += position * prices.last
    end

    {
      initial_capital: initial_capital,
      final_capital: capital.round(2),
      return_pct: ((capital / initial_capital - 1) * 100).round(2),
      num_trades: trades.length,
      trades: trades
    }
  end
end

# Example: Generate mean-reverting prices
prices = [100.0]
50.times do |i|
  # Mean-reverting random walk
  mean = 100
  prices << prices.last + (mean - prices.last) * 0.1 + rand(-3.0..3.0)
end

strategy = BollingerBandsStrategy.new(period: 20, std_dev: 2.0)

# Current analysis
current = strategy.analyze(prices)
puts "Bollinger Bands Analysis:"
puts "Signal: #{current[:signal]}"
puts "Price: $#{current[:price]}"
puts "Upper Band: $#{current[:upper]}"
puts "Middle Band: $#{current[:middle]}"
puts "Lower Band: $#{current[:lower]}"
puts "%B: #{current[:percent_b]}%"
puts "Bandwidth: #{current[:bandwidth]}%"

# Backtest
puts "\nBacktest Results:"
results = strategy.backtest(prices)
puts "Return: #{results[:return_pct]}%"
puts "Trades: #{results[:num_trades]}"

Volatility Breakout Detection

Identify when volatility expands after consolidation.

require 'sqa/tai'

def detect_volatility_breakout(high, low, close, lookback: 20)
  atr = SQA::TAI.atr(high, low, close, period: 14)

  # Calculate ATR statistics
  recent_atr = atr.compact.last(lookback)
  avg_atr = recent_atr.sum / recent_atr.length
  min_atr = recent_atr.min
  current_atr = atr.last

  # Volatility squeeze: ATR near lows
  in_squeeze = current_atr < avg_atr * 0.7

  # Volatility expansion: ATR breaking out
  breakout = current_atr > avg_atr * 1.5

  # Calculate price movement
  price_range_pct = ((high.last - low.last) / close.last * 100).round(2)

  status = if breakout
    :expanding
  elsif in_squeeze
    :contracting
  else
    :normal
  end

  {
    status: status,
    current_atr: current_atr.round(2),
    avg_atr: avg_atr.round(2),
    atr_ratio: (current_atr / avg_atr).round(2),
    price_range_pct: price_range_pct,
    action: determine_action(status, price_range_pct, close[-2], close[-1])
  }
end

def determine_action(status, range_pct, prev_close, current_close)
  if status == :contracting
    "Watch for breakout - volatility compressing"
  elsif status == :expanding && current_close > prev_close
    "Bullish breakout - consider long positions"
  elsif status == :expanding && current_close < prev_close
    "Bearish breakout - consider short positions"
  else
    "Normal volatility - no specific action"
  end
end

# Generate data with volatility squeeze then expansion
high = Array.new(30) { |i| 100 + rand(0.5..1.5) }  # Low volatility
high += Array.new(10) { |i| 101 + i * 2 + rand(1.0..4.0) }  # Expanding

low = high.map.with_index { |h, i| i < 30 ? h - rand(0.5..1.0) : h - rand(2.0..5.0) }
close = low.zip(high).map { |l, h| l + (h - l) * rand(0.3..0.7) }

result = detect_volatility_breakout(high, low, close, lookback: 20)

puts "Volatility Breakout Detection:"
puts "Status: #{result[:status]}"
puts "Current ATR: $#{result[:current_atr]}"
puts "Average ATR: $#{result[:avg_atr]}"
puts "ATR Ratio: #{result[:atr_ratio]}x"
puts "Price Range: #{result[:price_range_pct]}%"
puts "\nRecommendation: #{result[:action]}"

ATR Trailing Stop Loss

Implement a dynamic stop loss that adapts to volatility.

require 'sqa/tai'

class ATRTrailingStop
  def initialize(atr_multiplier: 2.0)
    @atr_multiplier = atr_multiplier
    @stop_loss = nil
  end

  def calculate_stop(high, low, close, position_type: :long)
    atr = SQA::TAI.atr(high, low, close, period: 14)
    current_atr = atr.last
    current_price = close.last

    if position_type == :long
      # Long position: stop below price
      new_stop = current_price - (current_atr * @atr_multiplier)

      # Only move stop up, never down
      @stop_loss = [@stop_loss || new_stop, new_stop].max
    else
      # Short position: stop above price
      new_stop = current_price + (current_atr * @atr_multiplier)

      # Only move stop down, never up
      @stop_loss = [@stop_loss || new_stop, new_stop].min
    end

    {
      current_price: current_price.round(2),
      stop_loss: @stop_loss.round(2),
      distance: ((current_price - @stop_loss).abs).round(2),
      distance_pct: (((current_price - @stop_loss).abs / current_price) * 100).round(2),
      atr: current_atr.round(2)
    }
  end

  def reset
    @stop_loss = nil
  end
end

# Simulate a trending market with trailing stop
length = 50
high = Array.new(length) { |i| 100 + i * 0.8 + rand(0.5..2.0) }
low = high.map { |h| h - rand(1.0..3.0) }
close = low.zip(high).map { |l, h| l + (h - l) * rand(0.4..0.6) }

trailing_stop = ATRTrailingStop.new(atr_multiplier: 2.0)

puts "ATR Trailing Stop Example (Long Position):"
puts "=" * 50

# Show trailing stop every 10 bars
[10, 20, 30, 40, 50].each do |bar|
  h = high[0..bar]
  l = low[0..bar]
  c = close[0..bar]

  stop_info = trailing_stop.calculate_stop(h, l, c, position_type: :long)

  puts "\nBar #{bar}:"
  puts "Price: $#{stop_info[:current_price]}"
  puts "Trailing Stop: $#{stop_info[:stop_loss]}"
  puts "Distance: $#{stop_info[:distance]} (#{stop_info[:distance_pct]}%)"
  puts "ATR: $#{stop_info[:atr]}"
end

Bollinger Band Squeeze

Detect low volatility periods that often precede large moves.

require 'sqa/tai'

def detect_bb_squeeze(prices, lookback: 20)
  upper, middle, lower = SQA::TAI.bbands(prices, period: 20)

  # Calculate bandwidth
  bandwidths = []
  upper.each_with_index do |u, i|
    next unless u && middle[i] && lower[i]
    bw = (u - lower[i]) / middle[i] * 100
    bandwidths << bw
  end

  return nil if bandwidths.empty?

  # Current bandwidth
  current_bw = bandwidths.last

  # Historical bandwidth stats
  recent_bw = bandwidths.last(lookback)
  avg_bw = recent_bw.sum / recent_bw.length
  min_bw = recent_bw.min

  # Squeeze: bandwidth in lowest 20%
  squeeze_threshold = avg_bw * 0.8
  in_squeeze = current_bw < squeeze_threshold

  # Breaking out of squeeze
  breaking_out = bandwidths[-2] < squeeze_threshold && current_bw >= squeeze_threshold

  {
    in_squeeze: in_squeeze,
    breaking_out: breaking_out,
    current_bandwidth: current_bw.round(2),
    avg_bandwidth: avg_bw.round(2),
    bandwidth_percentile: ((current_bw / avg_bw) * 100).round(0),
    price: prices.last.round(2),
    upper_band: upper.last.round(2),
    lower_band: lower.last.round(2)
  }
end

# Generate data with squeeze
prices = [100.0]
# Consolidation phase (squeeze)
20.times { prices << prices.last + rand(-0.5..0.5) }
# Breakout phase
15.times { |i| prices << prices.last + 1.5 + rand(-0.5..0.5) }

squeeze_info = detect_bb_squeeze(prices, lookback: 20)

puts "Bollinger Band Squeeze Analysis:"
puts "=" * 50
puts "In Squeeze: #{squeeze_info[:in_squeeze] ? 'Yes' : 'No'}"
puts "Breaking Out: #{squeeze_info[:breaking_out] ? 'YES - Take action!' : 'No'}"
puts "Current Bandwidth: #{squeeze_info[:current_bandwidth]}%"
puts "Average Bandwidth: #{squeeze_info[:avg_bandwidth]}%"
puts "Bandwidth Percentile: #{squeeze_info[:bandwidth_percentile]}%"
puts "\nPrice: $#{squeeze_info[:price]}"
puts "Upper Band: $#{squeeze_info[:upper_band]}"
puts "Lower Band: $#{squeeze_info[:lower_band]}"

if squeeze_info[:in_squeeze]
  puts "\n⚠️  Squeeze active - expect breakout soon!"
elsif squeeze_info[:breaking_out]
  puts "\n🚀 Breakout in progress - enter trade!"
end

Complete Volatility-Based Trading System

Combine multiple volatility indicators for a complete system.

require 'sqa/tai'

class VolatilityTradingSystem
  def analyze(high, low, close, prices)
    # Calculate indicators
    atr = SQA::TAI.atr(high, low, close, period: 14)
    upper, middle, lower = SQA::TAI.bbands(prices, period: 20)

    # ATR analysis
    recent_atr = atr.compact.last(20)
    avg_atr = recent_atr.sum / recent_atr.length
    atr_ratio = atr.last / avg_atr

    # Bollinger Bands analysis
    bandwidth = (upper.last - lower.last) / middle.last * 100
    percent_b = (close.last - lower.last) / (upper.last - lower.last) * 100

    # Volatility state
    volatility_state = if atr_ratio < 0.7
      :low
    elsif atr_ratio > 1.5
      :high
    else
      :normal
    end

    # Generate signals
    signal = generate_signal(
      volatility_state,
      close.last,
      upper.last,
      middle.last,
      lower.last,
      percent_b,
      bandwidth
    )

    {
      signal: signal,
      volatility: volatility_state,
      atr: atr.last.round(2),
      atr_ratio: atr_ratio.round(2),
      bandwidth: bandwidth.round(2),
      percent_b: percent_b.round(0),
      price: close.last.round(2),
      upper: upper.last.round(2),
      middle: middle.last.round(2),
      lower: lower.last.round(2)
    }
  end

  private

  def generate_signal(vol_state, price, upper, middle, lower, percent_b, bandwidth)
    if vol_state == :low && bandwidth < 10
      {
        action: :wait,
        reason: "Low volatility squeeze - wait for breakout",
        setup: "Prepare for volatility expansion"
      }
    elsif vol_state == :high && price > upper
      {
        action: :sell,
        reason: "High volatility + overbought",
        setup: "Mean reversion trade"
      }
    elsif vol_state == :high && price < lower
      {
        action: :buy,
        reason: "High volatility + oversold",
        setup: "Mean reversion trade"
      }
    elsif vol_state == :normal && percent_b < 5
      {
        action: :buy,
        reason: "Price at lower band in normal volatility",
        setup: "Mean reversion with tight stop"
      }
    elsif vol_state == :normal && percent_b > 95
      {
        action: :sell,
        reason: "Price at upper band in normal volatility",
        setup: "Mean reversion with tight stop"
      }
    else
      {
        action: :hold,
        reason: "No clear setup",
        setup: "Monitor for opportunities"
      }
    end
  end
end

# Generate sample data
length = 50
high = Array.new(length) { |i| 100 + Math.sin(i * 0.15) * 10 + i * 0.2 + rand(0.5..2.0) }
low = high.map { |h| h - rand(1.5..3.5) }
close = low.zip(high).map { |l, h| l + (h - l) * rand(0.3..0.7) }

system = VolatilityTradingSystem.new
analysis = system.analyze(high, low, close, close)

puts "Volatility Trading System Analysis:"
puts "=" * 50
puts "Volatility State: #{analysis[:volatility]}"
puts "ATR: $#{analysis[:atr]} (#{analysis[:atr_ratio]}x average)"
puts "Bandwidth: #{analysis[:bandwidth]}%"
puts "%B: #{analysis[:percent_b]}%"
puts "\nPrice: $#{analysis[:price]}"
puts "Upper Band: $#{analysis[:upper]}"
puts "Middle Band: $#{analysis[:middle]}"
puts "Lower Band: $#{analysis[:lower]}"
puts "\nSignal:"
puts "Action: #{analysis[:signal][:action]}"
puts "Reason: #{analysis[:signal][:reason]}"
puts "Setup: #{analysis[:signal][:setup]}"

See Also