IBKR Backtesting Framework
View SourceA comprehensive backtesting framework for the IBKR API Elixir library. Test your trading strategies against historical market data with detailed performance analytics.
Overview
The backtesting framework consists of several key modules:
Backtester.Bar- Historical market data structureBacktester.Portfolio- Portfolio management and P&L trackingBacktester.Strategy- Behavior for implementing trading strategiesBacktester.Engine- Backtesting execution engineBacktester.Example- Usage examples and utilities
Quick Start
1. Basic Backtest Example
# Run a simple moving average strategy on Apple stock
{:ok, result} = Backtester.Example.run_backtest("AAPL", "3mo", "1day")
# Print detailed report
Backtester.Example.print_report(result)2. Sample Backtest (No API Required)
# Test with synthetic data
result = Backtester.Example.run_sample_backtest(
strategy: Backtester.Strategies.MA,
strategy_opts: [window: 20]
)
Backtester.Example.print_report(result)Detailed Usage
Fetching Historical Data
# Get historical data from IBKR API
alias IbkrApi.ClientPortal.MarketData
{:ok, bars} = MarketData.get_historical_data("265598", "1mo", "1day")
# Convert to backtester format
backtest_bars = Enum.map(bars, &Backtester.Bar.from_ibkr/1)Running a Backtest
alias Backtester.{Engine, Strategies}
# Basic backtest
result = Engine.run(backtest_bars, Strategies.MA,
starting_cash: 50_000.0,
strategy_opts: [window: 10]
)
# Detailed backtest with bar-by-bar tracking
detailed_result = Engine.run_detailed(backtest_bars, Strategies.MA)Creating Custom Strategies
defmodule MyStrategy do
use Backtester.Strategy
def init(opts) do
%{
short_window: Keyword.get(opts, :short_window, 5),
long_window: Keyword.get(opts, :long_window, 20)
}
end
def signal(current_bar, previous_bars, state) do
short_ma = calculate_ma(previous_bars, state.short_window)
long_ma = calculate_ma(previous_bars, state.long_window)
cond do
short_ma > long_ma -> {:buy, state}
short_ma < long_ma -> {:sell, state}
true -> {:hold, state}
end
end
defp calculate_ma(bars, window) do
bars
|> Enum.take(window)
|> Enum.map(& &1.close)
|> Enum.sum()
|> Kernel./(window)
end
endAPI Reference
Backtester.Engine
run/3
Runs a basic backtest.
Parameters:
bars- List ofBacktester.Barstructs (chronologically ordered)strategy_module- Module implementingBacktester.Strategyopts- Options::starting_cash- Initial portfolio value (default: 100,000.0):strategy_opts- Options passed to strategy
Returns:
%{
portfolio: %Backtester.Portfolio{},
performance: %{
total_value: 105_250.0,
total_return: 5_250.0,
return_percent: 5.25,
realized_pnl: 2_100.0,
unrealized_pnl: 150.0,
total_trades: 8
},
trade_history: [...],
bars_processed: 100
}run_detailed/3
Runs a detailed backtest with bar-by-bar portfolio tracking.
Additional Returns:
portfolio_values- Array of portfolio values at each bar
calculate_metrics/1
Calculates extended performance metrics.
Additional Metrics:
win_rate- Percentage of profitable tradessharpe_ratio- Risk-adjusted return measuremax_drawdown- Maximum peak-to-trough declinevolatility- Standard deviation of returns
Backtester.Portfolio
Portfolio management with buy/sell/hold operations.
# Create new portfolio
portfolio = Backtester.Portfolio.new(100_000.0)
# Execute trades
portfolio = Backtester.Portfolio.buy(portfolio, 150.0)
portfolio = Backtester.Portfolio.sell(portfolio, 160.0)
# Get performance stats
stats = Backtester.Portfolio.performance_stats(portfolio, current_price)Backtester.Strategy
Behavior for implementing trading strategies.
Required Callback:
@callback signal(
current_bar :: Backtester.Bar.t(),
previous_bars :: [Backtester.Bar.t()],
state :: map()
) :: {:buy | :sell | :hold, map()}Optional Callback:
@callback init(opts :: keyword()) :: map()Built-in Strategies
Moving Average Strategy (Backtester.Strategies.MA)
Simple moving average crossover strategy.
Options:
:window- Moving average period (default: 5)
Logic:
- Buy when price > moving average
- Sell when price < moving average
- Hold when price equals moving average
# Use with custom window
Engine.run(bars, Backtester.Strategies.MA,
strategy_opts: [window: 20]
)Performance Metrics
Basic Metrics
- Total Return - Absolute profit/loss
- Return Percentage - Percentage gain/loss
- Realized P&L - Profit from completed trades
- Unrealized P&L - Profit from open positions
Advanced Metrics
- Win Rate - Percentage of profitable trades
- Sharpe Ratio - Risk-adjusted return (return/volatility)
- Maximum Drawdown - Largest peak-to-trough decline
- Volatility - Standard deviation of returns
Examples
Compare Multiple Strategies
strategies = [
Backtester.Strategies.MA
# Add more strategies here
]
{:ok, comparison} = Backtester.Example.compare_strategies(
"AAPL", "6mo", "1day", strategies
)
# Print comparison results
Enum.each(comparison.summary, fn strategy_result ->
IO.puts("#{strategy_result.strategy}: #{strategy_result.return_percent}%")
end)Custom Analysis
# Run detailed backtest
{:ok, result} = Backtester.Example.run_backtest("MSFT", "1y", "1day")
# Extract portfolio values for plotting
portfolio_values = result.portfolio_values
timestamps = Enum.map(portfolio_values, & &1.timestamp)
values = Enum.map(portfolio_values, & &1.portfolio_value)
# Calculate custom metrics
extended_metrics = Backtester.Engine.calculate_metrics(result)
IO.inspect(extended_metrics)Integration with IBKR API
The backtesting framework integrates seamlessly with the IBKR API:
- Contract Search - Use
IbkrApi.ClientPortal.Contract.search_contracts/2 - Historical Data - Use
IbkrApi.ClientPortal.MarketData.get_historical_data/4 - Data Conversion - Use
Backtester.Bar.from_historical_bar/1
Complete Workflow
# 1. Search for contract
{:ok, contracts} = IbkrApi.ClientPortal.Contract.search_contracts("AAPL")
contract_id = List.first(contracts).conid
# 2. Fetch historical data
{:ok, historical_bars} = IbkrApi.ClientPortal.MarketData.get_historical_data(
to_string(contract_id), "3mo", "1day"
)
# 3. Convert to backtest format
backtest_bars = Enum.map(historical_bars, &Backtester.Bar.from_ibkr/1)
# 4. Run backtest
result = Backtester.Engine.run_detailed(backtest_bars, Backtester.Strategies.MA)
# 5. Analyze results
Backtester.Example.print_report(Map.put(result, :symbol, "AAPL"))Best Practices
- Data Quality - Ensure historical data is clean and properly ordered
- Strategy Validation - Test strategies on multiple time periods and symbols
- Risk Management - Consider transaction costs and slippage in real trading
- Overfitting - Avoid over-optimizing strategies on historical data
- Forward Testing - Validate strategies on out-of-sample data
Limitations
- Simple Portfolio Model - Currently supports single-asset, long-only strategies
- No Transaction Costs - Backtests don't include commissions or slippage
- Perfect Execution - Assumes all trades execute at exact bar close prices
- No Risk Management - No built-in stop-loss or position sizing rules
Future Enhancements
- Multi-asset portfolio support
- Short selling capabilities
- Transaction cost modeling
- Advanced order types
- Risk management features
- Performance attribution analysis