Getting Started with ExPostFacto

View Source

ExPostFacto is a comprehensive backtesting library for trading strategies written in Elixir. This guide will help you get up and running quickly.

What is Backtesting?

Backtesting is the process of testing a trading strategy on historical data to see how it would have performed. ExPostFacto makes this easy by providing:

  • ๐Ÿ“Š Multiple data formats - CSV, JSON, lists of maps
  • ๐Ÿงน Data validation and cleaning - Automatic handling of messy data
  • ๐Ÿš€ Flexible strategy framework - Both simple functions and advanced behaviours
  • ๐Ÿ“ˆ Comprehensive statistics - Detailed performance metrics
  • โšก Performance optimization - Grid search, random search, walk-forward analysis

Installation

Add ExPostFacto to your mix.exs dependencies:

def deps do
  [
    {:ex_post_facto, "~> 0.2.0"}
  ]
end

Then run:

mix deps.get

Your First Backtest

Let's start with a simple example using some sample market data:

# Define some sample market data
market_data = [
  %{open: 100.0, high: 105.0, low: 98.0, close: 102.0, timestamp: "2023-01-01"},
  %{open: 102.0, high: 108.0, low: 101.0, close: 106.0, timestamp: "2023-01-02"},
  %{open: 106.0, high: 110.0, low: 104.0, close: 108.0, timestamp: "2023-01-03"},
  %{open: 108.0, high: 112.0, low: 107.0, close: 110.0, timestamp: "2023-01-04"},
  %{open: 110.0, high: 115.0, low: 109.0, close: 113.0, timestamp: "2023-01-05"}
]

# Simple buy-and-hold strategy - just buy on the first data point
{:ok, result} = ExPostFacto.backtest(
  market_data,
  {ExPostFacto.ExampleStrategies.SimpleBuyHold, :call, []},
  starting_balance: 10_000.0
)

# View the results
IO.puts("Total return: $#{result.result.total_profit_and_loss}")
IO.puts("Number of trades: #{result.result.trades_count}")

Loading Data from Files

ExPostFacto can automatically load data from CSV files:

# Load data from a CSV file
{:ok, result} = ExPostFacto.backtest(
  "path/to/your/market_data.csv",
  {MyStrategy, :call, []},
  starting_balance: 10_000.0
)

CSV Format

Your CSV should have these columns (case-insensitive):

  • Date or Timestamp - The date/time
  • Open - Opening price
  • High - Highest price
  • Low - Lowest price
  • Close - Closing price
  • Volume (optional) - Trading volume

Example:

Date,Open,High,Low,Close,Volume
2023-01-01,100.0,105.0,98.0,102.0,1000000
2023-01-02,102.0,108.0,101.0,106.0,1200000

Creating Your First Strategy

There are two ways to create strategies in ExPostFacto:

1. Simple Function Strategy (MFA Tuple)

defmodule MySimpleStrategy do
  def call(current_data, result) do
    # Buy if price is above $100
    if current_data.close > 100.0 do
      :buy
    else
      :sell
    end
  end
end

# Use it in a backtest
{:ok, result} = ExPostFacto.backtest(
  market_data,
  {MySimpleStrategy, :call, []},
  starting_balance: 10_000.0
)
defmodule MyAdvancedStrategy do
  use ExPostFacto.Strategy

  def init(opts) do
    threshold = Keyword.get(opts, :threshold, 100.0)
    {:ok, %{threshold: threshold}}
  end

  def next(state) do
    current_price = data().close

    if current_price > state.threshold do
      buy()
    else
      sell()
    end

    {:ok, state}
  end
end

# Use it in a backtest
{:ok, result} = ExPostFacto.backtest(
  market_data,
  {MyAdvancedStrategy, [threshold: 105.0]},
  starting_balance: 10_000.0
)

Understanding Results

Every backtest returns detailed statistics:

{:ok, output} = ExPostFacto.backtest(market_data, strategy)

# Access key metrics
result = output.result

IO.puts("=== Performance Summary ===")
IO.puts("Total P&L: $#{result.total_profit_and_loss}")
IO.puts("Total Return %: #{result.total_return_percentage}%")
IO.puts("Number of Trades: #{result.trades_count}")
IO.puts("Win Rate: #{result.win_rate}%")
IO.puts("Best Trade: #{result.best_trade_percentage}%")
IO.puts("Worst Trade: #{result.worst_trade_percentage}%")
IO.puts("Max Drawdown: #{result.max_draw_down_percentage}%")

# Advanced metrics (if available)
if Map.has_key?(result, :sharpe_ratio) do
  IO.puts("Sharpe Ratio: #{result.sharpe_ratio}")
  IO.puts("CAGR: #{result.cagr_percentage}%")
  IO.puts("Profit Factor: #{result.profit_factor}")
end

Data Validation and Cleaning

ExPostFacto automatically validates and cleans your data:

# Validate data manually
case ExPostFacto.validate_data(market_data) do
  :ok -> IO.puts("Data is valid!")
  {:error, reason} -> IO.puts("Validation error: #{reason}")
end

# Clean messy data
{:ok, clean_data} = ExPostFacto.clean_data(dirty_data)

# Control validation and cleaning in backtests
{:ok, result} = ExPostFacto.backtest(
  market_data,
  strategy,
  validate_data: true,   # Enable validation (default: true)
  clean_data: true       # Enable cleaning (default: true)
)

Error Handling

ExPostFacto provides detailed error messages to help you debug issues:

# Enhanced error handling
case ExPostFacto.backtest(data, strategy, enhanced_validation: true) do
  {:ok, output} ->
    output
  {:error, error} ->
    IO.puts("Backtest failed: #{error}")
    :error
end

Next Steps

Now that you have the basics, explore these advanced features:

  1. Strategy Development - Learn about the advanced Strategy behaviour
  2. Technical Indicators - Use built-in indicators in your strategies
  3. Strategy Optimization - Find optimal parameters for your strategies
  4. Best Practices - Guidelines for effective strategy development
  5. Migration Guide - Moving from other backtesting libraries

Common Issues

"Module not found" errors

Make sure your strategy module is compiled and available. Use iex -S mix to test interactively.

Data validation errors

Check that your OHLC data has valid relationships (high >= low, open/close between high/low).

Empty results

Ensure your strategy is actually generating trading signals. Use debug mode to trace execution:

{:ok, result} = ExPostFacto.backtest(
  data,
  strategy,
  enhanced_validation: true,
  debug: true
)

Example Strategies

ExPostFacto includes several example strategies to get you started:

# Simple moving average crossover
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.SmaStrategy, [fast_period: 10, slow_period: 20]}
)

# MACD strategy
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.AdvancedMacdStrategy, []}
)

# Buy and hold
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.SimpleBuyHold, :call, []}
)

Happy backtesting! ๐Ÿš€