190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
"""
|
|
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
|
|
|