institutional-trader/backend/python_service/services/dealer_flow_context.py

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