Indicator Framework Guide
View SourceExPostFacto now includes a comprehensive technical indicator framework that makes it easy to build sophisticated trading strategies.
Available Indicators
Simple Moving Average (SMA)
prices = [10, 11, 12, 13, 14, 15]
sma_values = ExPostFacto.Indicators.sma(prices, 3)
# => [nil, nil, 11.0, 12.0, 13.0, 14.0]Exponential Moving Average (EMA)
prices = [22.27, 22.19, 22.08, 22.17, 22.18]
ema_values = ExPostFacto.Indicators.ema(prices, 3)
# EMA gives more weight to recent pricesRelative Strength Index (RSI)
prices = [44, 44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.85, 46.08, 45.89]
rsi_values = ExPostFacto.Indicators.rsi(prices, 14)
# Values range from 0-100, indicating overbought/oversold conditionsMACD (Moving Average Convergence Divergence)
prices = [12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
{macd_line, signal_line, histogram} = ExPostFacto.Indicators.macd(prices)
# Returns three series: MACD line, signal line, and histogramBollinger Bands
prices = [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
{upper_band, middle_band, lower_band} = ExPostFacto.Indicators.bollinger_bands(prices, 5, 2)
# Returns upper band, middle band (SMA), and lower bandAverage True Range (ATR)
ohlc_data = [
%{high: 48.70, low: 47.79, close: 48.16},
%{high: 48.72, low: 48.14, close: 48.61},
%{high: 48.90, low: 48.39, close: 48.75}
]
atr_values = ExPostFacto.Indicators.atr(ohlc_data, 14)
# Measures volatilityCrossover Detection
Basic Crossover
fast_ma = [10, 11, 12, 13, 14]
slow_ma = [12, 12, 12, 12, 12]
# Check if fast MA crosses above slow MA
crossover = ExPostFacto.Indicators.crossover?(fast_ma, slow_ma)
# => true
# Check if fast MA crosses below slow MA
crossunder = ExPostFacto.Indicators.crossunder?(fast_ma, slow_ma)
# => falseUsing Indicators in Strategies
Strategy Module Integration
defmodule MyMacdStrategy do
use ExPostFacto.Strategy
def init(_opts) do
{:ok, %{price_history: []}}
end
def next(state) do
current_data = data()
current_price = current_data.close
# Update price history
price_history = [current_price | state.price_history]
# Calculate MACD using the indicator framework
{macd_line, signal_line, _histogram} = indicator(:macd, price_history)
# Make trading decisions based on MACD crossover
if crossover?(macd_line, signal_line) do
buy()
elsif crossunder?(macd_line, signal_line) do
sell()
end
{:ok, %{state | price_history: price_history}}
end
endDirect Indicator Access
# Within a strategy, you can call indicators directly:
sma_20 = indicator(:sma, price_data, 20)
ema_12 = indicator(:ema, price_data, 12)
rsi_14 = indicator(:rsi, price_data, 14)
# MACD with custom parameters
{macd, signal, hist} = indicator(:macd, price_data, {12, 26, 9})
# Bollinger Bands with custom parameters
{upper, middle, lower} = indicator(:bollinger_bands, price_data, {20, 2.5})Stream Support
All indicators work efficiently with Elixir streams for memory-efficient processing:
large_dataset
|> Stream.map(&(&1.close))
|> ExPostFacto.Indicators.sma(20)
|> Stream.filter(&(&1 != nil))
|> Enum.take(100)Advanced Example Strategy
Here's a comprehensive strategy using multiple indicators:
defmodule AdvancedStrategy do
use ExPostFacto.Strategy
def init(_opts) do
{:ok, %{price_history: []}}
end
def next(state) do
price = data().close
history = [price | state.price_history] |> Enum.take(100)
# Calculate multiple indicators
{macd, signal, _} = indicator(:macd, history)
rsi = indicator(:rsi, history) |> List.first()
{bb_upper, bb_middle, bb_lower} = indicator(:bollinger_bands, history)
sma_50 = indicator(:sma, history, 50) |> List.first()
current_price = List.first(history)
# Complex trading logic
cond do
# Bullish conditions
crossover?(macd, signal) and
rsi < 70 and
current_price <= List.first(bb_lower) * 1.02 ->
buy()
# Bearish conditions
crossunder?(macd, signal) and
rsi > 30 and
current_price >= List.first(bb_upper) * 0.98 ->
sell()
true ->
:no_action
end
{:ok, %{state | price_history: history}}
end
endPerformance Considerations
- Indicators work with streams for memory efficiency
- Keep price history limited to what you need for calculations
- Use
Enum.take/2to limit historical data size - All indicators handle
nilvalues gracefully - Crossover functions require at least 2 data points
Extending the Framework
You can create custom indicators by following the same patterns:
defmodule MyCustomIndicator do
def my_indicator(data, period) do
data
|> Stream.with_index()
|> Stream.map(fn {value, index} ->
if index + 1 >= period do
# Your custom calculation here
calculate_custom_value(data, index, period)
else
nil
end
end)
|> Enum.to_list()
end
endThe indicator framework provides a solid foundation for building sophisticated trading strategies with minimal code and maximum flexibility.