{"slug":"backtesting-in-illiquid-markets","qid":"impact_curve","label":"impact_curve","post_title":"backtesting-in-illiquid-markets","post_url":"/blog/backtesting-in-illiquid-markets#q-impact_curve","columns":["volume_quintile","thin_abs_move_bps","liquid_abs_move_bps","thin_over_liquid"],"rows":[{"volume_quintile":"Q1","thin_abs_move_bps":1.2,"liquid_abs_move_bps":1.1,"thin_over_liquid":1.18},{"volume_quintile":"Q2","thin_abs_move_bps":2.8,"liquid_abs_move_bps":1.3,"thin_over_liquid":2.09},{"volume_quintile":"Q3","thin_abs_move_bps":3.9,"liquid_abs_move_bps":1.7,"thin_over_liquid":2.33},{"volume_quintile":"Q4","thin_abs_move_bps":5.3,"liquid_abs_move_bps":2,"thin_over_liquid":2.61},{"volume_quintile":"Q5","thin_abs_move_bps":7.9,"liquid_abs_move_bps":2.7,"thin_over_liquid":2.95}],"shape":"ranking","sql":"WITH bars AS\n(\n    SELECT\n        ticker,\n        toStartOfMinute(toTimeZone(window_start, 'America/New_York')) AS et_minute,\n        toFloat64(open)   AS open_px,\n        toFloat64(close)  AS close_px,\n        toFloat64(volume) AS vol\n    FROM global_markets.delayed_stocks_minute_aggs\n    WHERE ticker IN ('SPY', 'AAPL', 'KO', 'SJM', 'LANC')\n      AND window_start >= '2026-07-01 00:00:00'\n      AND window_start <  '2026-09-26 00:00:00'\n      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60\n           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570\n      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60\n           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960\n      AND volume > 0\n      AND open > 0\n),\nranked AS\n(\n    SELECT\n        ticker,\n        avg(vol) AS avg_minute_vol\n    FROM bars\n    GROUP BY ticker\n    HAVING count() >= 500\n),\npicks AS\n(\n    SELECT\n        argMax(ticker, avg_minute_vol) AS liquid_ticker,\n        argMin(ticker, avg_minute_vol) AS thin_ticker\n    FROM ranked\n),\nsided AS\n(\n    SELECT\n        if(b.ticker = p.thin_ticker, 'thin', 'liquid') AS side,\n        b.et_minute                                    AS et_minute,\n        b.open_px                                      AS open_px,\n        b.close_px                                     AS close_px,\n        b.vol                                          AS vol\n    FROM bars AS b\n    CROSS JOIN picks AS p\n    WHERE b.ticker = p.thin_ticker\n       OR b.ticker = p.liquid_ticker\n),\ncuts AS\n(\n    SELECT\n        side,\n        quantilesDeterministic(0.2, 0.4, 0.6, 0.8)(vol, toUInt64(toUnixTimestamp(et_minute))) AS edges\n    FROM sided\n    GROUP BY side\n),\ntagged AS\n(\n    SELECT\n        s.side                                            AS side,\n        1 + length(arrayFilter(x -> s.vol >= x, c.edges)) AS quintile,\n        abs(s.close_px / s.open_px - 1) * 10000           AS move_bps\n    FROM sided AS s\n    INNER JOIN cuts AS c ON c.side = s.side\n)\nSELECT\n    concat('Q', toString(quintile))             AS volume_quintile,\n    round(avgIf(move_bps, side = 'thin'), 1)    AS thin_abs_move_bps,\n    round(avgIf(move_bps, side = 'liquid'), 1)  AS liquid_abs_move_bps,\n    round(avgIf(move_bps, side = 'thin')\n          / avgIf(move_bps, side = 'liquid'), 2) AS thin_over_liquid\nFROM tagged\nGROUP BY quintile\nHAVING countIf(side = 'thin') > 0\n   AND countIf(side = 'liquid') > 0\nORDER BY quintile","computed_at":"2026-10-01T16:17:33.959891+00:00","elapsed":0.022736327}