Skip to content

factrix.metrics.event_horizon

Multi-horizon event analysis — how does the density behave across time?

Answers
  • Is there pre-event leakage? (T-6..T-1 should be ~0)
  • At which horizon is the density strongest?
  • Does the alpha persist or decay quickly?
Metrics

compute_event_returns — per-event, per-offset raw return data event_around_return — return profile summary at each offset

Notes

Pipeline. Per-event return profile across k offsets (per-event step); a descriptive curve only — no hypothesis test, so p_value is None and the per-horizon hit_rate in per_offset is a raw fraction, not a tested statistic.

factrix.metrics.event_horizon.event_around_return

event_around_return(data: DataFrame, *, offsets: list[int] | None = None, factor_col: str = 'factor', price_col: str = 'price') -> MetricResult

Return profile at multiple offsets around event date.

No static panel-shape thresholds are declared (sample_threshold=SampleThreshold()) because this is a multi-horizon summary metric whose available offsets and event counts are factor-context-dependent.

Summarizes per-offset: mean, median, p25, p75, hit_rate, n.

The primary value is the pre-event leakage score: mean absolute return at pre-event offsets (should be ~0). High leakage → density may be reactive, not predictive.

Parameters:

Name Type Description Default
data DataFrame

Panel with date, asset_id, factor, price.

required
offsets list[int] | None

Defaults to [-6, -3, -1, 1, 6, 12, 24].

None

Returns:

Type Description
MetricResult

MetricResult with per-offset stats in metadata. When price data is

MetricResult

unavailable, returns a short-circuit MetricResult (value=NaN,

MetricResult

metadata["reason"]="no_price_data") so all metrics share a

MetricResult

single return contract.

Notes

For each offset k: mean, median, p25, p75, hit_rate across events with valid signed_return. The headline value = mean_{k < 0} |mean_k| summarises pre-event leakage — a healthy density has flat pre-event means.

factrix uses |mean| rather than absolute returns to avoid rewarding offsets where positive and negative pre-event drifts cancel — leakage with consistent direction would be missed by mean(|return|).

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.event_horizon import event_around_return
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> result = event_around_return(panel)
>>> result.name == ""
True

Offset conventions

Defaults: offsets = [-6, -3, -1, 1, 6, 12, 24]. Offset 0 is the event date itself and is excluded from the defaults; user-supplied offsets lists are honoured verbatim.

\(k\) Anchor Formula Sign-adjusted
\(k > 0\) (post-event) Cumulative from \(t+1\) entry price[t+1+k] / price[t+1] − 1 Yes — multiplied by sign(factor). The reading is signal quality.
\(k < 0\) (pre-event) Single bar at offset price[t+k] / price[t+k−1] − 1 No — the reading is leakage, where the bar's directional response matters independent of the eventual signal sign.
\(k = 0\) (corner) Single bar at event price[t] / price[t−1] − 1 No — falls into the pre-event branch. Pass with care; the event-day bar is usually contaminated by the announcement itself.

The pre/post asymmetry is intentional. Mixing the two conventions on a single chart (post-event cumulative + pre-event single-bar) is the default factrix presentation; downstream consumers should not re-cumulate the pre-event leg.

Serial correlation across offsets

The binomial null at each offset assumes per-event independence at that offset. Adjacent post-event offsets are serially correlated within the same event\(k = 6\) and \(k = 12\) share the \(t+1\) entry price and overlap on bars \([t+2, t+7]\). The reported per-offset \(p\)-values therefore have understated variance under the joint null across offsets; treat the curve as descriptive and read the binomial \(p\) one offset at a time. See also the confounded-event note on within-asset event clustering, which compounds the same issue.

Use cases

  • Right-size the event window


    Read the post-event mean curve over \(k = 1 \ldots K\) to locate the horizon where signed drift peaks before reverting. Drives the choice of EventConfig.event_window_post for downstream MFE/MAE and CAAR work.

  • Pre-event leakage check


    Inspect \(k < 0\) mean returns: a healthy signal has flat pre-event means. The headline event_around_return.value is \(\mathrm{mean}_{k < 0} |\mathrm{mean}_k|\), summarising the leakage score in a single number.

Choosing a function

Goal Function
Per-event, per-offset raw return table for custom plots / cuts compute_event_returns
Per-offset summary (mean / median / quartiles / hit-rate) with pre-event leakage headline event_around_return

Worked example — leakage score + per-offset curve

compute_event_returns → event_around_return on a synthetic event panel

import factrix as fx
from factrix.metrics.event_horizon import (
    compute_event_returns, event_around_return,
)

panel = fx.datasets.make_event_panel(
    n_assets=200, n_dates=500, event_rate=0.02,
    post_event_drift_bps=40.0, seed=2024,
)

rets = compute_event_returns(panel, offsets=[-6, -3, -1, 1, 6, 12, 24])
print(rets.head())
# ┌────────┬────────────┬──────────┬────────────────┐
# │ offset ┆ date       ┆ asset_id ┆ signed_return  │
# ├────────┼────────────┼──────────┼────────────────┤
# │   -6   ┆ 2024-01-04 ┆ A0001    ┆  0.0012        │
# │    1   ┆ 2024-01-04 ┆ A0001    ┆  0.0041        │
# │  ...   ┆ ...        ┆ ...      ┆ ...            │
# └────────┴────────────┴──────────┴────────────────┘

out = event_around_return(panel)
print(out.value)                              # mean |pre-event mean|
print(out.metadata["per_offset"][6]["mean"])  # post-event signed mean at k=6
# 0.0007   0.0094   (approximate)

See also