Migration Guide: From Other Backtesting Libraries to ExPostFacto

View Source

This guide helps you migrate from popular backtesting libraries to ExPostFacto, highlighting the differences and providing code translation examples.

Table of Contents

From Python backtesting.py

Basic Structure Comparison

Python backtesting.py:

from backtesting import Backtest, Strategy
import pandas as pd

class SMAStrategy(Strategy):
    n1 = 10  # Fast SMA
    n2 = 20  # Slow SMA
    
    def init(self):
        self.sma1 = self.I(SMA, self.data.Close, self.n1)
        self.sma2 = self.I(SMA, self.data.Close, self.n2)
    
    def next(self):
        if crossover(self.sma1, self.sma2):
            self.buy()
        elif crossover(self.sma2, self.sma1):
            self.sell()

# Run backtest
data = pd.read_csv('data.csv', index_col=0, parse_dates=True)
bt = Backtest(data, SMAStrategy)
result = bt.run()

ExPostFacto equivalent:

defmodule SMAStrategy do
  use ExPostFacto.Strategy
  
  def init(opts) do
    {:ok, %{
      n1: Keyword.get(opts, :n1, 10),
      n2: Keyword.get(opts, :n2, 20),
      price_history: [],
      sma1_history: [],
      sma2_history: []
    }}
  end
  
  def next(state) do
    current_price = data().close
    price_history = [current_price | state.price_history]
    
    sma1 = indicator(:sma, price_history, state.n1)
    sma2 = indicator(:sma, price_history, state.n2)
    
    sma1_history = [sma1 | state.sma1_history]
    sma2_history = [sma2 | state.sma2_history]
    
    # Check for crossovers
    if crossover?(sma1_history, sma2_history) do
      buy()
    elsif crossover?(sma2_history, sma1_history) do
      sell()
    end
    
    {:ok, %{state | 
      price_history: price_history,
      sma1_history: sma1_history,
      sma2_history: sma2_history
    }}
  end
end

# Run backtest
{:ok, result} = ExPostFacto.backtest(
  "data.csv",
  {SMAStrategy, [n1: 10, n2: 20]},
  starting_balance: 10_000.0
)

Key Differences

backtesting.pyExPostFactoNotes
self.I(indicator, ...)indicator(:name, data, params)Built-in indicators
self.buy()buy()Position management
self.sell()sell()Position management
crossover(a, b)crossover?(a, b)Signal detection
self.data.Closedata().closeData access
Parameters as class attributesParameters in init/1 optsConfiguration

Optimization Comparison

Python backtesting.py:

result = bt.optimize(
    n1=range(5, 20),
    n2=range(20, 50),
    maximize='Sharpe Ratio'
)

ExPostFacto:

{:ok, result} = ExPostFacto.optimize(
  data,
  SMAStrategy,
  [n1: 5..19, n2: 20..49],
  maximize: :sharpe_ratio
)

From Backtrader

Strategy Translation

Backtrader:

import backtrader as bt

class RSIStrategy(bt.Strategy):
    params = (
        ('rsi_period', 14),
        ('rsi_upper', 70),
        ('rsi_lower', 30),
    )
    
    def __init__(self):
        self.rsi = bt.indicators.RSI(period=self.params.rsi_period)
    
    def next(self):
        if not self.position:
            if self.rsi < self.params.rsi_lower:
                self.buy()
        else:
            if self.rsi > self.params.rsi_upper:
                self.sell()

ExPostFacto:

defmodule RSIStrategy do
  use ExPostFacto.Strategy
  
  def init(opts) do
    {:ok, %{
      rsi_period: Keyword.get(opts, :rsi_period, 14),
      rsi_upper: Keyword.get(opts, :rsi_upper, 70),
      rsi_lower: Keyword.get(opts, :rsi_lower, 30),
      price_history: []
    }}
  end
  
  def next(state) do
    price_history = [data().close | state.price_history]
    rsi_values = indicator(:rsi, price_history, state.rsi_period)
    current_rsi = List.first(rsi_values)
    
    current_position = position()
    
    cond do
      current_position == :none and current_rsi < state.rsi_lower ->
        buy()
      current_position == :long and current_rsi > state.rsi_upper ->
        close_buy()
      true ->
        :ok
    end
    
    {:ok, %{state | price_history: price_history}}
  end
