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.
- ๐ฏ Easy to Use: Simple API that gets you backtesting in minutes
- ๐ Professional Grade: Comprehensive statistics and performance metrics
- ๐ง Flexible: Support for simple functions or advanced strategy behaviours
- โก Fast: Concurrent optimization for large parameter spaces
- ๐งน Robust: Built-in data validation, cleaning, and error handling
- ๐ Growing: 6 technical indicators with more on the roadmap
- CSV files - Load data directly from CSV files
- Lists of maps - Use runtime data structures
- JSON - ๐บ๏ธ Roadmap
- Streaming - Handle large datasets with chunked processing
- Comprehensive OHLCV validation with detailed error messages
- Automatic data cleaning - Remove invalid points, sort by timestamp
- Enhanced timestamp handling - Support for multiple date formats
- Duplicate detection and removal
- Simple MFA functions for quick prototypes
- Advanced Strategy behaviour with state management
- Built-in helper functions -
buy(),sell(),position(), etc. - 6 technical indicators - SMA, EMA, RSI, MACD, Bollinger Bands, ATR (more on the roadmap)
- Parameter optimization with grid search, random search, walk-forward analysis
- Concurrent processing for large parameter spaces
- Chunked streaming for large datasets via
backtest_stream/3
- 45+ result fields - Total P&L, win rate, drawdown, trade duration, and more
- Financial ratios - Sharpe, Sortino, Calmar, CAGR, profit factor
- System quality - SQN, Kelly criterion, expectancy
- Risk metrics - Drawdown analysis, volatility (market risk metrics use simplified estimates)
See ENHANCED_DATA_HANDLING_EXAMPLES.md for detailed usage examples.
ExPostFacto can be used with LiveBook for interactive backtesting and analysis:
# In LiveBook, install dependencies:
Mix.install([
{:ex_post_facto, "~> 0.2.0"}
])
# Run interactive backtests
{:ok, result} = ExPostFacto.backtest(data, {MyStrategy, :call, []})See LiveBook Integration Guide for examples.
Add ExPostFacto to your mix.exs:
def deps do
[
{:ex_post_facto, "~> 0.2.0"}
]
endExPostFacto tracks round-trip trades โ a :buy paired with a :close_buy โ so
your strategy needs to both enter and exit positions for results to appear.
# 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 threshold strategy: buy when cheap, close when above target
defmodule SimpleThresholdStrategy do
def call(data, _result) do
if data.close > 105.0, do: :close_buy, else: :buy
end
end
{:ok, result} = ExPostFacto.backtest(
market_data,
{SimpleThresholdStrategy, :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}%")# ExPostFacto automatically handles CSV files
{:ok, result} = ExPostFacto.backtest(
"path/to/market_data.csv",
{MyStrategy, :call, []},
starting_balance: 100_000.0
)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
)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
)ExPostFacto includes 6 built-in technical indicators (more on the roadmap):
# 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)
atr = indicator(:atr, candles, 14)
# Crossover detection
if crossover?(fast_sma, slow_sma) do
buy()
endFind 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
)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
)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
]}
)- Getting Started Guide - Step-by-step introduction
- Interactive Tutorial - Livebook tutorial with examples
- Strategy API Guide - Comprehensive strategy development
- Technical Indicators - All available indicators and usage
- Best Practices - Guidelines for effective strategies
- Migration Guide - Moving from other libraries
- Enhanced Data Handling - Data formats and validation
- Error Handling - Debugging and validation
- Optimization Guide - Parameter optimization techniques
- Comprehensive Metrics - Performance analysis
# Handle large datasets with chunked processing
{:ok, result} = ExPostFacto.backtest_stream(
"very_large_dataset.csv",
{MyStrategy, :call, []},
chunk_size: 1000
)# Leverage all CPU cores for optimization
{:ok, result} = ExPostFacto.optimize(
data,
MyStrategy,
parameter_ranges,
method: :random_search,
samples: 1000,
max_concurrent: System.schedulers_online()
)# Generate parameter heatmaps from optimization results
{: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| Feature | ExPostFacto | backtesting.py | Backtrader | QuantConnect |
|---|---|---|---|---|
| Language | Elixir | Python | Python | C#/Python |
| Concurrency | โ Native | โ | โ | โ |
| Data Validation | โ Built-in | โ | โ | โ |
| Walk-Forward | โ | โ | โ | โ |
| Easy Setup | โ | โ | โ | โ |
| Indicators | 6 | 100+ | 100+ | 100+ |
We welcome contributions! Please see our contributing guidelines and check out the open issues.
ExPostFacto is released under the MIT License. See LICENSE for details.
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! ๐