""" Dealer-Aware Flow Context Service Tracks dealer hedging pressure, gamma exposure, and flow continuation patterns """ import pandas as pd import numpy as np from typing import Dict, Optional from datetime import timedelta from utils.logger import logger class DealerFlowContext: """ Analyzes flow from dealer perspective: - Net gamma exposure per symbol - Gamma flip proximity (when dealers become long/short gamma) - Dealer hedge pressure (forced hedging activity) """ def __init__(self): # Configuration self.analysis_window_minutes = 120 # Look back window for gamma tracking self.gamma_flip_threshold = 0.3 # Gamma flip when net gamma crosses threshold def calculate_net_gamma_exposure_per_symbol( self, df: pd.DataFrame, symbol: str, timestamp: pd.Timestamp ) -> float: """ Calculate net gamma exposure for a symbol at a given time. Sums all gamma exposures from recent flow. Positive = dealers are long gamma (hedging by selling on rallies, buying on dips) Negative = dealers are short gamma (hedging by buying on rallies, selling on dips) """ if df.empty: return 0.0 # Look at flow up to this timestamp window_start = timestamp - timedelta(minutes=self.analysis_window_minutes) mask = ( (df['symbol_norm'] == symbol.upper()) & (df['flow_ts_utc'] >= window_start) & (df['flow_ts_utc'] <= timestamp) & (df['gamma_exposure'].notna()) ) recent_flow = df[mask] if recent_flow.empty: return 0.0 # Sum gamma exposures net_gamma = recent_flow['gamma_exposure'].sum() return float(net_gamma) def calculate_gamma_flip_proximity( self, df: pd.DataFrame, row_idx: int ) -> Optional[float]: """ Calculate proximity to gamma flip (when dealers switch from long to short gamma or vice versa). Returns: -1.0 to 1.0 - Positive = approaching long gamma (dealers becoming volatility buyers) - Negative = approaching short gamma (dealers becoming volatility sellers) - 0.0 = near flip point """ if row_idx >= len(df): return None current_row = df.iloc[row_idx] symbol = current_row.get('symbol_norm') flow_ts_utc = current_row.get('flow_ts_utc') gamma_exposure = current_row.get('gamma_exposure', 0) or 0 if pd.isna(symbol) or pd.isna(flow_ts_utc): return None # Calculate current net gamma net_gamma = self.calculate_net_gamma_exposure_per_symbol(df, symbol, flow_ts_utc) # Normalize to -1 to 1 scale # Use exponential scaling to emphasize near-flip conditions if net_gamma == 0: return 0.0 # Simple normalization: divide by a large threshold # More sophisticated: use percentile or adaptive scaling threshold = 1000000000 # 1B in gamma exposure as normalization factor normalized = net_gamma / threshold # Clamp to -1 to 1 normalized = max(-1.0, min(1.0, normalized)) # Invert: negative normalized = positive proximity (approaching long gamma) # This is because dealer short gamma (negative) means they're selling volatility return float(-normalized) def calculate_dealer_hedge_pressure_score( self, df: pd.DataFrame, row_idx: int ) -> float: """ Calculate dealer hedge pressure score (0-100). Higher score = more forced hedging by dealers: - High net gamma exposure (dealers must hedge) - Recent flow creating gamma imbalance - Flow continuation (dealers hedging creates more flow) """ if row_idx >= len(df): return 0.0 current_row = df.iloc[row_idx] symbol = current_row.get('symbol_norm') flow_ts_utc = current_row.get('flow_ts_utc') gamma_exposure = current_row.get('gamma_exposure', 0) or 0 if pd.isna(symbol) or pd.isna(flow_ts_utc): return 0.0 score = 0.0 # Net gamma component (0-40 points) # High absolute net gamma = more hedge pressure net_gamma = abs(self.calculate_net_gamma_exposure_per_symbol(df, symbol, flow_ts_utc)) if net_gamma > 0: # Normalize: 500M = 20 points, 1B = 40 points normalized_gamma = min(1.0, net_gamma / 1000000000) # 1B threshold score += normalized_gamma * 40.0 # Recent gamma accumulation (0-30 points) # Recent flow creating gamma imbalance window_start = flow_ts_utc - timedelta(minutes=30) mask = ( (df['symbol_norm'] == symbol.upper()) & (df['flow_ts_utc'] >= window_start) & (df['flow_ts_utc'] <= flow_ts_utc) & (df['gamma_exposure'].notna()) ) recent_gamma = df[mask]['gamma_exposure'].sum() if abs(recent_gamma) > 0: # Normalize recent gamma accumulation normalized_recent = min(1.0, abs(recent_gamma) / 500000000) # 500M threshold score += normalized_recent * 30.0 # Flow continuation component (0-30 points) # If flow is continuing in same direction, dealers are likely hedging follow_on_ratio = current_row.get('follow_on_ratio') if follow_on_ratio is not None and not pd.isna(follow_on_ratio): # High follow-on ratio = continuation = hedge pressure score += follow_on_ratio * 30.0 return min(100.0, max(0.0, score)) def validate_flow_continuation( self, df: pd.DataFrame, row_idx: int ) -> bool: """ Validate if flow continuation is likely based on dealer hedge pressure. Returns True if continuation is expected (dealers forced to hedge). """ current_row = df.iloc[row_idx] dealer_pressure = current_row.get('dealer_hedge_pressure_score', 0) or 0 net_gamma = current_row.get('net_gamma_exposure_per_symbol', 0) or 0 # High dealer pressure + significant gamma exposure = continuation likely if dealer_pressure > 50.0 and abs(net_gamma) > 100000000: # 100M threshold return True return False def validate_flow_reversal( self, df: pd.DataFrame, row_idx: int ) -> bool: """ Validate if flow reversal is likely. Reversal happens when: - Dealers finish hedging (gamma exposure neutralizes) - Price fails at key levels (VWAP, opening range) - Flow shows distribution pattern (decreasing premium, widening gaps) """ current_row = df.iloc[row_idx] dealer_pressure = current_row.get('dealer_hedge_pressure_score', 0) or 0 follow_on_ratio = current_row.get('follow_on_ratio') flow_acceleration = current_row.get('flow_acceleration') # Low dealer pressure + low follow-on ratio = reversal likely if dealer_pressure < 30.0: if follow_on_ratio is not None and follow_on_ratio < 0.3: return True # Negative flow acceleration = flow weakening = reversal if flow_acceleration is not None and flow_acceleration < -10000: return True return False def enrich_with_dealer_context(self, df: pd.DataFrame) -> pd.DataFrame: """ Add dealer-aware flow context metrics to DataFrame. Adds: net_gamma_exposure_per_symbol, gamma_flip_proximity, dealer_hedge_pressure_score """ if df.empty: return df df = df.copy() # Initialize columns df['net_gamma_exposure_per_symbol'] = 0.0 df['gamma_flip_proximity'] = None df['dealer_hedge_pressure_score'] = 0.0 # Sort by timestamp for proper gamma tracking df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True) # Calculate metrics for each row for idx in df_sorted.index: original_idx = df_sorted.index[idx] row = df_sorted.iloc[idx] symbol = row.get('symbol_norm') flow_ts_utc = row.get('flow_ts_utc') if pd.notna(symbol) and pd.notna(flow_ts_utc): # Net gamma exposure net_gamma = self.calculate_net_gamma_exposure_per_symbol( df_sorted, symbol, flow_ts_utc ) df.at[original_idx, 'net_gamma_exposure_per_symbol'] = float(net_gamma) # Gamma flip proximity flip_prox = self.calculate_gamma_flip_proximity(df_sorted, idx) if flip_prox is not None: df.at[original_idx, 'gamma_flip_proximity'] = float(flip_prox) # Dealer hedge pressure pressure = self.calculate_dealer_hedge_pressure_score(df_sorted, idx) df.at[original_idx, 'dealer_hedge_pressure_score'] = float(pressure) logger.info(f"Dealer context enrichment complete. Mean pressure: {df['dealer_hedge_pressure_score'].mean():.2f}") return df