end

Position Management

BacktraderExPostFactoDescription
self.buy()buy()Enter long position
self.sell()close_buy()Close long position
self.sell() (short)sell()Enter short position
self.buy() (cover)close_sell()Close short position
self.positionposition()Current position

From Zipline

Algorithm Structure

Zipline:

from zipline.api import order, symbol, record, schedule_function
from zipline.algorithm import TradingAlgorithm

def initialize(context):
    context.asset = symbol('AAPL')
    context.short_window = 10
    context.long_window = 30

def handle_data(context, data):
    short_mavg = data.history(context.asset, 'price', context.short_window, '1d').mean()
    long_mavg = data.history(context.asset, 'price', context.long_window, '1d').mean()
    
    if short_mavg > long_mavg:
        order(context.asset, 100)
    elif short_mavg < long_mavg:
        order(context.asset, -100)

ExPostFacto:

defmodule ZiplinePortStrategy do
  use ExPostFacto.Strategy
  
  def init(opts) do
    {:ok, %{
      short_window: Keyword.get(opts, :short_window, 10),
      long_window: Keyword.get(opts, :long_window, 30),
      price_history: []
    }}
  end
  
  def next(state) do
    price_history = [data().close | state.price_history]
    
    if length(price_history) >= state.long_window do
      short_mavg = calculate_mavg(price_history, state.short_window)
      long_mavg = calculate_mavg(price_history, state.long_window)
      
      cond do
        short_mavg > long_mavg and position() != :long ->
          if position() == :short, do: close_sell()
          buy()
        short_mavg < long_mavg and position() != :short ->
          if position() == :long, do: close_buy()
          sell()
        true ->
          :ok
      end
    end
    
    {:ok, %{state | price_history: price_history}}
  end
  
  defp calculate_mavg(prices, window) do
    prices |> Enum.take(window) |> Enum.sum() |> Kernel./(window)
  end
end

From QuantConnect

Algorithm Conversion

