160 lines
5.7 KiB
Python
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
|
|
|