""" 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.) return df