Skip to main content
The stats module provides comprehensive performance analysis tools for backtesting results, including equity curves, risk metrics, and customizable statistics.

Quick Start

import numpy as np
from hftbacktest.stats import LinearAssetRecord

# Load backtest results
record = np.load('backtest_results.npz')['0']  # Asset 0

# Generate statistics
stats = (
    LinearAssetRecord(record)
        .resample('10s')      # Resample to 10-second intervals
        .monthly()            # Generate monthly breakdowns
        .stats(book_size=100000)  # $100k capital
)

# Display summary
print(stats.summary())

# Plot equity curve
stats.plot()

Recording Backtest State

Recorder

Records time-series state information during backtesting.
from hftbacktest import Recorder
from numba import njit

recorder = Recorder(num_assets=1, record_size=100000)

@njit
def strategy(hbt, rec):
    while hbt.elapse(10_000_000) == 0:  # Every 10ms
        # Record current state
        rec.record(hbt)
        
        # ... strategy logic ...
    
    return True

# Run backtest
strategy(hbt, recorder.recorder)

# Save results
recorder.to_npz('results.npz')

Constructor

num_assets
uint64
required
Number of assets being tracked.
record_size
uint64
required
Maximum number of records to store. Raises IndexError if exceeded.
# For 24 hours at 10ms intervals: 24 * 3600 * 100 = 8,640,000
recorder = Recorder(num_assets=1, record_size=10_000_000)

Properties

recorder
Recorder_
Returns the JIT-compiled recorder instance for use in @njit functions.
@njit
def strategy(hbt, rec):
    rec.record(hbt)  # Use in Numba code

strategy(hbt, recorder.recorder)

Methods

to_npz
method
Saves recorded data to a file.Parameters:
  • file (str): Output file path (.npz format)
recorder.to_npz('backtest_results.npz')
get
method
Retrieves records for a specific asset.Parameters:
  • asset_no (int): Asset number
Returns: NDArray[record_dtype] - Array of records
asset0_records = recorder.get(0)
print(f"Recorded {len(asset0_records)} data points")

Asset Records

LinearAssetRecord

Processes records for linear assets (standard futures, spot markets).
from hftbacktest.stats import LinearAssetRecord
import numpy as np

# Load recorded data
data = np.load('backtest_results.npz')['0']

# Create record processor
record = LinearAssetRecord(data)

Constructor

data
NDArray | DataFrame
required
Backtest record data as NumPy array or Polars DataFrame.

Configuration Methods

contract_size
method
Sets the contract size multiplier.Parameters:
  • contract_size (float): Contract multiplier. Default: 1.0
Returns: Self (for method chaining)
record.contract_size(1.0)  # 1x multiplier for spot
time_unit
method
Sets the time unit for timestamp conversion.Parameters:
  • time_unit (str): Time unit (‘ns’, ‘us’, ‘ms’, ‘s’). Default: ‘ns’
Returns: Self
record.time_unit('ns')  # Nanoseconds
resample
method
Sets resampling frequency for downsampling.Parameters:
  • frequency (str): Polars-compatible frequency string. Default: ’10s’
Returns: Self
record.resample('1m')   # 1-minute bars
record.resample('10s')  # 10-second bars
record.resample('1h')   # 1-hour bars
monthly
method
Generates monthly statistics breakdown.Returns: Self
record.monthly()
daily
method
Generates daily statistics breakdown.Returns: Self
record.daily()
stats
method
Computes statistics with specified metrics.Parameters:
  • metrics (List[Metric | Type[Metric]], optional): List of metrics to compute. Default: SR, Sortino, Ret, MaxDrawdown, DailyNumberOfTrades, DailyTradingValue, ReturnOverMDD, ReturnOverTrade, MaxPositionValue
  • **kwargs: Additional arguments passed to metric constructors
Returns: Stats instance
from hftbacktest.stats import SR, AnnualRet, MaxDrawdown

stats = record.stats(
    metrics=[SR, AnnualRet, MaxDrawdown],
    book_size=100000,
    trading_days_per_year=365
)

InverseAssetRecord

Processes records for inverse assets (inverse perpetuals).
from hftbacktest.stats import InverseAssetRecord

record = InverseAssetRecord(data)
stats = record.resample('10s').stats(book_size=1.0)  # 1 BTC capital
Provides the same interface as LinearAssetRecord but with inverse asset equity calculation.

Stats

Container for computed statistics.

Methods

