{"slug":"do-volume-indicators-predict-anything","qid":"horizons","label":"horizons","post_title":"do-volume-indicators-predict-anything","post_url":"/blog/do-volume-indicators-predict-anything#q-horizons","columns":["label","divergence_avg_pct","confirmed_avg_pct","gap_pct","welch_t","divergence_count","confirmed_count"],"rows":[{"label":"5 sessions","divergence_avg_pct":0.46,"confirmed_avg_pct":0.12,"gap_pct":0.34,"welch_t":2.59,"divergence_count":517,"confirmed_count":3949},{"label":"10 sessions","divergence_avg_pct":0.79,"confirmed_avg_pct":0.28,"gap_pct":0.52,"welch_t":3.04,"divergence_count":517,"confirmed_count":3943},{"label":"20 sessions","divergence_avg_pct":1.53,"confirmed_avg_pct":0.51,"gap_pct":1.02,"welch_t":4.12,"divergence_count":516,"confirmed_count":3923}],"shape":"table","sql":"WITH\n    bars AS\n    (\n        SELECT\n            ticker,\n            date,\n            max(toFloat64(close))  AS close,\n            max(toFloat64(volume)) AS volume\n        FROM global_markets.stocks_daily_aggs\n        WHERE ticker IN ('MSFT', 'SPY', 'KO', 'JNJ', 'JPM', 'XOM', 'PG', 'PEP', 'MCD', 'HD')\n          AND date >= '2016-01-04'\n          AND date <= '2026-06-30'\n        GROUP BY ticker, date\n    ),\n    stepped AS\n    (\n        SELECT\n            ticker,\n            date,\n            close,\n            volume,\n            lagInFrame(close, 1) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close\n        FROM bars\n    ),\n    cumulative AS\n    (\n        SELECT\n            ticker,\n            date,\n            close,\n            sum(if(prev_close = 0, 0, if(close > prev_close, volume, if(close < prev_close, -volume, 0))))\n                OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS obv,\n            row_number() OVER (PARTITION BY ticker ORDER BY date ASC) AS bar_no\n        FROM stepped\n    ),\n    marked AS\n    (\n        SELECT\n            close,\n            obv,\n            bar_no,\n            max(close)             OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS high_20,\n            lagInFrame(obv, 20)    OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) AS obv_20_back,\n            leadInFrame(close, 5)  OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING)  AS close_fwd_5,\n            leadInFrame(close, 10) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING) AS close_fwd_10,\n            leadInFrame(close, 20) OVER (PARTITION BY ticker ORDER BY date ASC ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING) AS close_fwd_20\n        FROM cumulative\n    ),\n    events AS\n    (\n        SELECT\n            obv < obv_20_back AS is_divergent,\n            if(close_fwd_5  > 0, 100 * (close_fwd_5  / close - 1), NULL) AS ret_5,\n            if(close_fwd_10 > 0, 100 * (close_fwd_10 / close - 1), NULL) AS ret_10,\n            if(close_fwd_20 > 0, 100 * (close_fwd_20 / close - 1), NULL) AS ret_20\n        FROM marked\n        WHERE bar_no > 21\n          AND close >= high_20\n    ),\n    long_form AS\n    (\n        SELECT\n            is_divergent,\n            horizon.1 AS label,\n            horizon.2 AS ret\n        FROM\n        (\n            SELECT\n                is_divergent,\n                arrayJoin([('5 sessions', ret_5), ('10 sessions', ret_10), ('20 sessions', ret_20)]) AS horizon\n            FROM events\n        )\n        WHERE isNotNull(horizon.2)\n    )\nSELECT\n    label,\n    round(avgIf(ret, is_divergent), 2)                                AS divergence_avg_pct,\n    round(avgIf(ret, NOT is_divergent), 2)                            AS confirmed_avg_pct,\n    round(avgIf(ret, is_divergent) - avgIf(ret, NOT is_divergent), 2) AS gap_pct,\n    round((avgIf(ret, is_divergent) - avgIf(ret, NOT is_divergent))\n          / sqrt(varSampIf(ret, is_divergent) / countIf(is_divergent)\n               + varSampIf(ret, NOT is_divergent) / countIf(NOT is_divergent)), 2) AS welch_t,\n    countIf(is_divergent)                                             AS divergence_count,\n    countIf(NOT is_divergent)                                         AS confirmed_count\nFROM long_form\nGROUP BY label\nHAVING divergence_count > 50 AND confirmed_count > 50\nORDER BY toUInt16OrZero(splitByChar(' ', label)[1]) ASC","computed_at":"2026-10-01T16:15:07.406944+00:00","elapsed":1.461572069}