# `ZenQuant.Backtest`
[🔗](https://github.com/ZenHive/zen_quant/blob/v0.8.1/lib/zen_quant/backtest.ex#L1)

Pure-function backtest harness for evaluating trading strategies over recorded JSONL data.

Wraps `ZenQuant.Recorder.Replay.reduce/4` — same data + same strategy = same result
(deterministic, verifiable). This is the foundation for EIP-8004 trustless verification:
validators re-execute the same backtest and compare results.

## Usage

    # Run a strategy over recorded data
    {:ok, result} = Backtest.run("/tmp/session.jsonl", &my_strategy/2, %{}, type: :ticker)
    result.state       # => final strategy state
    result.entries_processed  # => number of entries processed

    # Compute risk metrics from a list of period returns
    metrics = Backtest.metrics([0.01, -0.005, 0.02, 0.003])
    metrics.sharpe     # => Sharpe ratio

    # Evaluate: run + auto-metrics (strategy must accumulate :returns key)
    {:ok, result} = Backtest.evaluate("/tmp/session.jsonl", &tracker/2, %{returns: []})
    result.metrics     # => %{sharpe: ..., sortino: ..., max_drawdown: ..., calmar: ...}

## Strategy Convention

Strategies that want auto-metrics from `evaluate/4` should accumulate a `:returns` key
in their state (a list of per-period returns). This is documented, not enforced.

# `evaluate`

```elixir
@spec evaluate(Path.t(), (map(), term() -&gt; term()), term(), keyword()) ::
  {:ok,
   %{state: term(), metrics: map() | nil, entries_processed: non_neg_integer()}}
  | {:error, term()}
```

Runs a strategy and computes risk metrics if the final state contains a `:returns` key.

Composition of `run/4` + `metrics/2`. Strategies that accumulate a `:returns` key
(list of per-period returns) in their state get automatic risk metrics.

## Return values for `metrics` field

- `nil` — strategy state doesn't have a `:returns` key (or state isn't a map)
- `%{sharpe: nil, ...}` — state has `:returns` but insufficient data (< 2 elements)
- `%{sharpe: float(), ...}` — full metrics computed

## Options

All `Replay` filter options pass through: `type:`, `from:`, `to:`, `speed:`.
Metrics options: `risk_free_rate:` (default `0`), `periods_per_year:` (default `365`).

## Examples

    {:ok, result} = Backtest.evaluate("/tmp/session.jsonl", &tracker/2, %{returns: []})
    result.metrics  # => %{sharpe: ..., sortino: ..., ...} or nil

# `metrics`

```elixir
@spec metrics(
  [number()],
  keyword()
) :: map()
```

Computes standard risk metrics from a list of period returns.

Pure function, no I/O. Delegates to `ZenQuant.Risk` for individual calculations.

Returns `nil` for risk metrics when `length(returns) < 2` — avoids misleading values
from insufficient data.

## Options

- `risk_free_rate:` (default `0`) — passed to Sharpe and Sortino
- `periods_per_year:` (default `365`) — passed to Calmar

## Examples

    Backtest.metrics([0.01, -0.005, 0.02, 0.003, -0.01])
    # => %{total_return: 0.018, count: 5, sharpe: ..., sortino: ..., max_drawdown: ..., calmar: ...}

    Backtest.metrics([0.01])
    # => %{total_return: 0.01, count: 1, sharpe: nil, sortino: nil, max_drawdown: nil, calmar: nil}

# `run`

```elixir
@spec run(Path.t(), (map(), term() -&gt; term()), term(), keyword()) ::
  {:ok, %{state: term(), entries_processed: non_neg_integer()}}
  | {:error, term()}
```

Runs a strategy function over recorded JSONL entries.

The strategy has signature `(entry :: map(), state :: term()) -> new_state`.
Delegates to `Replay.reduce/4` internally.

## Options

All `Replay` filter options pass through: `type:`, `from:`, `to:`, `speed:`.

## Examples

    {:ok, result} = Backtest.run("/tmp/session.jsonl", fn entry, count ->
      count + 1
    end, 0)
    result.entries_processed  # => number of entries
    result.state              # => final count

---

*Consult [api-reference.md](api-reference.md) for complete listing*