summary
method
Returns a summary DataFrame of statistics.Parameters:
  • pretty (bool): Format for pretty printing. Default: False
Returns: Polars DataFrame
summary_df = stats.summary()
print(summary_df)
plot
method
Plots equity curves and position charts.Parameters:
  • price_as_ret (bool): Plot price as cumulative returns. Default: False
  • backend (str): Plotting backend (‘matplotlib’ or ‘holoviews’). Default: ‘matplotlib’
Returns: Figure object
# Plot with matplotlib
fig = stats.plot(price_as_ret=True, backend='matplotlib')
fig.savefig('equity_curve.png')

# Plot with holoviews (interactive)
plot = stats.plot(backend='holoviews')

Performance Metrics

All metrics inherit from the Metric base class and can be customized.

Base Metric Class

from hftbacktest.stats import Metric
import polars as pl

class CustomMetric(Metric):
    def __init__(self, name='CustomMetric'):
        self.name = name
    
    def compute(self, df: pl.DataFrame, context: dict) -> dict:
        # Compute your metric
        value = df['position'].abs().mean()
        return {self.name: value}

Return Metrics

Ret
class
Total return.Parameters:
  • name (str, optional): Metric name. Default: ‘Return’
  • book_size (float, optional): Capital allocation for percentage calculation
from hftbacktest.stats import Ret

stats = record.stats(
    metrics=[Ret('TotalReturn', book_size=100000)]
)
AnnualRet
class
Annualized return.Parameters:
  • name (str, optional): Metric name. Default: ‘AnnualReturn’
  • book_size (float, optional): Capital allocation
  • trading_days_per_year (float): Trading days per year. Default: 252
from hftbacktest.stats import AnnualRet

stats = record.stats(
    metrics=[AnnualRet(trading_days_per_year=365)],
    book_size=100000
)

Risk Metrics

SR
class
Sharpe Ratio (without risk-free rate).Parameters:
  • name (str, optional): Metric name. Default: ‘SR’
  • trading_days_per_year (float): Trading days for annualization. Default: 252
from hftbacktest.stats import SR

# Crypto markets (24/7)
stats = record.stats(
    metrics=[SR('SR365', trading_days_per_year=365)]
)
Sortino
class
Sortino Ratio (penalizes only downside volatility).Parameters:
  • name (str, optional): Metric name. Default: ‘Sortino’
  • trading_days_per_year (float): Default: 252
from hftbacktest.stats import Sortino

stats = record.stats(metrics=[Sortino()])
MaxDrawdown
class
Maximum drawdown.Parameters:
  • name (str, optional): Metric name. Default: ‘MaxDrawdown’
  • book_size (float, optional): Capital for percentage calculation
from hftbacktest.stats import MaxDrawdown

stats = record.stats(
    metrics=[MaxDrawdown()],
    book_size=100000
)

Composite Metrics

ReturnOverMDD
class
Return divided by maximum drawdown.Parameters:
  • name (str, optional): Default: ‘ReturnOverMDD’
from hftbacktest.stats import ReturnOverMDD

stats = record.stats(metrics=[ReturnOverMDD()])
ReturnOverTrade
class
Return per unit of trading value.Parameters:
  • name (str, optional): Default: ‘ReturnOverTrade’
from hftbacktest.stats import ReturnOverTrade

stats = record.stats(metrics=[ReturnOverTrade()])

Trading Activity Metrics

NumberOfTrades
class
Total number of trades.Parameters:
  • name (str, optional): Default: ‘NumberOfTrades’
DailyNumberOfTrades
class
Average number of trades per day.Parameters:
  • name (str, optional): Default: ‘DailyNumberOfTrades’
TradingVolume
class
Total trading volume (quantity).Parameters:
  • name (str, optional): Default: ‘TradingVolume’
DailyTradingVolume
class
Average daily trading volume.Parameters:
  • name (str, optional): Default: ‘DailyTradingVolume’
TradingValue
class
Total trading value (notional) or turnover.Parameters:
  • name (str, optional): Default: ‘TradingValue’ or ‘Turnover’ if book_size provided
  • book_size (float, optional): Capital for turnover calculation
from hftbacktest.stats import TradingValue

# Get turnover ratio
stats = record.stats(
    metrics=[TradingValue()],
    book_size=100000
)
DailyTradingValue
class
Average daily trading value or turnover.Parameters:
  • name (str, optional): Default: ‘DailyTradingValue’ or ‘DailyTurnover’ if book_size provided
  • book_size (float, optional): Capital for turnover calculation

