Skip to content

Configure realistic ack-latency distributions

The default ack-latency knobs (set_submit_ack_latency(latency_ns, jitter_ns)) sample uniform jitter around a fixed base. Real venue latency is heavy-tailed (lognormal-like p50 ≈ 5ms, p99 ≈ 80ms) and correlated: when the venue is under load, every order is slow, not each one independently.

LatencyDistribution ships four kinds plus a burst-correlation knob. The scalar set_submit_ack_latency setter still works and maps to the Uniform kind for backward compatibility.

Kinds

Kind Use when
Constant Deterministic baselines, parity tests.
Uniform Legacy base ± jitter behaviour.
Lognormal Real venue ack timings — heavy right tail.
Empirical You have a recorded histogram and want to resample it.

Burst correlation

Set with set_burst_correlation(rho) where rho is in [0, 1). Implementation is AR(1) on the standard-normal residual for Lognormal (or on the rank index for Empirical). The independent- draw default is rho = 0.

Why care: a strategy that places, races, and cancels every 50ms under a uniform model behaves very differently from the same strategy under a rho = 0.6 burst regime, where slow acks cluster. The latter matches what live traders observe during venue load spikes.

Apply from a strategy

"""Apply a heavy-tailed, burst-correlated ack-latency distribution."""
import flox_py as flox

exec = flox.SimulatedExecutor()

# Lognormal: median 5ms, sigma 0.7 → p99 ~ 50ms tail.
dist = flox.LatencyDistribution.lognormal(median_ns=5_000_000, sigma=0.7)
# Couple successive draws via AR(1): when the venue is slow, the
# next ack tends to be slow too.
dist.set_burst_correlation(0.4)

exec.set_submit_ack_latency_distribution(dist)
exec.set_cancel_ack_latency_distribution(
    flox.LatencyDistribution.lognormal(median_ns=8_000_000, sigma=0.6))
exec.set_replace_ack_latency_distribution(
    flox.LatencyDistribution.lognormal(median_ns=12_000_000, sigma=0.6))

# Empirical: resample from an observed histogram.
recorded = [1_000_000, 1_500_000, 2_000_000, 3_000_000, 4_500_000,
            6_000_000, 9_000_000, 15_000_000, 35_000_000, 80_000_000]
empirical = flox.LatencyDistribution.empirical(recorded)
exec.set_submit_ack_latency_distribution(empirical)

assert dist.median_ns() == 5_000_000
const flox = require('@flox-foundation/flox');
const dist = new flox.LatencyDistribution();
dist.setLognormal(5_000_000, 0.7);
dist.setBurstCorrelation(0.4);
exec.setSubmitAckLatencyDistribution(dist);
from flox.backtest import SimulatedExecutor, LatencyDistribution

dist = LatencyDistribution.lognormal(5_000_000, 0.7)
dist.set_burst_correlation(0.4)
exec = SimulatedExecutor()
exec.set_submit_ack_latency_distribution(dist)
const dist = __flox_latency_distribution_create();
__flox_latency_distribution_set_lognormal(dist, 5000000n, 0.7);
__flox_latency_distribution_set_burst_correlation(dist, 0.4);
__flox_simulated_executor_set_submit_ack_latency_distribution(exec, dist);
#include "flox/backtest/latency_distribution.h"
auto dist = flox::LatencyDistribution::lognormal(5'000'000, 0.7);
dist.setBurstCorrelation(0.4);
sim.setSubmitAckLatencyDistribution(dist);

Calibrating from real data

A first-pass workflow when you have a venue tape with timestamped submits and acks:

  1. Compute ack_latency = ack_ts - submit_ts for each pair.
  2. Fit lognormal: mu = mean(ln(latency)), sigma = stdev(ln(latency)), median = exp(mu).
  3. Estimate burst correlation: lag-1 autocorrelation of the ln(latency) series, clamped to [0, 0.95].

For Empirical use, downsample the latency series (every Nth sample or a histogram bucket) and pass it directly. The estimator resamples with replacement.

Notes

  • The scalar set_submit_ack_latency setter is preserved and maps to Uniform(base - jitter, base + jitter). Existing tests keep passing.
  • Distributions are copied on attach. Mutating a distribution after attaching does not affect the executor — call set_*_latency_distribution again to update.
  • Empirical sampling uses replacement, so finite samples cannot exhaust the source histogram.