STRASMORE/EXPLORE 2,225 QUERIES

forward_record

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-09-13, from can-a-death-cross-be-bullish.

as of table 4×9read in context →
forward_record — 4 rows by 9 columns, computed from US exchange, SIP and OPRA data.
horizoncase_counthigher_counthigher_pctall_sessions_higher_pctavg_return_pctmedian_return_pctworst_pctbest_pct
1 month116556622.8-8.912.1
3 months11982725.67.1-10.116.4
6 months118737610.611.9-11.427.7
12 months119828214.719.1-41.250.9
Rows × columns
4 × 9
Computed
Completeness
No missing values
Source
US exchange, SIP and OPRA market data
Licence
Strasmore terms · free, no signup
Formats
JSON · CSV · the SQL below

What each column holds

Column definitions for forward_record, derived from the stored result.
ColumnTypeRangeNotes
horizon text 4 distinct values (1 month, 12 months, 3 months…)
case_count number every row is 11 count
higher_count number 6 to 9 count
higher_pct number 55 to 82 percent
all_sessions_higher_pct number 66 to 82 percent
avg_return_pct number 2 to 14.7 percent
median_return_pct number 2.8 to 19.1 percent
worst_pct number -41.2 to -8.9 percent
best_pct number 12.1 to 50.9 percent

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.

the exact SQL behind every number
WITH
bars AS
(
    SELECT
        date,
        toFloat64(argMax(close, _ingest_time)) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
    GROUP BY date
),
smas AS
(
    SELECT
        date,
        px,
        row_number() OVER (ORDER BY date) AS rn,
        avg(px) OVER (ORDER BY date ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS avg_50,
        avg(px) OVER (ORDER BY date ROWS BETWEEN 199 PRECEDING AND CURRENT ROW) AS avg_200,
        max(px) OVER (ORDER BY date ROWS BETWEEN 251 PRECEDING AND CURRENT ROW) AS high_1y,
        leadInFrame(px, 21) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS px_1m,
        leadInFrame(px, 63) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS px_3m,
        leadInFrame(px, 126) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS px_6m,
        leadInFrame(px, 252) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS px_12m
    FROM bars
),
states AS
(
    SELECT
        *,
        (avg_50 < avg_200) AS below,
        lagInFrame((avg_50 < avg_200), 1) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS prev_below
    FROM smas
),
flags AS
(
    SELECT
        *,
        (rn >= 201 AND below = 1 AND prev_below = 0) AS is_death,
        (rn >= 201 AND below = 0 AND prev_below = 1) AS is_golden
    FROM states
),
unrolled AS
(
    SELECT
        is_death,
        tupleElement(h, 1) AS horizon_order,
        tupleElement(h, 2) AS horizon,
        tupleElement(h, 3) AS fwd_px,
        px
    FROM flags
    ARRAY JOIN [(1, '1 month', px_1m), (2, '3 months', px_3m), (3, '6 months', px_6m), (4, '12 months', px_12m)] AS h
    WHERE rn >= 201
      AND tupleElement(h, 3) > 0
)
SELECT
    horizon,
    countIf(is_death)                                                            AS case_count,
    countIf(is_death AND fwd_px > px)                                            AS higher_count,
    toInt32(round(100 * countIf(is_death AND fwd_px > px) / countIf(is_death)))  AS higher_pct,
    toInt32(round(100 * countIf(fwd_px > px) / count()))                         AS all_sessions_higher_pct,
    round(avgIf((fwd_px / px - 1) * 100, is_death), 1)                           AS avg_return_pct,
    round(quantileExactIf((fwd_px / px - 1) * 100, is_death), 1)                 AS median_return_pct,
    round(minIf((fwd_px / px - 1) * 100, is_death), 1)                           AS worst_pct,
    round(maxIf((fwd_px / px - 1) * 100, is_death), 1)                           AS best_pct
FROM unrolled
GROUP BY horizon_order, horizon
HAVING countIf(is_death) > 0
ORDER BY horizon_order

Run your own version of this

The same 22 years of US equities and 12 years of options data are queryable in SQL or plain English. A free account runs 100 queries a day and takes no card.

More from this analysiscan-a-death-cross-be-bullish
death_crosses table 11×6 trace_2020 series 126×4 whipsaw ranking 11×4 window scalar 1×11 covid_2020 scalar 1×16 The 2s10s spread, every print of the half table 124×2 See all 2,225 queries →