Enhanced Strategy API Guide
View SourceOverview
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/1andnext/1functions - 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
end2. 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 positionsell()- Enter a short positionclose_buy()- Close a long positionclose_sell()- Close a short position
Context Access
data()- Get current market data point (OHLC)equity()- Get current account equityposition()- Get current position (:long,:short, or:none)
Utilities
crossover?(series1, series2)- Check if series1 crosses above series2indicator(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
endMoving 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
endMigration 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
endState 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}
endBenefits
- Intuitive: Clear separation of initialization and execution logic
- Stateful: Easy state management between data points
- Contextual: Built-in access to trading context and position state
- Testable: Easy to unit test individual strategy components
- Composable: Can build complex strategies from simple components
- 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