Skip to content

Backtesting

The Whale.BackTest module provides a replay engine for evaluating betting strategies against historical outcome data. It applies the full Whale stack (sizing, bankroll, risk, policy, guards) on each simulated decision point and collects performance metrics.

A strategy is a function from market state to a list of candidate bets:

import Whale.BackTest exposing (Strategy, Candidate)
import Whale.Types exposing (Probability, Odds)
let my_strategy : Strategy =
fun state ->
state.opportunities
|> List.filter (fun o -> o.edge > 0.02)
|> List.map (fun o -> Candidate.new o.prob o.odds)

The backtest engine calls this function at each time step, feeds candidates through the sizing and guard pipeline, and records outcomes.

import Whale.BackTest exposing (run, Config)
let config = Config.new
{ initial_bankroll = Money.usd 10_000.00
, sizing = Sizing.fractional_kelly 0.5
, policies = my_policies
, guards = my_guards
}
let results = BackTest.run config my_strategy historical_data

historical_data is a time-ordered sequence of market snapshots, each containing the opportunities visible at that point and the realized outcomes.

BackTest.run returns a BackTest.Results record:

FieldTypeMeaning
final_bankrollMoneyEnding balance
peak_bankrollMoneyHigh-water mark reached
max_drawdownFloatLargest peak-to-trough decline as a fraction
total_betsIntNumber of bets placed
win_rateFloatFraction of bets that returned a profit
total_returnFloat(final - initial) / initial
sharpeFloatAnnualized Sharpe ratio of per-period returns
log_growthFloatRealized geometric growth rate

Run multiple configurations over the same historical data and compare on the metrics that matter to your use case:

let results_full = BackTest.run config_full strategy data
let results_half = BackTest.run config_half strategy data
let better = if results_half.sharpe > results_full.sharpe
then "half Kelly"
else "full Kelly"

The backtesting surface is deterministic: identical inputs produce identical outputs. For Monte Carlo exploration (shuffled outcome order, synthetic data), generate the input sequences upstream and feed them through BackTest.run as separate historical datasets.