Skip to content

factrix.metrics.event_horizon

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

Answers
  • Is there pre-event leakage? (T-6..T-1 should be ~0)
  • At which horizon is the signal 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); descriptive curve plus binomial test on per-horizon hit rate.

factrix.metrics.event_horizon.compute_event_returns

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

Per-event return at multiple time offsets relative to event date.

For each event (factor != 0) and each offset k: - Post-event (k > 0): cumulative return from t+1 entry. signed_return = sign(factor) × (price[t+1+k] / price[t+1] - 1) - Pre-event (k < 0): single-bar return at that offset. return = price[t+k] / price[t+k-1] - 1 (unsigned, for leakage check)

Parameters:

Name Type Description Default
df DataFrame

Panel with date, asset_id, factor, price.

required
offsets list[int] | None

Time offsets relative to event date. Defaults to [-6, -3, -1, 1, 6, 12, 24].

None

Returns:

Type Description
DataFrame

DataFrame with offset, date, asset_id, signed_return, abs_return.

DataFrame

Empty if price column not present.

Notes

For each event (t_event, asset) and offset k::

k > 0:  signed_return = sign(factor) *
                        (price[t+1+k] / price[t+1] - 1)
k < 0:  signed_return = price[t+k] / price[t+k-1] - 1

factrix asymmetrically signs only post-event returns. Pre-event single-bar returns are unsigned because they are read for leakage detection, where the absolute response (independent of the eventual signal direction) is what matters.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.event_horizon import compute_event_returns
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> per_event = compute_event_returns(panel)
>>> set(per_event.columns) >= {"offset", "date", "asset_id", "signed_return"}
True

factrix.metrics.event_horizon.event_around_return

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

Return profile at multiple offsets around event date.

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 → signal may be reactive, not predictive.

Parameters:

Name Type Description Default
df 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
MetricOutput

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

MetricOutput

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

MetricOutput

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

MetricOutput

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 signal 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
'event_around_return'

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=0.004, with_price=True, 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