Skip to main content
Pro Feature. Requires a Pro or Ultra subscription. Get started at api.mathematicalcompany.com
What is this? ACD models predict how long until the next trade or event. Just like GARCH models volatility clustering (big moves follow big moves), ACD captures duration clustering (fast trades follow fast trades). Use it to detect unusual trading activity - if a trade arrives much sooner than expected, someone might have information.

ACD Duration Models

Horizon implements Autoregressive Conditional Duration (ACD) models from Engle and Russell (1998) for modeling the time between market events. ACD models capture the clustering of trade arrivals and provide real-time estimates of expected duration, surprise, and hazard rate. All computation runs in Rust via PyO3.

ACD(1,1) Model

Autoregressive conditional duration with GARCH-like dynamics for inter-event times.

Surprise Detection

Quantify how unexpected a trade arrival is relative to the fitted duration process.

Hazard Rate

Instantaneous probability of an event occurring given elapsed time since the last event.

Pipeline Integration

hz.duration_monitor() injects live duration state into your strategy context each cycle.

Why Duration Models?

Trade arrivals in prediction markets are not uniformly distributed. Activity clusters around news events, resolution deadlines, and whale entries. The ACD model captures this clustering by modeling the conditional expected duration between events as an autoregressive process: psi_i = omega + alpha * x_(i-1) + beta * psi_(i-1) where x_i is the observed duration and psi_i is the conditional expected duration. Short durations predict more short durations (clustering), and the model mean-reverts to omega / (1 - alpha - beta).
ACD models are the duration-domain analogue of GARCH models for volatility. Just as GARCH captures volatility clustering, ACD captures trade-arrival clustering. Together they provide a complete picture of market microstructure dynamics.

API

hz.AcdModel

Create an ACD(1,1) model with specified parameters.
import horizon as hz

model = hz.AcdModel(omega=0.1, alpha=0.15, beta=0.80)
print(f"Unconditional mean duration: {model.expected_duration():.4f}")
ParameterTypeDescription
omegafloatIntercept (must be positive)
alphafloatLag-duration coefficient (non-negative)
betafloatLag-expected-duration coefficient (non-negative)
Stationarity requires alpha + beta < 1. If this condition is violated, a ValueError is raised.

AcdModel Methods

update(event_time)

Feed a new event time into the model. Updates the internal state with the observed inter-event duration.
import horizon as hz

model = hz.AcdModel(omega=0.1, alpha=0.15, beta=0.80)

event_times = [0.0, 1.2, 1.8, 3.5, 4.1, 7.0, 7.3]
for t in event_times:
    state = model.update(t)
    print(f"t={t:.1f}  expected={state.expected_duration:.3f}  surprise={state.surprise:.3f}")
ParameterTypeDescription
event_timefloatTimestamp of the new event (must be non-decreasing)
Returns an AcdState object.

expected_duration()

Return the current conditional expected duration (psi_i) given the model state.
psi = model.expected_duration()
print(f"Next event expected in {psi:.2f} seconds")
Returns float.

surprise(duration)

Compute the surprise of an observed duration: x_i / psi_i. Values greater than 1.0 indicate the event arrived later than expected; values less than 1.0 indicate it arrived sooner.
s = model.surprise(duration=0.5)
print(f"Surprise: {s:.3f}")  # < 1.0 means faster than expected
ParameterTypeDescription
durationfloatObserved inter-event duration (non-negative)
Returns float.

hazard_rate(elapsed)

Estimate the instantaneous hazard rate given the time elapsed since the last event. Under the exponential ACD assumption, h(t) = 1 / psi_i (constant hazard), but the model adjusts for clustering effects.
h = model.hazard_rate(elapsed=2.5)
print(f"Hazard rate: {h:.4f}")
ParameterTypeDescription
elapsedfloatTime elapsed since the last event (non-negative)
Returns float.

AcdState Type

Returned by AcdModel.update().
FieldTypeDescription
expected_durationfloatConditional expected duration (psi_i) after the update
observed_durationfloatDuration between the last two events (x_i)
surprisefloatRatio x_i / psi_i
hazard_ratefloatInstantaneous hazard at the time of the event

