docs/MIGRATION_GUIDE.md

# Migration Guide: From Other Backtesting Libraries to ExPostFacto

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](#from-python-backtestingpy)
- [From Backtrader](#from-backtrader)
- [From Zipline](#from-zipline)  
- [From QuantConnect](#from-quantconnect)
- [From Pine Script](#from-pine-script)
- [Feature Comparison](#feature-comparison)
- [Common Migration Patterns](#common-migration-patterns)

## From Python backtesting.py

### Basic Structure Comparison

**Python backtesting.py:**
```python
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:**
```elixir
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.py | ExPostFacto | Notes |
|----------------|-------------|-------|
| `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.Close` | `data().close` | Data access |
| Parameters as class attributes | Parameters in `init/1` opts | Configuration |

### Optimization Comparison

**Python backtesting.py:**
```python
result = bt.optimize(
    n1=range(5, 20),
    n2=range(20, 50),
    maximize='Sharpe Ratio'
)
```

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

## From Backtrader

### Strategy Translation

**Backtrader:**
```python
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:**
```elixir
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

| Backtrader | ExPostFacto | Description |
|------------|-------------|-------------|
| `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.position` | `position()` | Current position |

## From Zipline

### Algorithm Structure

**Zipline:**
```python
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:**
```elixir
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#):**
```csharp
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:**
```elixir
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:**
```pine
//@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:**
```elixir
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

| Feature | ExPostFacto | backtesting.py | Backtrader | Zipline | QuantConnect |
|---------|-------------|----------------|------------|---------|--------------|
| **Language** | Elixir | Python | Python | Python | C#/Python |
| **Strategy Types** | MFA + Behaviour | Class-based | Class-based | Function-based | Class-based |
| **Built-in Indicators** | ✅ | ✅ | ✅ | Limited | ✅ |
| **Optimization** | ✅ | ✅ | ✅ | ❌ | ✅ |
| **Walk-Forward** | ✅ | ❌ | ✅ | ❌ | ✅ |
| **Live Trading** | ❌ | ❌ | ✅ | ❌ | ✅ |
| **Multi-Asset** | Limited | ✅ | ✅ | ✅ | ✅ |
| **Data Cleaning** | ✅ | ❌ | ❌ | ✅ | ✅ |
| **Concurrent Processing** | ✅ | ❌ | ❌ | ❌ | ✅ |

### Syntax Mapping

| Concept | ExPostFacto | backtesting.py | Backtrader | Pine Script |
|---------|-------------|----------------|------------|-------------|
| **Buy Signal** | `buy()` | `self.buy()` | `self.buy()` | `strategy.entry("Long", strategy.long)` |
| **Sell Signal** | `sell()` | `self.sell()` | `self.sell()` | `strategy.entry("Short", strategy.short)` |
| **Close Long** | `close_buy()` | `self.sell()` | `self.sell()` | `strategy.close("Long")` |
| **Close Short** | `close_sell()` | `self.buy()` | `self.buy()` | `strategy.close("Short")` |
| **Current Price** | `data().close` | `self.data.Close[-1]` | `self.data.close[0]` | `close` |
| **Position** | `position()` | `self.position` | `self.position` | `strategy.position_size` |
| **SMA** | `indicator(:sma, data, n)` | `self.I(SMA, data, n)` | `bt.indicators.SMA(period=n)` | `ta.sma(close, n)` |
| **Crossover** | `crossover?(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
```python
class Strategy:
    fast_period = 10
    slow_period = 20
```

**To:** ExPostFacto init options
```elixir
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
```python
def __init__(self):
    self.price_history = []
    self.signals = []
```

**To:** ExPostFacto state map
```elixir
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
```python
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
```elixir
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
```python
result = bt.optimize(param1=range(5, 15), param2=range(20, 30))
```

**To:** ExPostFacto optimize
```elixir
{: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](STRATEGY_API.md) for detailed behaviour documentation
2. Review [Best Practices](BEST_PRACTICES.md) 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!