QuantConnect (C#):

public class BasicAlgorithm : QCAlgorithm
{
    private SimpleMovingAverage sma;
    
    public override void Initialize()
    {
        SetStartDate(2020, 1, 1);
        SetEndDate(2021, 1, 1);
        SetCash(100000);
        
        AddEquity("SPY", Resolution.Daily);
        sma = SMA("SPY", 14);
    }
    
    public override void OnData(Slice data)
    {
        if (data.Bars.ContainsKey("SPY"))
        {
            var price = data.Bars["SPY"].Close;
            
            if (price > sma && !Portfolio.Invested)
            {
                SetHoldings("SPY", 1.0);
            }
            else if (price < sma && Portfolio.Invested)
            {
                Liquidate("SPY");
            }
        }
    }
}

ExPostFacto:

defmodule QuantConnectPortStrategy do
  use ExPostFacto.Strategy
  
  def init(opts) do
    {:ok, %{
      sma_period: Keyword.get(opts, :sma_period, 14),
      price_history: []
    }}
  end
  
  def next(state) do
    current_price = data().close
    price_history = [current_price | state.price_history]
    
    if length(price_history) >= state.sma_period do
      sma_value = indicator(:sma, price_history, state.sma_period) |> List.first()
      current_position = position()
      
      cond do
        current_price > sma_value and current_position != :long ->
          if current_position == :short, do: close_sell()
          buy()
        current_price < sma_value and current_position != :none ->
          if current_position == :long, do: close_buy()
          if current_position == :short, do: close_sell()
        true ->
          :ok
      end
    end
    
    {:ok, %{state | price_history: price_history}}
  end
end

From Pine Script

Script Translation

Pine Script:

//@version=5
strategy("SMA Cross", overlay=true)

short_length = input.int(9, "Short SMA Length")
long_length = input.int(21, "Long SMA Length")

short_sma = ta.sma(close, short_length)
long_sma = ta.sma(close, long_length)

if ta.crossover(short_sma, long_sma)
    strategy.entry("Long", strategy.long)

if ta.crossunder(short_sma, long_sma)
    strategy.close("Long")

ExPostFacto:

defmodule PineScriptPortStrategy do
  use ExPostFacto.Strategy
  
  def init(opts) do
    {:ok, %{
      short_length: Keyword.get(opts, :short_length, 9),
      long_length: Keyword.get(opts, :long_length, 21),
      price_history: [],
      short_sma_history: [],
      long_sma_history: []
    }}
  end
  
  def next(state) do
    current_close = data().close
    price_history = [current_close | state.price_history]
    
    short_sma = indicator(:sma, price_history, state.short_length) |> List.first()
    long_sma = indicator(:sma, price_history, state.long_length) |> List.first()
    
    short_sma_history = [short_sma | state.short_sma_history]
    long_sma_history = [long_sma | state.long_sma_history]
    
    # Check for crossovers
    if crossover?(short_sma_history, long_sma_history) do
      buy()
    elsif crossover?(long_sma_history, short_sma_history) do
      close_buy()
    end
    
    {:ok, %{state | 
      price_history: price_history,
      short_sma_history: short_sma_history,
      long_sma_history: long_sma_history
    }}
  end
end

Feature Comparison

Core Features

FeatureExPostFactobacktesting.pyBacktraderZiplineQuantConnect
LanguageElixirPythonPythonPythonC#/Python
Strategy TypesMFA + BehaviourClass-basedClass-basedFunction-basedClass-based
Built-in IndicatorsLimited
Optimization
Walk-Forward
Live Trading
Multi-AssetLimited
Data Cleaning
Concurrent Processing

Syntax Mapping

ConceptExPostFactobacktesting.pyBacktraderPine Script
Buy Signalbuy()self.buy()self.buy()strategy.entry("Long", strategy.long)
Sell Signalsell()self.sell()self.sell()strategy.entry("Short", strategy.short)
Close Longclose_buy()self.sell()self.sell()strategy.close("Long")
Close Shortclose_sell()self.buy()self.buy()strategy.close("Short")
Current Pricedata().closeself.data.Close[-1]self.data.close[0]close
Positionposition()self.positionself.positionstrategy.position_size
SMAindicator(:sma, data, n)self.I(SMA, data, n)bt.indicators.SMA(period=n)ta.sma(close, n)
Crossovercrossover?(a, b)crossover(a, b)a > b and a[-1] <= b[-1]ta.crossover(a, b)

Common Migration Patterns

1. Parameter Configuration

From: Class attributes or function parameters

class Strategy:
    fast_period = 10
    slow_period = 20

To: ExPostFacto init options

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

2. State Management

From: Instance variables

def __init__(self):
    self.price_history = []
    self.signals = []

To: ExPostFacto state map

def init(_opts) do
  {:ok, %{
    price_history: [],
    signals: []
  }}
end

def next(state) do
  new_state = %{state | price_history: updated_history}
  {:ok, new_state}
end

3. Indicator Usage

From: Self-updating indicators

def init(self):
    self.sma = self.I(SMA, self.data.Close, 20)

def next(self):
    current_sma = self.sma[-1]

To: Manual calculation with history

def next(state) do
  price_history = [data().close | state.price_history]
  sma_value = indicator(:sma, price_history, 20) |> List.first()
  {:ok, %{state | price_history: price_history}}
end

4. Optimization

From: Built-in optimize functions

result = bt.optimize(param1=range(5, 15), param2=range(20, 30))

To: ExPostFacto optimize

{:ok, result} = ExPostFacto.optimize(
  data, Strategy,
  [param1: 5..14, param2: 20..29]
)

Migration Checklist

When migrating from other libraries:

  • [ ] Convert class-based strategies to ExPostFacto Strategy behaviour
  • [ ] Translate indicator usage to ExPostFacto indicator framework
  • [ ] Convert position management calls
  • [ ] Adapt parameter configuration to init/1 pattern
  • [ ] Update state management to use immutable state maps
  • [ ] Convert optimization code to ExPostFacto format
  • [ ] Test with same data to verify equivalent results
  • [ ] Update any custom indicators or calculations
  • [ ] Adapt data loading and preprocessing
  • [ ] Review and update risk management logic

Getting Help

If you need help migrating specific strategies or have questions about equivalent functionality:

  1. Check the Strategy API Guide for detailed behaviour documentation
  2. Review Best Practices for recommended patterns
  3. Look at example strategies in lib/ex_post_facto/example_strategies/
  4. Open an issue on GitHub with your specific migration question

The ExPostFacto community is here to help make your migration as smooth as possible!