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

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