ExPostFacto

A comprehensive backtesting library for trading strategies written in Elixir.

[!IMPORTANT] This library is under active, pre 1.0 development. The APIs are not to be considered stable. Calculations may not be correct. See the LICENSE but use at your own risk.

ExPostFacto empowers traders and developers to test their trading strategies against historical data with confidence. Built with Elixir's concurrency and fault-tolerance in mind, it provides enterprise-grade backtesting capabilities with an intuitive API.

๐Ÿš€ Why ExPostFacto?

โœจ Key Features

Multiple Input Formats

Data Validation & Cleaning

Flexible Strategy Framework

Performance & Optimization

Comprehensive Analytics

See ENHANCED_DATA_HANDLING_EXAMPLES.md for detailed usage examples.

LiveBook Integration

ExPostFacto works seamlessly with LiveBook for interactive backtesting and analysis:

# In LiveBook, install dependencies:
Mix.install([
  {:ex_post_facto, "~> 0.2.0"},
  {:kino, "~> 0.12.0"},
  {:kino_vega_lite, "~> 0.1.0"}
])

# Run interactive backtests with rich visualizations
{:ok, result} = ExPostFacto.backtest(data, {MyStrategy, :call, []})

See LiveBook Integration Guide for comprehensive examples, interactive forms, and visualization techniques.

๐Ÿ“– Quick Start

Installation

Add ExPostFacto to your mix.exs:

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

Your First Backtest

# 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"}
]

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

# View results
IO.puts("Total return: $#{result.result.total_profit_and_loss}")
IO.puts("Win rate: #{result.result.win_rate}%")

Load Data from CSV

# ExPostFacto automatically handles CSV files
{:ok, result} = ExPostFacto.backtest(
  "path/to/market_data.csv",
  {MyStrategy, :call, []},
  starting_balance: 100_000.0
)

๐ŸŽฏ Strategy Development

Simple Function Strategy (MFA)

defmodule SimpleThresholdStrategy do
  def call(data, _result) do
    if data.close > 105.0, do: :buy, else: :sell
  end
end

{:ok, result} = ExPostFacto.backtest(
  market_data,
  {SimpleThresholdStrategy, :call, []},
  starting_balance: 10_000.0
)

Advanced Strategy Behaviour

defmodule MovingAverageStrategy do
  use ExPostFacto.Strategy

  def init(opts) do
    {:ok, %{
      fast_period: Keyword.get(opts, :fast_period, 10),
      slow_period: Keyword.get(opts, :slow_period, 20),
      price_history: []
    }}
  end

  def next(state) do
    current_price = data().close
    price_history = [current_price | state.price_history]

    if length(price_history) >= state.slow_period do
      fast_sma = indicator(:sma, price_history, state.fast_period)
      slow_sma = indicator(:sma, price_history, state.slow_period)

      if List.first(fast_sma) > List.first(slow_sma) do
        buy()
      else
        sell()
      end
    end

    {:ok, %{state | price_history: price_history}}
  end
end

# Run with custom parameters
{:ok, result} = ExPostFacto.backtest(
  market_data,
  {MovingAverageStrategy, [fast_period: 5, slow_period: 15]},
  starting_balance: 10_000.0
)

๐Ÿ“ˆ Technical Indicators

ExPostFacto includes 20+ built-in technical indicators:

# Available indicators
prices = [100, 101, 102, 103, 104, 105]

sma_20 = indicator(:sma, prices, 20)
ema_12 = indicator(:ema, prices, 12)
rsi_14 = indicator(:rsi, prices, 14)
{macd, signal, histogram} = indicator(:macd, prices)
{bb_upper, bb_middle, bb_lower} = indicator(:bollinger_bands, prices)

# Crossover detection
if crossover?(fast_sma, slow_sma) do
  buy()
end

๐ŸŽ›๏ธ Strategy Optimization

Find optimal parameters automatically:

# Grid search optimization
{:ok, result} = ExPostFacto.optimize(
  market_data,
  MovingAverageStrategy,
  [fast_period: 5..15, slow_period: 20..30],
  maximize: :sharpe_ratio
)

IO.puts("Best parameters: #{inspect(result.best_params)}")
IO.puts("Best Sharpe ratio: #{result.best_score}")

# Walk-forward analysis for robust testing
{:ok, result} = ExPostFacto.optimize(
  market_data,
  MovingAverageStrategy,
  [fast_period: 5..15, slow_period: 20..30],
  method: :walk_forward,
  training_window: 252,  # 1 year
  validation_window: 63  # 3 months
)

๐Ÿงน Data Validation & Cleaning

ExPostFacto ensures your data is clean and valid:

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

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

# Enhanced error handling
{:ok, result} = ExPostFacto.backtest(
  market_data,
  strategy,
  enhanced_validation: true,
  debug: true
)

๐Ÿ“Š Example Strategies

ExPostFacto includes several example strategies:

# Moving Average Crossover
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.SmaStrategy, [fast_period: 10, slow_period: 20]}
)

# RSI Mean Reversion
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.RSIMeanReversionStrategy, [
    rsi_period: 14,
    oversold_threshold: 30,
    overbought_threshold: 70
  ]}
)

# Bollinger Band Strategy
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.BollingerBandStrategy, [period: 20, std_dev: 2.0]}
)

# Breakout Strategy
{:ok, result} = ExPostFacto.backtest(
  data,
  {ExPostFacto.ExampleStrategies.BreakoutStrategy, [
    lookback_period: 20,
    breakout_threshold: 0.02
  ]}
)

๐Ÿ“š Documentation & Learning

Complete Documentation

Data Handling

Advanced Features

๐Ÿ”ง Advanced Features

Streaming for Large Datasets

# Handle massive datasets efficiently
{:ok, result} = ExPostFacto.backtest_stream(
  "very_large_dataset.csv",
  {MyStrategy, []},
  chunk_size: 1000,
  memory_limit_mb: 100
)

Concurrent Optimization

# Leverage all CPU cores for optimization
{:ok, result} = ExPostFacto.optimize(
  data,
  MyStrategy,
  parameter_ranges,
  method: :random_search,
  samples: 1000,
  max_concurrent: System.schedulers_online()
)

Heatmap Visualization

# Generate parameter heatmaps
{:ok, optimization_result} = ExPostFacto.optimize(data, MyStrategy, param_ranges)
{:ok, heatmap} = ExPostFacto.heatmap(optimization_result, :param1, :param2)

# Use heatmap data for visualization
IO.inspect(heatmap.scores)  # 2D array of performance scores

๐Ÿ†š Comparison with Other Libraries

Feature ExPostFacto backtesting.py Backtrader QuantConnect
Language Elixir Python Python C#/Python
Concurrency โœ… Native โŒ โŒ โœ…
Memory Efficiency โœ… Streaming โŒ โŒ โœ…
Data Validation โœ… Built-in โŒ โŒ โœ…
Walk-Forward โœ… โŒ โœ… โœ…
Easy Setup โœ… โœ… โŒ โŒ

๐Ÿค Contributing

We welcome contributions! Please see our contributing guidelines and check out the open issues.

๐Ÿ“„ License

ExPostFacto is released under the MIT License. See LICENSE for details.

๐Ÿ™ Acknowledgments

Inspired by Python's backtesting.py and other excellent backtesting libraries. Built with the power and elegance of Elixir.


Ready to backtest your trading strategies? Get started now! ๐Ÿš€