ExPostFacto Troubleshooting Guide

View Source

This guide helps you diagnose and fix common issues when using ExPostFacto.

Table of Contents

Installation Issues

Problem: Mix dependencies won't resolve

Error:

** (Mix) Could not resolve dependency :ex_post_facto

Solutions:

  1. Check your mix.exs dependency specification:

    {:ex_post_facto, "~> 0.2.0"}
  2. Clear dependency cache:

    mix deps.clean --all
    mix deps.get
    
  3. Check Elixir/OTP version compatibility:

    elixir --version
    

Problem: Compilation errors

Error:

** (CompileError) lib/my_strategy.ex:5: undefined function buy/0

Solution: 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}
end

Data Problems

Problem: "Data cannot be empty" error

Cause: Your data list is empty or becomes empty after cleaning.

Solutions:

  1. Check your data source:

    IO.inspect(length(market_data), label: "Data length")
  2. Verify data format:

    IO.inspect(hd(market_data), label: "First data point")
  3. 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:

  1. Check file path:

    File.exists?("path/to/data.csv")
  2. Use absolute paths:

    path = Path.expand("data/market_data.csv")
    {:ok, result} = ExPostFacto.backtest(path, strategy)
  3. 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:

  1. Clean data before validation:

    {:ok, clean_data} = ExPostFacto.clean_data(dirty_data)
    {:ok, result} = ExPostFacto.backtest(clean_data, strategy)
  2. 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)
  3. 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:

  1. Enable debug mode:

    {:ok, result} = ExPostFacto.backtest(
      data, strategy,
      enhanced_validation: true,
      debug: true
    )
  2. 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}
    end
  3. Check 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/1

Solutions:

  1. Always return proper tuple from next/1:

    def next(state) do
      # Your logic here
      {:ok, state}  # Always return this format
    end
  2. Handle 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}
    end
  3. Use 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:

  1. 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}}
    end
  2. Handle 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:

  1. 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}}
    end
  2. Use streaming for large datasets:

    {:ok, result} = ExPostFacto.backtest_stream(
      "large_file.csv",
      strategy,
      chunk_size: 1000
    )
  3. 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:

  1. Reduce parameter ranges:

    # Instead of large ranges
    [fast: 5..50, slow: 20..200]
    
    # Use smaller, focused ranges
    [fast: 8..12, slow: 18..22]
  2. Use random search for large spaces:

    {:ok, result} = ExPostFacto.optimize(
      data, strategy,
      param_ranges,
      method: :random_search,
      samples: 100
    )
  3. 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:

  1. 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
    end
  2. Use 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 undefined

Solution: 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 found

Solution: 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
end

Debugging 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}
end

Test 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
end

Use 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:

  1. Check the logs - Look for warning messages that might indicate problems
  2. Review examples - Compare your code with working examples in lib/ex_post_facto/example_strategies/
  3. Read the docs - Check the API reference and guides in the docs/ directory
  4. Create minimal reproduction - Simplify your strategy to isolate the issue
  5. Open an issue - If you've found a bug, please report it on GitHub

Common Gotchas

  1. Strategy state must be immutable - Always return updated state from next/1
  2. Indicator data order - Most recent data should be first in the list
  3. Position management - buy() enters long, close_buy() exits long
  4. Data requirements - Some indicators need minimum data points to work
  5. 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! 🐛➡️✨