Enhanced Strategy API Guide

View Source

Overview

The Enhanced Strategy API provides a more intuitive and powerful way to develop trading strategies for ExPostFacto. Instead of the traditional MFA tuple approach, strategies now implement a behaviour with clear init/1 and next/1 callbacks, along with built-in helper functions and access to trading context.

Key Features

  • Intuitive Callbacks: Simple init/1 and next/1 functions
  • Built-in Helpers: buy(), sell(), close_buy(), close_sell()
  • Context Access: data(), equity(), position() functions
  • Indicator Support: Framework for technical indicators
  • Backward Compatible: Existing MFA strategies continue to work

Basic Usage

1. Creating a Strategy

defmodule MyStrategy do
  use ExPostFacto.Strategy

  def init(opts) do
    # Initialize strategy state
    {:ok, %{}}
  end

  def next(state) do
    # Make trading decisions
    current_price = data().close
    if current_price > 10.0 do
      buy()
    end
    {:ok, state}
  end
end

2. Running a Backtest

# New Strategy behaviour approach
{:ok, result} = ExPostFacto.backtest(
  market_data, 
  {MyStrategy, [param: 10]},
  starting_balance: 10_000.0
)

# Traditional MFA approach (still supported)
{:ok, result} = ExPostFacto.backtest(
  market_data,
  {MyModule, :my_function, []},
  starting_balance: 10_000.0
)

Available Helper Functions

Trading Actions

  • buy() - Enter a long position
  • sell() - Enter a short position
  • close_buy() - Close a long position
  • close_sell() - Close a short position

Context Access

  • data() - Get current market data point (OHLC)
  • equity() - Get current account equity
  • position() - Get current position (:long, :short, or :none)

Utilities

  • crossover?(series1, series2) - Check if series1 crosses above series2
  • indicator(func, data, period) - Basic indicator calculation

Example Strategies

Simple Buy and Hold

defmodule SimpleBuyHold do
  use ExPostFacto.Strategy

  def init(opts) do
    max_trades = Keyword.get(opts, :max_trades, 1)
    {:ok, %{trades_made: 0, max_trades: max_trades}}
  end

  def next(state) do
    if state.trades_made < state.max_trades and position() == :none do
      buy()
      {:ok, %{state | trades_made: state.trades_made + 1}}
    else
      {:ok, state}
    end
  end
end

Moving Average Crossover

defmodule SmaStrategy do
  use ExPostFacto.Strategy

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

  def next(state) do
    current_price = data().close
    updated_history = [current_price | state.price_history]
    
    fast_sma = calculate_sma(updated_history, state.fast_period)
    slow_sma = calculate_sma(updated_history, state.slow_period)
    
    # Trading logic based on SMA crossover
    make_trading_decision(fast_sma, slow_sma)
    
    {:ok, %{state | price_history: updated_history}}
  end

  defp calculate_sma(prices, period) do
    if length(prices) >= period do
      prices |> Enum.take(period) |> Enum.sum() |> Kernel./(period)
    else
      0.0
    end
  end

  defp make_trading_decision(fast_sma, slow_sma) do
    cond do
      fast_sma > slow_sma and position() != :long ->
        if position() == :short, do: close_sell()
        buy()
      
      fast_sma < slow_sma and position() != :short ->
        if position() == :long, do: close_buy()
        sell()
        
      true -> :ok
    end
  end
end

Migration from MFA Tuples

Before (MFA Tuple)

defmodule OldStrategy do
  def my_strategy(data_point, result) do
    if data_point.close > 10.0 do
      :buy
    else
      :noop
    end
  end
end

# Usage
ExPostFacto.backtest(data, {OldStrategy, :my_strategy, []})

After (Strategy Behaviour)

defmodule NewStrategy do
  use ExPostFacto.Strategy

  def init(_opts), do: {:ok, %{}}

  def next(state) do
    if data().close > 10.0 do
      buy()
    end
    {:ok, state}
  end
end

# Usage  
ExPostFacto.backtest(data, {NewStrategy, []})

Advanced Features

Error Handling

def init(opts) do
  case validate_options(opts) do
    :ok -> {:ok, initial_state}
    {:error, reason} -> {:error, reason}
  end
end

def next(state) do
  try do
    # Strategy logic
    {:ok, new_state}
  rescue
    e -> {:error, e}
  end
end

State Management

def next(state) do
  # Access and update strategy state
  new_state = %{
    state | 
    trade_count: state.trade_count + 1,
    last_price: data().close
  }
  {:ok, new_state}
end

Benefits

  1. Intuitive: Clear separation of initialization and execution logic
  2. Stateful: Easy state management between data points
  3. Contextual: Built-in access to trading context and position state
  4. Testable: Easy to unit test individual strategy components
  5. Composable: Can build complex strategies from simple components
  6. Compatible: Works alongside existing MFA tuple strategies

Implementation Notes

  • The Strategy behaviour is implemented using Elixir behaviours and callbacks
  • StrategyContext uses an Agent for state management during execution
  • Helper functions provide a clean API for common trading operations
  • The system automatically detects strategy type (MFA vs behaviour) and routes accordingly
  • All existing functionality and APIs remain unchanged for backward compatibility