horizons
Answered against 22 years of US equities and 12 years of US options data and published with the query that produced it. This result is stored as of 2026-10-01, from do-volume-indicators-predict-anything.
| label | divergence_avg_pct | confirmed_avg_pct | gap_pct | welch_t | divergence_count | confirmed_count |
|---|---|---|---|---|---|---|
| 5 sessions | 0.46 | 0.12 | 0.34 | 2.59 | 517 | 3949 |
| 10 sessions | 0.79 | 0.28 | 0.52 | 3.04 | 517 | 3943 |
| 20 sessions | 1.53 | 0.51 | 1.02 | 4.12 | 516 | 3923 |
- Rows × columns
- 3 × 7
- Computed
- Completeness
- No missing values
- Source
- US exchange, SIP and OPRA market data
- Licence
- Strasmore terms · free, no signup
What each column holds
| Column | Type | Range | Notes |
|---|---|---|---|
label |
text | 3 distinct values (10 sessions, 20 sessions, 5 sessions) | |
divergence_avg_pct |
number | 0.46 to 1.53 | percent |
confirmed_avg_pct |
number | 0.12 to 0.51 | percent |
gap_pct |
number | 0.34 to 1.02 | percent |
welch_t |
number | 2.59 to 4.12 | |
divergence_count |
number | 516 to 517 | count |
confirmed_count |
number | 3,923 to 3,949 | count |
Computed from Strasmore's warehouse of US exchange, SIP and OPRA market data. Equity prices are delayed; options greeks and implied volatility are end-of-day. This result is stored, not recomputed on load — it is exactly the numbers that were returned on , and the query below is what returned them.
Run it yourself
This is the exact query behind the result above. Change a ticker, a date or a column and run it against the warehouse — no account, no key. The no-signup tier is smaller than the one this page was computed on; a query that reaches past it comes back saying which plan runs it.
WITH
bars AS
(
SELECT
ticker,
date,
max(toFloat64(close)) AS close,
max(toFloat64(volume)) AS volume
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('MSFT', 'SPY', 'KO', 'JNJ', 'JPM', 'XOM', 'PG', 'PEP', 'MCD', 'HD')
AND date >= '2016-01-04'
AND date <= '2026-06-30'
GROUP BY ticker, date
),
stepped AS
(
SELECT
ticker,
date,
close,
volume,
lagInFrame(close, 1) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
FROM bars
),
cumulative AS
(
SELECT
ticker,
date,
close,
sum(if(prev_close = 0, 0, if(close > prev_close, volume, if(close < prev_close, -volume, 0))))
OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS obv,
row_number() OVER (PARTITION BY ticker ORDER BY date ASC) AS bar_no
FROM stepped
),
marked AS
(
SELECT
close,
obv,
bar_no,
max(close) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS high_20,
lagInFrame(obv, 20) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) AS obv_20_back,
leadInFrame(close, 5) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING) AS close_fwd_5,
leadInFrame(close, 10) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING) AS close_fwd_10,
leadInFrame(close, 20) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING) AS close_fwd_20
FROM cumulative
),
events AS
(
SELECT
obv < obv_20_back AS is_divergent,
if(close_fwd_5 > 0, 100 * (close_fwd_5 / close - 1), NULL) AS ret_5,
if(close_fwd_10 > 0, 100 * (close_fwd_10 / close - 1), NULL) AS ret_10,
if(close_fwd_20 > 0, 100 * (close_fwd_20 / close - 1), NULL) AS ret_20
FROM marked
WHERE bar_no > 21
AND close >= high_20
),
long_form AS
(
SELECT
is_divergent,
horizon.1 AS label,
horizon.2 AS ret
FROM
(
SELECT
is_divergent,
arrayJoin([('5 sessions', ret_5), ('10 sessions', ret_10), ('20 sessions', ret_20)]) AS horizon
FROM events
)
WHERE isNotNull(horizon.2)
)
SELECT
label,
round(avgIf(ret, is_divergent), 2) AS divergence_avg_pct,
round(avgIf(ret, NOT is_divergent), 2) AS confirmed_avg_pct,
round(avgIf(ret, is_divergent) - avgIf(ret, NOT is_divergent), 2) AS gap_pct,
round((avgIf(ret, is_divergent) - avgIf(ret, NOT is_divergent))
/ sqrt(varSampIf(ret, is_divergent) / countIf(is_divergent)
+ varSampIf(ret, NOT is_divergent) / countIf(NOT is_divergent)), 2) AS welch_t,
countIf(is_divergent) AS divergence_count,
countIf(NOT is_divergent) AS confirmed_count
FROM long_form
GROUP BY label
HAVING divergence_count > 50 AND confirmed_count > 50
ORDER BY toUInt16OrZero(splitByChar(' ', label)[1]) ASC
Work with this data in your AI assistant
Opens ready to query, with this page's data. Free, no account.