Fitting from Data

hz.fit_acd

Estimate ACD(1,1) parameters from a series of event times using maximum likelihood.
import horizon as hz

event_times = [0.0, 1.2, 1.8, 3.5, 4.1, 7.0, 7.3, 8.9, 10.1, 12.6]
model = hz.fit_acd(event_times)
print(f"omega={model.omega:.4f}, alpha={model.alpha:.4f}, beta={model.beta:.4f}")
ParameterTypeDescription
event_timeslist[float]Sorted event timestamps (at least 3 events)
Returns a fitted AcdModel.

hz.acd_log_likelihood

Compute the log-likelihood of an ACD(1,1) model given observed event times. Useful for model comparison and diagnostics.
import horizon as hz

event_times = [0.0, 1.2, 1.8, 3.5, 4.1, 7.0, 7.3, 8.9]

model_a = hz.AcdModel(omega=0.1, alpha=0.15, beta=0.80)
model_b = hz.AcdModel(omega=0.2, alpha=0.10, beta=0.85)

ll_a = hz.acd_log_likelihood(model_a, event_times)
ll_b = hz.acd_log_likelihood(model_b, event_times)

print(f"Model A log-likelihood: {ll_a:.4f}")
print(f"Model B log-likelihood: {ll_b:.4f}")
print(f"Better model: {'A' if ll_a > ll_b else 'B'}")
ParameterTypeDescription
modelAcdModelThe ACD model to evaluate
event_timeslist[float]Sorted event timestamps (at least 3 events)
Returns float: the log-likelihood value.

Pipeline Integration

hz.duration_monitor

Pipeline function that tracks inter-event durations in real time and injects ACD state into ctx.params["acd"].
import horizon as hz

def my_strategy(ctx):
    acd = ctx.params.get("acd")
    if acd is None:
        return []

    # Skip quoting when events are arriving unusually fast
    if acd["surprise"] < 0.5:
        print("Event clustering detected, pausing quotes")
        return []

    # Widen spread when hazard rate is high
    spread = 0.04 if acd["hazard_rate"] > 0.5 else 0.02

    mid = ctx.feed.price
    return [
        hz.quote(ctx, hz.Side.Yes, hz.OrderSide.Buy, mid - spread, 10),
        hz.quote(ctx, hz.Side.Yes, hz.OrderSide.Sell, mid + spread, 10),
    ]

hz.run(
    name="duration-aware-mm",
    markets=["0xcondition..."],
    pipeline=[
        hz.duration_monitor(),
        my_strategy,
    ],
    interval=1.0,
)
The ctx.params["acd"] dict contains:
KeyTypeDescription
expected_durationfloatCurrent conditional expected duration
surprisefloatSurprise of the most recent event
hazard_ratefloatCurrent instantaneous hazard rate
n_eventsintTotal events observed
mean_durationfloatSample mean of observed durations

Mathematical Background

The ACD(1,1) model specifies the conditional expected duration as:psi_i = omega + alpha * x_(i-1) + beta * psi_(i-1)The unconditional mean duration is E[x] = omega / (1 - alpha - beta), which requires alpha + beta < 1 for stationarity.The standardized durations epsilon_i = x_i / psi_i are i.i.d. with unit mean under the model. Departures from this (surprise values far from 1.0) indicate model misspecification or regime changes.
Under the exponential ACD assumption, the log-likelihood is:L = sum(-log(psi_i) - x_i / psi_i)fit_acd maximizes this over (omega, alpha, beta) subject to omega > 0, alpha >= 0, beta >= 0, alpha + beta < 1. The optimizer uses bounded L-BFGS-B internally.
The hazard rate h(t) = f(t) / S(t) gives the instantaneous probability of an event at time t, conditional on no event having occurred since the last one. Under exponential ACD, the hazard is constant at 1/psi_i between events. A rising hazard over elapsed time suggests the next event is overdue.