/* =========================== PRE-MARKET SCANNER (FIXED) =========================== */ with knobs as ( select null::text as bucket_name, -- 'SEMIS' | 'BANKS' | NULL/'ALL' 0.0::numeric as min_abs_gap_pct, -- tighten later (e.g., 1.0) 0.0::numeric as min_rvol, -- tighten later (e.g., 1.2) false as require_alert ), pick_day as ( select max(d_et)::date as d0 from public.minute_bars_et where session='rth' ), universe as ( select distinct m.symbol from public.minute_bars_et m, pick_day d, knobs k where m.d_et=d.d0 and (k.bucket_name is null or k.bucket_name='ALL' or m.symbol in (select symbol from public.bucket_members where bucket=k.bucket_name)) ), -- PRE-MARKET aggregates pm as ( select m.symbol, m.d_et, count(*) as pm_bars, min(m.low) as pm_low_raw, max(m.high) as pm_high_raw, sum(m.volume)::numeric as pm_vol_raw, (select close from public.minute_bars_et x where x.symbol=m.symbol and x.d_et=m.d_et and x.session='pre' order by x.ts desc limit 1) as pm_last_raw from public.minute_bars_et m, pick_day d where m.d_et=d.d0 and m.session='pre' and m.symbol in (select symbol from universe) group by m.symbol, m.d_et ), -- 09:30 open (ET) open_0930 as ( select u.symbol, (select o.open from public.minute_bars_et o, pick_day d where o.symbol=u.symbol and o.d_et=d.d0 and o.session='rth' and o.hh=9 and o.mm=30 limit 1) as open_0930 from universe u ), -- Opening range 09:30–09:34 as fallback or5 as ( select m.symbol, m.d_et, max(m.high) as or_hi, min(m.low) as or_lo, sum(m.volume)::numeric as or_vol from public.minute_bars_et m, pick_day d where m.d_et=d.d0 and m.session='rth' and (m.hh=9 and m.mm between 30 and 34) and m.symbol in (select symbol from universe) group by m.symbol, m.d_et ), -- Prev trading day close/high prev_day as ( select distinct on (d.symbol) d.symbol, d.close as prior_close, d.high as pdh from public.daily_bars d, pick_day pd where d.date < pd.d0 order by d.symbol, d.date desc ), -- 20-session PM average (RVOL denom) hist_pm as ( select symbol, avg(pm_vol)::numeric as avg20_pm_vol from ( select symbol, d_et, sum(volume)::numeric as pm_vol from public.minute_bars_et, pick_day d where session='pre' and d_et >= (d.d0 - interval '40 day')::date and d_et < d.d0 group by symbol, d_et ) t group by symbol ), -- 20-session OR-5 average (fallback RVOL denom) hist_or as ( select symbol, avg(or_vol)::numeric as avg20_or_vol from ( select m.symbol, m.d_et, sum(m.volume)::numeric as or_vol from public.minute_bars_et m where m.session='rth' and (m.hh=9 and m.mm between 30 and 34) group by m.symbol, m.d_et ) x where x.d_et < (select d0 from pick_day) group by symbol ), -- Alerts (read CamelCase table safely) alerts_raw as ( select case when coalesce(to_jsonb(a)->>'Date', to_jsonb(a)->>'date', to_jsonb(a)->>'AlertDate', to_jsonb(a)->>'alert_date') ~ '^\d{4}-\d{2}-\d{2}$' then coalesce(to_jsonb(a)->>'Date', to_jsonb(a)->>'date', to_jsonb(a)->>'AlertDate', to_jsonb(a)->>'alert_date')::date when coalesce(to_jsonb(a)->>'Date', to_jsonb(a)->>'date', to_jsonb(a)->>'AlertDate', to_jsonb(a)->>'alert_date') ~ '^\d{2}/\d{2}/\d{4}$' then to_date(coalesce(to_jsonb(a)->>'Date', to_jsonb(a)->>'date', to_jsonb(a)->>'AlertDate', to_jsonb(a)->>'alert_date'), 'MM/DD/YYYY') else null end as alert_date, coalesce(to_jsonb(a)->>'Ticker', to_jsonb(a)->>'ticker', to_jsonb(a)->>'Symbol', to_jsonb(a)->>'symbol') as symbol, coalesce(to_jsonb(a)->>'Type', to_jsonb(a)->>'type') as alert_type, nullif(regexp_replace(coalesce(to_jsonb(a)->>'Notional', to_jsonb(a)->>'notional'), '[,\s]', '', 'g'), '')::numeric as notional, nullif(regexp_replace(coalesce(to_jsonb(a)->>'Pct_of_Avg30Day', to_jsonb(a)->>'pct_of_avg30day', to_jsonb(a)->>'Pct_of_Avg30', to_jsonb(a)->>'pct_of_avg30'), '[,%\s]', '', 'g'), '')::numeric as pct_of_avg30 from public."AlertStream_monthly" a ), alerts as ( select r.symbol, count(*) as alert_count, max(r.notional) as max_alert_notional, max(r.pct_of_avg30) as max_pct_of_avg30, string_agg(distinct r.alert_type, ', ' order by r.alert_type) as alert_types from alerts_raw r, pick_day d where r.alert_date = d.d0 and r.symbol in (select symbol from universe) group by r.symbol ), scan as ( select u.symbol, -- reference price (PM last → 09:30 → prior close) coalesce(pm.pm_last_raw, o.open_0930, pd.prior_close) as ref_price, pd.prior_close, round( (100 * (coalesce(pm.pm_last_raw, o.open_0930, pd.prior_close) / nullif(pd.prior_close,0.0) - 1))::numeric, 2 ) as pm_gap_pct, -- activity = PM if exists, else OR5 coalesce(pm.pm_high_raw, or5.or_hi, o.open_0930, pd.prior_close) as activity_high, coalesce(pm.pm_low_raw, or5.or_lo, o.open_0930, pd.prior_close) as activity_low, coalesce(pm.pm_vol_raw, or5.or_vol, 0)::numeric as activity_vol, case when coalesce(pm.pm_bars,0) > 0 and coalesce(pm.pm_last_raw,0) <> 0 then round( (100 * ((pm.pm_high_raw - pm.pm_low_raw) / nullif(pm.pm_last_raw,0.0)))::numeric, 2 ) when or5.or_hi is not null and or5.or_lo is not null and o.open_0930 is not null then round( (100 * ((or5.or_hi - or5.or_lo) / nullif(o.open_0930,0.0)))::numeric, 2 ) else 0::numeric end as activity_range_pct, -- activity RVOL (PM RVOL if PM exists, else OR5 RVOL) case when coalesce(pm.pm_bars,0) > 0 then coalesce(round( (pm.pm_vol_raw / nullif(hp.avg20_pm_vol,0))::numeric, 2 ), round( (or5.or_vol / nullif(ho.avg20_or_vol,0))::numeric, 2 ), 0) else coalesce(round( (or5.or_vol / nullif(ho.avg20_or_vol,0))::numeric, 2 ), 0) end as activity_rvol, -- PM-only fields (pm_rvol now falls back to OR5 rvol if PM avg is missing) coalesce(pm.pm_high_raw, or5.or_hi, o.open_0930, pd.prior_close) as pm_high, coalesce(pm.pm_low_raw, or5.or_lo, o.open_0930, pd.prior_close) as pm_low, coalesce(pm.pm_vol_raw, 0)::numeric as pm_vol, coalesce(round( (pm.pm_vol_raw / nullif(hp.avg20_pm_vol,0))::numeric, 2 ), round( (or5.or_vol / nullif(ho.avg20_or_vol,0))::numeric, 2 ), 0) as pm_rvol, -- <-- FIXED: fallback so it's not stuck at 0 coalesce(pm.pm_bars > 0, false) as has_premarket, o.open_0930, pd.pdh, -- "go" logic: PM high → OR5 high → PDH case when coalesce(pm.pm_bars,0) > 0 and pm.pm_high_raw is not null then o.open_0930 > pm.pm_high_raw when or5.or_hi is not null then o.open_0930 > or5.or_hi else o.open_0930 > pd.pdh end as gap_and_go_candidate, (o.open_0930 > pd.pdh) as open_above_pdh, -- alerts coalesce(a.alert_count,0) as alert_count, coalesce(a.alert_types,'') as alert_types, coalesce(a.max_alert_notional,0) as max_alert_notional, coalesce(a.max_pct_of_avg30,0) as max_pct_of_avg30, (a.symbol is not null) as has_alert from universe u left join pm on pm.symbol = u.symbol left join prev_day pd on pd.symbol = u.symbol left join open_0930 o on o.symbol = u.symbol left join or5 on or5.symbol = u.symbol left join hist_pm hp on hp.symbol = u.symbol left join hist_or ho on ho.symbol = u.symbol left join alerts a on a.symbol = u.symbol where pd.prior_close is not null and abs( (100 * (coalesce(pm.pm_last_raw, o.open_0930, pd.prior_close) / nullif(pd.prior_close,0.0) - 1)) ) >= (select min_abs_gap_pct from knobs) and ( (coalesce(pm.pm_bars,0) > 0 and coalesce((pm.pm_vol_raw / nullif(hp.avg20_pm_vol,0)), (or5.or_vol / nullif(ho.avg20_or_vol,0)), 0) >= (select min_rvol from knobs)) or (coalesce(pm.pm_bars,0) = 0 and (coalesce(or5.or_vol,0) / nullif(ho.avg20_or_vol,0)) >= (select min_rvol from knobs)) or (select min_rvol from knobs) = 0 ) and (not (select require_alert from knobs) or a.symbol is not null) ) select * from scan order by abs(pm_gap_pct) desc nulls last, pm_gap_pct desc; /*Three ready‑to‑trade playbooks 1) Gap‑and‑Go (trend continuation from the bell) Objective: ride early momentum in the gap direction. Scan: abs(pm_gap_pct) ≥ 1–3 (ticker‑dependent) activity_rvol ≥ 1.2 gap_and_go_candidate = true Optional: has_alert = true AND alert_types ~ '(Block|Options|AnnualHigh)' Plan (long example): Trigger: break and hold above activity_high (PMH if PM exists; else OR‑5‑high). Stop: below activity_low or below VWAP after reclaim. First target: +1R or measured move = (activity_high − activity_low) added to the breakout. Trail: under 5/15‑min swing lows or VWAP. When to stand down: activity_range_pct ≥ 5–7% and activity_rvol < 1.0 → often prone to early fades. 2) Gap‑Fade to VWAP/Fill (mean‑reversion) Objective: fade exhausted gaps back toward prior close or VWAP. Scan: abs(pm_gap_pct) ≥ 3–5 activity_rvol ≤ 0.9 (quiet) has_alert = false (or alert_types lacks “Block/Options/AnnualHigh”) Optional: open_above_pdh = false for gap‑ups; reverse for gap‑downs. Plan (short example on gap‑up): Trigger: failure at activity_high (wick through and close back below) or VWAP rejection. Stop: above the failure wick or above activity_high. Targets: VWAP → prior_close → 50% gap‑fill. Abort: if reclaim of activity_high on volume (activity_rvol jumps >1.2), switch to Plan 1. 3) OR‑Break + Alert Confluence (no PM, but institutional tone) Objective: trade names without PM that show strong OR‑5 and alert footprint. Scan: has_premarket = false activity_rvol ≥ 1.3 alert_count > 0 and alert_types ~ '(Block|Options|AnnualHigh)' open_above_pdh = true (long bias) or below PDL for shorts. Plan (long): Trigger: break of activity_high (OR‑5 high) with a quick retest. Stop: under activity_low. Targets: +1R → prior‑day range extension → round numbers. Practical filters that work Liquidity guardrails: ref_price ≥ 5 and activity_vol ≥ 100k (adjust per strategy). Momentum set: abs(pm_gap_pct) ≥ 2, activity_rvol ≥ 1.3, activity_range_pct ≥ 2, has_alert OR open_above_pdh. Fade set: abs(pm_gap_pct) ≥ 4, activity_rvol ≤ 0.9, open_above_pdh = false (for gap‑ups), no “Block/Options/AnnualHigh” alert. How to turn columns into orders (repeatable process) Sort by abs(pm_gap_pct) to see the tape’s gravity wells. Tag each symbol as PM or OR5 mode: If has_premarket = true, your activity_ = PM*. Else, activity_ = OR‑5* (treat those levels as your “opening range”). Keep only what’s busy: activity_rvol ≥ 1.2 (momentum) or ≤ 0.9 (fade). Layer alerts: prioritize has_alert and the heavy types (Block, Options, AnnualHigh). Plan levels: Longs → buy breaks/retests of activity_high; risk to activity_low. Shorts → sell breaks/rejections of activity_low; risk to activity_high. Size off the range: position size = risk_$ / (activity_high − activity_low). Manage: partial at +1R, trail under swings or VWAP; stop if the thesis (level/VWAP) is lost. Quick sanity checks you can apply on your sheet If has_premarket = false across the board, your data source isn’t fetching extended hours. The scanner still works (OR‑5 mode), but PM‑specific fields will be quiet. If pm_rvol is 0 yet activity_rvol > 0, that’s expected in OR‑5 mode; work with activity_rvol for decisions. If gap_and_go_candidate = true with has_premarket = false, that means open > OR‑5 high (a legit momentum tell even without PM). TL;DR mapping: column → decisions pm_gap_pct → rank & bias (biggest ± first). activity_high/low → breakout/stop levels. activity_vol & activity_rvol → participation; ≥1.2 momentum, ≤0.9 fade. activity_range_pct → expansion vs reversion regime. has_premarket → interpret activity_ as PM or OR‑5. open_above_pdh / gap_and_go_candidate → trend‑day potential and early go/no‑go. alerts (count/types/notional) → catalyst & conviction filter. When you glue those together—a bias (gap), proof of life (RVOL), clean levels (activity high/low), and a catalyst (alerts)—you’ve got a trade that’s both findable and repeatable.*/