Position Metrics

MaxPositionValue
class
Maximum open position value.Parameters:
  • name (str, optional): Default: ‘MaxPositionValue’
from hftbacktest.stats import MaxPositionValue

stats = record.stats(metrics=[MaxPositionValue()])
MeanPositionValue
class
Average open position value.Parameters:
  • name (str, optional): Default: ‘MeanPositionValue’
MedianPositionValue
class
Median open position value.Parameters:
  • name (str, optional): Default: ‘MedianPositionValue’
MaxLeverage
class
Maximum leverage (position value / capital).Parameters:
  • name (str, optional): Default: ‘MaxLeverage’
  • book_size (float): Capital allocation (required)
from hftbacktest.stats import MaxLeverage

stats = record.stats(
    metrics=[MaxLeverage()],
    book_size=100000
)

Complete Example

import numpy as np
from numba import njit
from hftbacktest import (
    BacktestAsset,
    HashMapMarketDepthBacktest,
    Recorder,
    GTX, LIMIT, BUY
)
from hftbacktest.stats import (
    LinearAssetRecord,
    SR, Sortino, Ret, AnnualRet,
    MaxDrawdown, ReturnOverMDD,
    DailyNumberOfTrades,
    DailyTradingValue,
    MaxPositionValue,
    MaxLeverage
)

# Setup recorder
recorder = Recorder(num_assets=1, record_size=1_000_000)

@njit
def simple_strategy(hbt, rec):
    asset_no = 0
    
    while hbt.elapse(10_000_000) == 0:  # Every 10ms
        # Record state
        rec.record(hbt)
        
        # Simple strategy logic
        depth = hbt.depth(asset_no)
        mid = (depth.best_bid + depth.best_ask) / 2.0
        
        hbt.submit_buy_order(
            asset_no, 1, depth.best_bid,
            0.01, GTX, LIMIT, False
        )
    
    return True

# Run backtest
asset = BacktestAsset().data('data.npz').linear_asset(1.0)
hbt = HashMapMarketDepthBacktest([asset])
simple_strategy(hbt, recorder.recorder)

# Save results
recorder.to_npz('results.npz')

# Analyze performance
data = np.load('results.npz')['0']

stats = (
    LinearAssetRecord(data)
        .resample('1m')     # 1-minute resampling
        .monthly()          # Monthly breakdown
        .stats(
            metrics=[
                SR('SR365', trading_days_per_year=365),
                Sortino(),
                Ret(),
                AnnualRet(trading_days_per_year=365),
                MaxDrawdown(),
                ReturnOverMDD(),
                DailyNumberOfTrades(),
                DailyTradingValue(),
                MaxPositionValue(),
                MaxLeverage()
            ],
            book_size=100000  # $100k capital
        )
)

# Display results
print("\n=== Performance Summary ===")
summary = stats.summary()
print(summary)

# Plot equity curve
fig = stats.plot(price_as_ret=True)
fig.savefig('performance.png', dpi=300, bbox_inches='tight')
print("\nEquity curve saved to performance.png")

Best Practices

Recording Frequency

  • High-frequency strategies: Record every 1-10 seconds
  • Medium-frequency: Record every 1-5 minutes
  • Low-frequency: Record every 5-60 minutes
Balance detail vs memory usage:
# Calculate required size
backtest_hours = 24 * 30  # 30 days
record_interval_seconds = 10
record_size = backtest_hours * 3600 / record_interval_seconds

recorder = Recorder(num_assets=1, record_size=int(record_size * 1.1))

Capital Allocation

Always specify book_size for meaningful percentage-based metrics:
stats = record.stats(book_size=100000)  # $100k capital

Custom Metrics

Create domain-specific metrics:
from hftbacktest.stats import Metric
import polars as pl

class AvgHoldingTime(Metric):
    def __init__(self):
        self.name = 'AvgHoldingTime'
    
    def compute(self, df: pl.DataFrame, context: dict) -> dict:
        # Calculate average time in position
        in_position = (df['position'] != 0).to_numpy()
        if in_position.sum() == 0:
            return {self.name: 0}
        
        # Simple approximation
        holding_ratio = in_position.mean()
        total_time = (df['timestamp'][-1] - df['timestamp'][0]).total_seconds()
        avg_time = total_time * holding_ratio
        
        return {self.name: avg_time / 3600}  # Convert to hours

stats = record.stats(metrics=[AvgHoldingTime()])

Build docs developers (and LLMs) love