""" Institutional Confidence Metrics Service Calculates confidence scores for institutional flow signals """ import pandas as pd import numpy as np from typing import Optional from utils.logger import logger class InstitutionalConfidence: """ Calculates institutional confidence metrics: - confidence_score (0-100) - institutional_likelihood (0-1) - dealer_pain_level (0-100) - expected_move_vs_implied (ratio) """ def __init__(self): # Configuration self.min_premium_for_institutional = 200000 # Minimum premium for institutional classification def calculate_confidence_score(self, row: pd.Series) -> float: """ Calculate overall confidence score (0-100) combining multiple factors. Higher = more confidence in signal quality. """ score = 0.0 # Relative premium component (0-25 points) relative_premium = row.get('relative_premium_score', 0) or 0 score += (relative_premium / 100.0) * 25.0 # Signal strength component (0-25 points) signal_strength = row.get('signal_strength', 0) or 0 score += (signal_strength / 100.0) * 25.0 # Dealer pressure component (0-20 points) dealer_pressure = row.get('dealer_hedge_pressure_score', 0) or 0 score += (dealer_pressure / 100.0) * 20.0 # Flow continuation component (0-15 points) follow_on_ratio = row.get('follow_on_ratio') if follow_on_ratio is not None and not pd.isna(follow_on_ratio): score += follow_on_ratio * 15.0 # Strike laddering component (0-15 points) strike_laddering = row.get('strike_laddering_detected', False) if strike_laddering: score += 15.0 return min(100.0, max(0.0, score)) def calculate_institutional_likelihood(self, row: pd.Series) -> float: """ Calculate likelihood that flow is institutional (0-1). Based on premium size, trade characteristics, and patterns. """ premium_num = row.get('premium_num', 0) or 0 relative_premium = row.get('relative_premium_score', 0) or 0 size_concentration = row.get('size_concentration_score', 0) or 0 likelihood = 0.0 # Premium size component (0-40%) if premium_num >= 1000000: # $1M+ likelihood += 0.40 elif premium_num >= 500000: # $500K+ likelihood += 0.30 elif premium_num >= self.min_premium_for_institutional: likelihood += 0.20 # Relative premium component (0-30%) if relative_premium >= 80: likelihood += 0.30 elif relative_premium >= 60: likelihood += 0.20 elif relative_premium >= 40: likelihood += 0.10 # Size concentration component (0-30%) # Institutional trades are often concentrated if size_concentration >= 70: likelihood += 0.30 elif size_concentration >= 50: likelihood += 0.20 elif size_concentration >= 30: likelihood += 0.10 return min(1.0, max(0.0, likelihood)) def calculate_dealer_pain_level(self, row: pd.Series) -> float: """ Calculate dealer pain level (0-100). Higher = dealers in pain (large gamma exposure, forced to hedge). """ dealer_pressure = row.get('dealer_hedge_pressure_score', 0) or 0 net_gamma = abs(row.get('net_gamma_exposure_per_symbol', 0) or 0) gamma_flip_prox = row.get('gamma_flip_proximity') pain = 0.0 # Dealer pressure component (0-50 points) pain += (dealer_pressure / 100.0) * 50.0 # Gamma exposure component (0-30 points) # Large absolute gamma = more pain if net_gamma > 0: normalized_gamma = min(1.0, net_gamma / 1000000000) # 1B threshold pain += normalized_gamma * 30.0 # Gamma flip proximity component (0-20 points) # Near flip = high pain (dealers forced to adjust) if gamma_flip_prox is not None and not pd.isna(gamma_flip_prox): # Absolute value of proximity (closer to 0 = more pain) pain += (1.0 - abs(gamma_flip_prox)) * 20.0 return min(100.0, max(0.0, pain)) def calculate_expected_move_vs_implied(self, row: pd.Series) -> Optional[float]: """ Calculate expected move vs implied move ratio. Estimates expected move from flow characteristics vs implied volatility. Returns: ratio (expected_move / implied_move) - >1.0 = flow suggests larger move than implied - <1.0 = flow suggests smaller move than implied - None if cannot calculate """ # Simplified calculation: use premium and delta exposure as proxies premium_num = row.get('premium_num', 0) or 0 spot_num = row.get('spot_num', 0) or 0 delta_exposure = abs(row.get('delta_exposure', 0) or 0) if pd.isna(spot_num) or spot_num == 0: return None # Estimate expected move from premium paid # High premium relative to spot = expectation of larger move if premium_num > 0: prem_to_spot_ratio = premium_num / spot_num # Estimate implied move (simplified: assume 1% IV = 1% move expectation) # This is a placeholder - in production, use actual IV from options chain implied_move_pct = 2.0 # Default 2% implied move # Estimate expected move from premium # Premium of 2% of spot = expectation of ~2% move (rough approximation) expected_move_pct = prem_to_spot_ratio * 100.0 # Calculate ratio if implied_move_pct > 0: ratio = expected_move_pct / implied_move_pct return float(ratio) return None def enrich_with_confidence_metrics(self, df: pd.DataFrame) -> pd.DataFrame: """ Add institutional confidence metrics to DataFrame. Adds: confidence_score, institutional_likelihood, dealer_pain_level, expected_move_vs_implied """ if df.empty: return df df = df.copy() # Initialize columns df['confidence_score'] = 0.0 df['institutional_likelihood'] = 0.0 df['dealer_pain_level'] = 0.0 df['expected_move_vs_implied'] = None # Calculate metrics df['confidence_score'] = df.apply(self.calculate_confidence_score, axis=1) df['institutional_likelihood'] = df.apply(self.calculate_institutional_likelihood, axis=1) df['dealer_pain_level'] = df.apply(self.calculate_dealer_pain_level, axis=1) # Expected move vs implied (some rows may not have this) for idx in df.index: expected_move = self.calculate_expected_move_vs_implied(df.iloc[idx]) if expected_move is not None: df.at[idx, 'expected_move_vs_implied'] = float(expected_move) logger.info(f"Confidence metrics complete. Mean confidence: {df['confidence_score'].mean():.2f}") return df