from app.analysis.levels import Level, LevelKind, Side from app.bars.models import Bar, Timeframe from app.bars.session import bucket_start from app.config import MA_WEIGHT_FACTOR, TIMEFRAME_WEIGHT def build_vwap_level(minute_bars: list[Bar]) -> list[Level]: """Session VWAP, anchored to the CME session open. Institutional execution is benchmarked against VWAP, which is what earns it a place here: it is watched by people whose orders are large enough to move price, not merely by chartists. Anchoring uses the same 18:00 ET session boundary as the daily bars, so VWAP resets when the trading day does rather than at UTC midnight. """ if not minute_bars: return [] session_open = bucket_start(minute_bars[-1].t, Timeframe.D1) cumulative_pv = 0.0 cumulative_volume = 0 points: list[tuple[int, float]] = [] for bar in minute_bars: if bar.t < session_open: continue typical = (bar.h + bar.l + bar.c) / 3 cumulative_pv += typical * bar.v cumulative_volume += bar.v # Yahoo reports zero-volume minutes in thin overnight trade; they carry # no VWAP information and must not divide by zero. if cumulative_volume > 0: points.append((bar.t, cumulative_pv / cumulative_volume)) if not points: return [] current = points[-1][1] last_bar = minute_bars[-1] return [ Level( id="vwap:session", kind=LevelKind.VWAP, tf=Timeframe.D1, side=Side.SUPPORT if current <= last_bar.c else Side.RESISTANCE, weight=TIMEFRAME_WEIGHT[Timeframe.D1] * MA_WEIGHT_FACTOR, score=1.0, label="Session VWAP", anchor_t=points[-1][0], anchor_p=current, slope=0.0, points=points, touches=0, first_t=points[0][0], last_t=points[-1][0], provisional=not last_bar.closed, hidden=False, ) ]