257 lines
9.4 KiB
Python
257 lines
9.4 KiB
Python
"""
|
|
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
|
|
|