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

160 lines
5.7 KiB
Python

"""
Output Formatter
Formats processed data to match SQL output format
"""
import pandas as pd
from typing import Dict, Any
class OutputFormatter:
"""Format processed flow data for API response"""
@staticmethod
def format_premium(value: float) -> str:
"""Format premium value (M, K, or plain number)"""
if pd.isna(value) or value is None:
return None
try:
abs_val = abs(float(value))
sign = '-' if float(value) < 0 else ''
if abs_val >= 1e6:
return f"{sign}{abs_val / 1e6:.2f} M"
elif abs_val >= 1e3:
return f"{sign}{int(round(abs_val / 1e3))} K"
else:
return f"{sign}{int(round(abs_val))}"
except (TypeError, ValueError):
return None
@staticmethod
def format_symbol_display(row: Dict[str, Any]) -> str:
"""Format symbol display line"""
direction = row.get('direction', '')
dir_count = row.get('dir_count', 0) or 0
symbol = row.get('Symbol') or row.get('symbol_norm', '')
session_bucket = row.get('session_bucket', '?')
near_alert_type = row.get('near_alert_type')
badge_round = row.get('badge_round', '')
badge_more = row.get('badge_more', '')
flash = row.get('flash', '')
premium_num = row.get('premium_num', 0) or 0
# Direction prefix
if direction == 'BULL':
prefix = f"({dir_count}🟩) "
elif direction == 'BEAR':
prefix = f"({dir_count}🟥) "
else:
prefix = ""
# Catalyst indicator
catalyst = "" if near_alert_type else ""
# Badges
badges = f" {badge_round}{badge_more}" if (badge_round or badge_more) else ""
# Fire emoji
if premium_num > 1000000:
fire = " 🔥"
elif premium_num > 500000:
fire = " 💵"
elif premium_num > 100000:
fire = " "
else:
fire = ""
return f"{prefix}{symbol} · {session_bucket}{catalyst}{badges}{flash}{fire}"
@staticmethod
def format_tape_align(row: Dict[str, Any]) -> str:
"""Format tape alignment arrow"""
tape_alignment = row.get('tape_alignment', 0)
direction = row.get('direction', '')
if tape_alignment == 1:
return '↗︎' if direction == 'BULL' else '↘︎'
return ''
@staticmethod
def format_time(ts_local) -> str:
"""Format time as HH12:MI:SS AM"""
if pd.isna(ts_local):
return None
if isinstance(ts_local, str):
return ts_local
try:
return ts_local.strftime('%I:%M:%S %p')
except (AttributeError, ValueError):
return None
@staticmethod
def format_final_output(df: pd.DataFrame) -> pd.DataFrame:
"""Format final output to match SQL query format"""
if df.empty:
return df
df = df.copy()
# Format dates and times (handle missing columns)
if 'flow_ts_local' in df.columns:
df['CreatedDate'] = pd.to_datetime(df['flow_ts_local'], errors='coerce').dt.date
df['CreatedTime'] = df['flow_ts_local'].apply(OutputFormatter.format_time)
else:
df['CreatedDate'] = None
df['CreatedTime'] = None
# Format symbol display (handle missing columns)
if all(col in df.columns for col in ['direction', 'dir_count', 'Symbol', 'symbol_norm', 'session_bucket']):
df['Symbol'] = df.apply(OutputFormatter.format_symbol_display, axis=1)
elif 'Symbol' in df.columns or 'symbol_norm' in df.columns:
df['Symbol'] = df.get('Symbol', df.get('symbol_norm', ''))
# Format premiums
df['NetPremium'] = df.apply(
lambda row: OutputFormatter.format_premium(
(row.get('bull_total', 0) or 0) - (row.get('bear_total', 0) or 0)
),
axis=1
)
df['Premium'] = df.apply(
lambda row: OutputFormatter.format_premium(row.get('premium_num')),
axis=1
)
# Format tape alignment
df['TapeAlign'] = df.apply(OutputFormatter.format_tape_align, axis=1)
# Rename columns to match SQL output
column_mapping = {
'pct_vs_prior_close': 'PctVsPriorClose',
'pct_vs_rth_open': 'PctVsRthOpen',
'pct_5m_momo': 'Pct5m',
'pct_15m_momo': 'Pct15m',
'near_alert_type': 'NearAlert',
'Rocket_with_mny': 'Rocket',
}
# Only rename columns that exist (don't drop Phase 1 columns)
existing_mapping = {k: v for k, v in column_mapping.items() if k in df.columns}
df = df.rename(columns=existing_mapping)
# Ensure Phase 1 columns are preserved (don't drop them)
# Phase 1 columns should remain as-is (signal_tier, checklist_score, etc.)
# Ensure all new institutional analytics columns are preserved:
# - premium_zscore, premium_percentile_intraday, relative_premium_score
# - aggression_score, size_concentration_score, repeat_trade_velocity, strike_clustering_score, signal_strength
# - early_noise_reject, flow_acceleration, time_between_hits, follow_on_ratio, strike_laddering_detected
# - delta_exposure, gamma_exposure, volatility_intent
# - net_gamma_exposure_per_symbol, gamma_flip_proximity, dealer_hedge_pressure_score
# - market_regime, flow_state
# - confidence_score, institutional_likelihood, dealer_pain_level, expected_move_vs_implied
return df