ExPostFacto Troubleshooting Guide
View SourceThis guide helps you diagnose and fix common issues when using ExPostFacto.
Table of Contents
- Installation Issues
- Data Problems
- Strategy Development Issues
- Performance Problems
- Validation Errors
- Common Error Messages
- Debugging Tips
Installation Issues
Problem: Mix dependencies won't resolve
Error:
** (Mix) Could not resolve dependency :ex_post_factoSolutions:
Check your
mix.exsdependency specification:{:ex_post_facto, "~> 0.2.0"}Clear dependency cache:
mix deps.clean --all mix deps.getCheck Elixir/OTP version compatibility:
elixir --version
Problem: Compilation errors
Error:
** (CompileError) lib/my_strategy.ex:5: undefined function buy/0Solution: Make sure you're using the Strategy behaviour correctly:
defmodule MyStrategy do
use ExPostFacto.Strategy # This imports buy/0, sell/0, etc.
def init(_opts), do: {:ok, %{}}
def next(state), do: {:ok, state}
endData Problems
Problem: "Data cannot be empty" error
Cause: Your data list is empty or becomes empty after cleaning.
Solutions:
Check your data source:
IO.inspect(length(market_data), label: "Data length")Verify data format:
IO.inspect(hd(market_data), label: "First data point")Check if data cleaning removes all points:
{:ok, cleaned} = ExPostFacto.clean_data(market_data) IO.puts("Original: #{length(market_data)}, Cleaned: #{length(cleaned)}")
Problem: CSV file loading fails
Error:
{:error, "failed to load data: failed to read file: enoent"}Solutions:
Check file path:
File.exists?("path/to/data.csv")Use absolute paths:
path = Path.expand("data/market_data.csv") {:ok, result} = ExPostFacto.backtest(path, strategy)Verify CSV format:
Date,Open,High,Low,Close,Volume 2023-01-01,100.0,105.0,98.0,102.0,1000000
Problem: Data validation errors
Error:
{:error, "data point 5: invalid OHLC data: high (95.0) must be >= low (98.0)"}Solutions:
Clean data before validation:
{:ok, clean_data} = ExPostFacto.clean_data(dirty_data) {:ok, result} = ExPostFacto.backtest(clean_data, strategy)Fix data manually:
fixed_data = Enum.map(data, fn point -> %{point | high: max(point.high, max(point.open, point.close)), low: min(point.low, min(point.open, point.close)) } end)Skip validation (not recommended):
{:ok, result} = ExPostFacto.backtest(data, strategy, validate_data: false)
Strategy Development Issues
Problem: Strategy never generates trades
Symptoms: trades_count: 0 in results.
Debugging steps:
Enable debug mode:
{:ok, result} = ExPostFacto.backtest( data, strategy, enhanced_validation: true, debug: true )Add logging to your strategy:
def next(state) do current_price = data().close IO.puts("Current price: #{current_price}") if current_price > 100 do IO.puts("Buy condition met!") buy() end {:ok, state} endCheck data availability:
def next(state) do if length(state.price_history) < 20 do IO.puts("Not enough data yet: #{length(state.price_history)}") else # Your strategy logic end {:ok, state} end
Problem: Strategy crashes during execution
Error:
** (FunctionClauseError) no function clause matching in MyStrategy.next/1Solutions:
Always return proper tuple from
next/1:def next(state) do # Your logic here {:ok, state} # Always return this format endHandle all possible states:
def next(state) do case calculate_signal(state) do {:ok, :buy} -> buy() {:ok, :sell} -> sell() {:error, _reason} -> :ok # Handle errors gracefully end {:ok, state} endUse pattern matching safely:
def next(state) do case Map.get(state, :price_history, []) do [] -> :ok # No history yet prices when length(prices) >= 10 -> # Your logic _ -> :ok # Not enough data end {:ok, state} end
Problem: Indicators return nil or unexpected values
Cause: Insufficient data for indicator calculation.
Solutions:
Check data length requirements:
def next(state) do price_history = [data().close | state.price_history] if length(price_history) >= 20 do # Ensure enough data sma = indicator(:sma, price_history, 20) current_sma = List.first(sma) if current_sma do # Check for nil # Use indicator value end end {:ok, %{state | price_history: price_history}} endHandle edge cases:
def safe_indicator(type, data, params) do case indicator(type, data, params) do [nil | _] -> nil [value | _] when is_number(value) -> value _ -> nil end end
Performance Problems
Problem: Backtests are very slow
Solutions:
Limit price history:
def next(state) do max_history = 100 # Only keep what you need price_history = [data().close | state.price_history] |> Enum.take(max_history) {:ok, %{state | price_history: price_history}} endUse streaming for large datasets:
{:ok, result} = ExPostFacto.backtest_stream( "large_file.csv", strategy, chunk_size: 1000 )Optimize indicator calculations:
# Bad: Recalculate every time def next(state) do sma = indicator(:sma, state.price_history, 20) # ... end # Good: Cache calculations def next(state) do state = maybe_update_sma(state) # Use cached state.sma_value end
Problem: Optimization takes too long
Solutions:
Reduce parameter ranges:
# Instead of large ranges [fast: 5..50, slow: 20..200] # Use smaller, focused ranges [fast: 8..12, slow: 18..22]Use random search for large spaces:
{:ok, result} = ExPostFacto.optimize( data, strategy, param_ranges, method: :random_search, samples: 100 )Set reasonable limits:
{:ok, result} = ExPostFacto.optimize( data, strategy, param_ranges, max_combinations: 500 )
Validation Errors
Problem: Enhanced validation errors
Error:
{:error, %ExPostFacto.Validation.ValidationError{
message: "Strategy validation failed",
context: %{...}
}}Solutions:
Format errors for readability:
case ExPostFacto.backtest(data, strategy, enhanced_validation: true) do {:ok, result} -> result {:error, %ExPostFacto.Validation.ValidationError{} = error} -> IO.puts(ExPostFacto.Validation.format_error(error)) :error endUse debug mode:
{:ok, result} = ExPostFacto.backtest( data, strategy, enhanced_validation: true, debug: true )
Common Error Messages
"Module not found" or "Function not exported"
Error:
** (UndefinedFunctionError) function MyStrategy.init/1 is undefinedSolution: Ensure your strategy module implements the required callbacks:
defmodule MyStrategy do
use ExPostFacto.Strategy
def init(opts) do
{:ok, %{}}
end
def next(state) do
{:ok, state}
end
end"Invalid strategy format"
Cause: Incorrect strategy specification.
Solutions:
# Correct formats:
{MyStrategy, :call, []} # MFA tuple
{MyStrategy, [param: value]} # Strategy behaviour
# Incorrect:
MyStrategy # Just module name
{MyStrategy} # Incomplete tuple"Position function not available"
Error:
** (UndefinedFunctionError) function :position not foundSolution:
Use position() inside Strategy behaviour context:
defmodule MyStrategy do
use ExPostFacto.Strategy
def next(state) do
current_pos = position() # This works here
# ...
end
end
# Not in MFA functions:
defmodule MyMFAStrategy do
def call(data, result) do
# position() not available here
# Use result.current_position instead
end
endDebugging Tips
Enable Enhanced Logging
# Full debugging
{:ok, result} = ExPostFacto.backtest(
data, strategy,
enhanced_validation: true,
debug: true,
warnings: true
)Add Temporary Logging
def next(state) do
IO.inspect(data(), label: "Current data")
IO.inspect(position(), label: "Current position")
IO.inspect(equity(), label: "Current equity")
# Your strategy logic
{:ok, state}
endTest with Minimal Data
# Create simple test data
test_data = [
%{open: 100, high: 105, low: 98, close: 102},
%{open: 102, high: 108, low: 101, close: 106},
%{open: 106, high: 110, low: 104, close: 108}
]
{:ok, result} = ExPostFacto.backtest(test_data, strategy)Validate Strategy Logic Separately
# Test your strategy logic outside of backtesting
defmodule StrategyTester do
def test_logic do
state = %{threshold: 100}
data = %{close: 105}
# Mock the data() function
result = if data.close > state.threshold, do: :buy, else: :sell
IO.puts("Signal: #{result}")
end
endUse IEx for Interactive Testing
# In IEx
iex> data = [%{open: 100, high: 105, low: 98, close: 102}]
iex> {:ok, result} = ExPostFacto.backtest(data, {MyStrategy, []})
iex> IO.inspect(result.result)Profile Performance
# Time your backtests
{time, {:ok, result}} = :timer.tc(fn ->
ExPostFacto.backtest(data, strategy)
end)
IO.puts("Backtest took #{time / 1000} ms")Check Memory Usage
# Monitor memory during development
before = :erlang.memory(:total)
{:ok, result} = ExPostFacto.backtest(data, strategy)
after_mem = :erlang.memory(:total)
IO.puts("Memory used: #{(after_mem - before) / 1024 / 1024} MB")Getting Help
If you're still having issues:
- Check the logs - Look for warning messages that might indicate problems
- Review examples - Compare your code with working examples in
lib/ex_post_facto/example_strategies/ - Read the docs - Check the API reference and guides in the
docs/directory - Create minimal reproduction - Simplify your strategy to isolate the issue
- Open an issue - If you've found a bug, please report it on GitHub
Common Gotchas
- Strategy state must be immutable - Always return updated state from
next/1 - Indicator data order - Most recent data should be first in the list
- Position management -
buy()enters long,close_buy()exits long - Data requirements - Some indicators need minimum data points to work
- Memory management - Limit historical data to what you actually need
Remember: When in doubt, enable debug mode and add logging to understand what's happening in your strategy!
Performance Checklist
- [ ] Limit price history to reasonable size (< 200 points usually sufficient)
- [ ] Cache expensive calculations when possible
- [ ] Use appropriate optimization methods for your parameter space
- [ ] Consider streaming for very large datasets
- [ ] Profile memory usage for long-running strategies
- [ ] Use concurrent optimization when testing many parameters
Happy debugging! 🐛➡️✨