""" Price Reaction Tracker Tracks how price moves after a signal appears - critical for filtering hedges/rolls Uses Yahoo Finance for real-time data instead of database """ import pandas as pd import asyncpg from datetime import datetime, timedelta from typing import Dict, Optional from utils.logger import logger from services.yahoo_finance_service import YahooFinanceService class PriceReactionTracker: """Service for tracking price reactions after flow signals""" def __init__(self): self.reaction_threshold_pct = 0.5 # 0.5% threshold for "flow led to move" self.use_yahoo_finance = False # Temporarily disabled - focus on Phase 1 button self.yahoo_service = YahooFinanceService() if self.use_yahoo_finance else None async def get_price_at_time( self, symbol: str, timestamp: datetime, pool: asyncpg.Pool = None # Not used anymore, kept for compatibility ) -> Optional[float]: """Get price at or before a specific timestamp using Yahoo Finance""" if not self.use_yahoo_finance or not self.yahoo_service: return None # Yahoo Finance disabled try: return self.yahoo_service.get_price_at_time(symbol, timestamp) except Exception as e: logger.debug(f"Error getting price at time from Yahoo Finance: {e}") return None async def enrich_with_reactions( self, flow_df: pd.DataFrame, pool: asyncpg.Pool ) -> pd.DataFrame: """Enrich flow data with price reaction tracking""" df = flow_df.copy() if df.empty: return df logger.info(f"Tracking price reactions for {len(df)} signals...") # Get unique symbols and timestamps for batch processing unique_symbols = df['symbol_norm'].unique().tolist() # Batch fetch price reactions reaction_data = [] async with pool.acquire() as conn: for idx, row in df.iterrows(): symbol = row['symbol_norm'] signal_time = row['flow_ts_utc'] price_at_signal = row.get('u_close') if pd.isna(signal_time) or not price_at_signal or price_at_signal == 0: reaction_data.append({ 'price_reaction_5m_pct': None, 'price_reaction_15m_pct': None, 'price_reaction_30m_pct': None, 'high_break_5m': False, 'low_break_5m': False, 'flow_led_to_move': False }) continue # Get prices at 5m, 15m, 30m after signal price_5m = None price_15m = None price_30m = None try: ts_5m = signal_time + timedelta(minutes=5) price_5m = await self.get_price_at_time(symbol, ts_5m, pool) except Exception as e: logger.debug(f"Error fetching 5m price for {symbol}: {e}") try: ts_15m = signal_time + timedelta(minutes=15) price_15m = await self.get_price_at_time(symbol, ts_15m, pool) except Exception as e: logger.debug(f"Error fetching 15m price for {symbol}: {e}") try: ts_30m = signal_time + timedelta(minutes=30) price_30m = await self.get_price_at_time(symbol, ts_30m, pool) except Exception as e: logger.debug(f"Error fetching 30m price for {symbol}: {e}") # Calculate reaction percentages reaction_5m = None reaction_15m = None reaction_30m = None if price_5m and price_at_signal: try: reaction_5m = round(((price_5m - price_at_signal) / price_at_signal) * 100, 2) except (TypeError, ZeroDivisionError): pass if price_15m and price_at_signal: try: reaction_15m = round(((price_15m - price_at_signal) / price_at_signal) * 100, 2) except (TypeError, ZeroDivisionError): pass if price_30m and price_at_signal: try: reaction_30m = round(((price_30m - price_at_signal) / price_at_signal) * 100, 2) except (TypeError, ZeroDivisionError): pass # High/Low break confirmation high_at_signal = row.get('u_high', 0) or 0 low_at_signal = row.get('u_low', 0) or 0 high_break_5m = False low_break_5m = False if price_5m: if high_at_signal > 0 and price_5m > high_at_signal: high_break_5m = True if low_at_signal > 0 and price_5m < low_at_signal: low_break_5m = True # Determine if flow led to move flow_led_to_move = False if reaction_5m is not None: flow_led_to_move = abs(reaction_5m) >= self.reaction_threshold_pct reaction_data.append({ 'price_reaction_5m_pct': reaction_5m, 'price_reaction_15m_pct': reaction_15m, 'price_reaction_30m_pct': reaction_30m, 'high_break_5m': high_break_5m, 'low_break_5m': low_break_5m, 'flow_led_to_move': flow_led_to_move }) # Merge reaction data reaction_df = pd.DataFrame(reaction_data, index=df.index) df = pd.concat([df, reaction_df], axis=1) logger.info(f"✅ Price reaction tracking complete. {df['flow_led_to_move'].sum()} signals led to price moves") return df