EquationDB

← Back to all articles

Event studies in one line: EVENTS and OUTCOMES

2 October 2026 · EquationDB team

Most trading ideas start as a sentence: when a stock does X, it tends to do Y afterwards. Testing that sentence properly is an event study, and in most stacks an event study is a project. You pull prices for every symbol, compute the indicator, find the days the condition fired, line up forward returns at several horizons, compute a baseline so you know whether the "edge" is just the market drifting up, and then do it all again when you want to split the result by sector or by decade.

In EquationDB the whole loop is two statements. EVENTS tells you what happened. OUTCOMES tells you what usually happened next, measured against a baseline, with a t-statistic. This article walks through both and the traps they are designed to avoid.

Events are stored, not recomputed

EquationDB runs 60+ event detectors on every closed bar: moving-average crosses, 52-week highs and lows, gaps, squeezes, streaks, volume surges, earnings beats, analyst changes, market-regime shifts and more. Each occurrence is written to an event index once, when it happens, with:

  • a magnitude: the size of the move that triggered it (RSI points, the % gap, and so on);
  • a rarity: the 0–100 percentile of that magnitude among the same symbol's past occurrences;
  • forward returns (fwd_1d, fwd_5d, fwd_20d, fwd_60d on daily events), filled in as the future arrives;
  • an impact summary: what followed earlier occurrences of this event for this symbol, using only outcomes that were already known at the time, so there is no lookahead.

Because the index already exists, "what just happened?" is a lookup, not a scan:

EVENTS * IN top500 LAST 1d WHERE rarity >= 95 SORT rarity DESC

This is the last session's most unusual events across the 500 largest US stocks: every row was in the top 5% of its own history. Rarity is per symbol on purpose. A 3% gap is routine for a volatile momentum stock and remarkable for a utility, and rarity lets you rank both on the same scale. Use magnitude when you want to compare raw sizes.

Events can also be chained. A squeeze that releases and is quickly followed by a breakout is a different situation from either event alone:

EVENTS SEQUENCE squeeze_release -> new_high_20 WITHIN 5 bars IN top500 LAST 1y

Every match is a symbol where a squeeze release was followed by a new 20-day high within five bars, over the last year.

OUTCOMES: what happened next, against a baseline

Looking at a list of events is how you get ideas. Measuring them is a different job, and that is what OUTCOMES is for:

OUTCOMES AFTER golden_cross IN top500 SINCE 2005-01-01

For each forward horizon (1, 5, 20 and 60 days for daily events) you get one row with:

ColumnMeaning
nnumber of occurrences
mean_pct, median_pct, stdev_pctforward return statistics, in %
up_rateshare of occurrences followed by a positive return
t_statt-statistic of the mean
baseline_mean_pct, baseline_up_ratethe same symbols' unconditional forward return over the same period
edge_pctmean_pct − baseline_mean_pct

The baseline is the important part. Stocks go up over time, so almost any long signal shows a positive average forward return. The question that matters is whether the signal did better than simply holding the same stocks over the same years. edge_pct answers it directly. A positive mean_pct with a negative edge_pct means the condition did worse than doing nothing in particular.

For named events, OUTCOMES reads the event index and the forward returns that were already filled in, so a study over decades of history comes back quickly.

Any condition is an event

You are not limited to the built-in detectors. Anything you can write as a boolean expression can follow AFTER:

OUTCOMES AFTER rsi(14) CROSS BELOW 30 AND close > sma(200) IN top500 SINCE 2010-01-01 FWD 5d, 20d

This is an oversold reading inside a long-term uptrend, measured 5 and 20 sessions later. Conditions are scanned bar by bar with the same function code that maintains live state, so the RSI used in the study is the same RSI you would see in a live screen or an alert. FWD picks the horizons; for conditions you can use any durations.

Note the CROSS BELOW. A condition is evaluated on every bar, so rsi(14) < 30 counts every day a stock stays oversold, and a single two-week slide becomes ten overlapping samples. CROSS BELOW 30 counts each entry once, which is almost always what you mean.

Split the evidence before you trust it

An average over thousands of events can hide the fact that one sector or one era did all the work. GROUP BY splits the statistics by sector, industry, symbol, year or decade:

OUTCOMES AFTER gap_down IN top500 SINCE 2020-01-01 FWD 5d GROUP BY sector

Five-day returns after gap-downs since 2020, one row per sector. If the edge lives in two sectors and is negative in the rest, that is a different trade from "buy gap-downs".

Time is the other axis worth checking. Edges that were real in the 1990s are often arbitraged away:

OUTCOMES AFTER close CROSS ABOVE highest(high, 52w)[-1] IN top500 GROUP BY decade

This measures fresh 52-week-high breakouts, decade by decade. highest(high, 52w)[-1] is the prior bar's 52-week high, so the condition fires on the day the close first clears it.

Reading the result honestly

A few habits make the numbers more trustworthy:

  • Look at edge_pct and t_stat together. A large edge on a small n is noise until proven otherwise. A t-statistic around 2 is the usual minimum before taking an edge seriously, and with many conditions tried it should be higher.
  • Prefer the median when the mean is driven by a few outliers. median_pct next to mean_pct shows quickly whether a handful of huge moves are carrying the average.
  • Check stability. An edge that holds across sectors and across decades is worth far more than a larger one that appears in a single slice.
  • Mind survivorship. top500 is today's membership, so long studies over it favor companies that survived. Treat the result as research, not a promise.
  • Count your attempts. If you try fifty conditions, a few will look significant by chance. When you want the database to do the trying, use SEARCH, which applies false-discovery control across every candidate it tests.

From study to alert

A study is only useful if you can act on it. Because events and conditions run on the same function code everywhere, the condition you just measured can become a WHEN alert, a BACKTEST entry rule or a WATCH subscription without being rewritten. The usual path is: find something unusual with EVENTS, measure it with OUTCOMES, split it with GROUP BY until you believe it, then turn it into rules with BACKTEST.

More detail is in EVENTS, OUTCOMES and the event catalog. The tutorial Events and what followed them walks through the same loop step by step.