/* ===========================
   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.*/