---
title: "ACD Duration Models"
description: "Autoregressive Conditional Duration models for inter-event timing analysis in prediction markets."
icon: "clock-rotate-left"
---

<Note>
  **Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Note>

<Tip>
  **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.
</Tip>

# 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.

<CardGroup cols={2}>
  <Card title="ACD(1,1) Model" icon="clock">
    Autoregressive conditional duration with GARCH-like dynamics for inter-event times.
  </Card>
  <Card title="Surprise Detection" icon="bolt">
    Quantify how unexpected a trade arrival is relative to the fitted duration process.
  </Card>
  <Card title="Hazard Rate" icon="triangle-exclamation">
    Instantaneous probability of an event occurring given elapsed time since the last event.
  </Card>
  <Card title="Pipeline Integration" icon="diagram-project">
    `hz.duration_monitor()` injects live duration state into your strategy context each cycle.
  </Card>
</CardGroup>

---

## 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)`.

<Note>
  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.
</Note>

---

## API

### hz.AcdModel

Create an ACD(1,1) model with specified parameters.

```python
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}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `omega` | `float` | Intercept (must be positive) |
| `alpha` | `float` | Lag-duration coefficient (non-negative) |
| `beta` | `float` | Lag-expected-duration coefficient (non-negative) |

<Warning>
  Stationarity requires `alpha + beta < 1`. If this condition is violated, a `ValueError` is raised.
</Warning>

### AcdModel Methods

#### update(event_time)

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

```python
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}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `event_time` | `float` | Timestamp 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.

```python
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.

```python
s = model.surprise(duration=0.5)
print(f"Surprise: {s:.3f}")  # < 1.0 means faster than expected
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `duration` | `float` | Observed 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.

```python
h = model.hazard_rate(elapsed=2.5)
print(f"Hazard rate: {h:.4f}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `elapsed` | `float` | Time elapsed since the last event (non-negative) |

Returns `float`.

### AcdState Type

Returned by `AcdModel.update()`.

| Field | Type | Description |
|-------|------|-------------|
| `expected_duration` | `float` | Conditional expected duration (psi_i) after the update |
| `observed_duration` | `float` | Duration between the last two events (x_i) |
| `surprise` | `float` | Ratio `x_i / psi_i` |
| `hazard_rate` | `float` | Instantaneous 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.

```python
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}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `event_times` | `list[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.

```python
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'}")
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `model` | `AcdModel` | The ACD model to evaluate |
| `event_times` | `list[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"]`.

```python
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:

| Key | Type | Description |
|-----|------|-------------|
| `expected_duration` | `float` | Current conditional expected duration |
| `surprise` | `float` | Surprise of the most recent event |
| `hazard_rate` | `float` | Current instantaneous hazard rate |
| `n_events` | `int` | Total events observed |
| `mean_duration` | `float` | Sample mean of observed durations |

---

## Mathematical Background

<AccordionGroup>
  <Accordion title="ACD(1,1) Dynamics">
    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.
  </Accordion>
  <Accordion title="Maximum Likelihood Estimation">
    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.
  </Accordion>
  <Accordion title="Hazard Rate Interpretation">
    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.
  </Accordion>
</AccordionGroup>
