Skip to main content

earnings volatility - how my algos adapt to quarterly chaos

earnings week chaos.

GOOGL, TSLA, META all this week.

how my algos handle it.

the earnings problem
#

normal day: VIX 15, predictable ranges, clean signals

earnings day: VIX spikes 20%+, gaps, reversals, noise

my strategies work in normal conditions.

earnings require different approach.

earnings detection system
#

import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Optional
import yfinance as yf

@dataclass
class EarningsEvent:
    symbol: str
    report_date: datetime
    report_time: str  # 'BMO' (before market open) or 'AMC' (after market close)
    expected_move: float  # implied by options
    historical_move_avg: float
    market_cap: float  # for weighting impact

class EarningsVolatilityAdapter:
    """
    Adapts trading parameters around earnings announcements
    """

    def __init__(self):
        self.earnings_calendar: List[EarningsEvent] = []
        self.major_symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META',
                             'TSLA', 'NVDA', 'JPM', 'BAC', 'WMT']
        self.index_impact_threshold = 0.5  # % of index weight

    def load_earnings_calendar(self, start_date: datetime, end_date: datetime):
        """
        Load earnings calendar for major movers
        """
        for symbol in self.major_symbols:
            try:
                ticker = yf.Ticker(symbol)
                earnings_dates = ticker.earnings_dates

                if earnings_dates is not None:
                    for date, row in earnings_dates.iterrows():
                        if start_date <= date.to_pydatetime() <= end_date:
                            self.earnings_calendar.append(EarningsEvent(
                                symbol=symbol,
                                report_date=date.to_pydatetime(),
                                report_time='AMC',  # would need additional data
                                expected_move=self._get_expected_move(symbol),
                                historical_move_avg=self._get_historical_move(symbol),
                                market_cap=ticker.info.get('marketCap', 0)
                            ))
            except Exception as e:
                print(f"Error loading {symbol}: {e}")

    def _get_expected_move(self, symbol: str) -> float:
        """Calculate expected move from ATM straddle price"""
        # Simplified - would use real options data
        return np.random.uniform(3, 8)  # typical 3-8% expected moves

    def _get_historical_move(self, symbol: str) -> float:
        """Get average historical earnings move"""
        # Would query historical data
        return np.random.uniform(4, 10)

    def get_earnings_impact(self, trade_date: datetime) -> dict:
        """
        Calculate earnings impact on trading for given date
        """
        # Check for same-day earnings
        same_day = [e for e in self.earnings_calendar
                   if e.report_date.date() == trade_date.date()]

        # Check for next-day earnings (AMC reports affect next day)
        next_day = [e for e in self.earnings_calendar
                   if e.report_date.date() == (trade_date - timedelta(days=1)).date()
                   and e.report_time == 'AMC']

        # Check for upcoming (within 2 days)
        upcoming = [e for e in self.earnings_calendar
                   if 0 < (e.report_date.date() - trade_date.date()).days <= 2]

        # Calculate impact score
        impact_score = 0

        for event in same_day + next_day:
            if event.symbol in ['AAPL', 'MSFT', 'GOOGL', 'AMZN']:
                impact_score += 3  # mega cap = huge impact
            elif event.symbol in ['META', 'TSLA', 'NVDA']:
                impact_score += 2  # large cap = significant impact
            else:
                impact_score += 1

        # Upcoming events add uncertainty
        impact_score += len(upcoming) * 0.5

        return {
            'date': trade_date,
            'same_day_earnings': [e.symbol for e in same_day],
            'post_earnings': [e.symbol for e in next_day],
            'upcoming_earnings': [e.symbol for e in upcoming],
            'impact_score': impact_score,
            'recommendation': self._get_recommendation(impact_score)
        }

    def _get_recommendation(self, impact_score: float) -> dict:
        """Trading recommendations based on earnings impact"""
        if impact_score >= 4:
            return {
                'action': 'REDUCE_EXPOSURE',
                'position_size_mult': 0.3,
                'avoid_symbols': True,
                'reason': 'High earnings impact - major names reporting'
            }
        elif impact_score >= 2:
            return {
                'action': 'CAUTIOUS',
                'position_size_mult': 0.6,
                'avoid_symbols': True,
                'reason': 'Moderate earnings impact'
            }
        elif impact_score >= 1:
            return {
                'action': 'AWARE',
                'position_size_mult': 0.8,
                'avoid_symbols': False,
                'reason': 'Minor earnings impact - stay alert'
            }
        else:
            return {
                'action': 'NORMAL',
                'position_size_mult': 1.0,
                'avoid_symbols': False,
                'reason': 'No significant earnings impact'
            }

    def adjust_strategy_params(
        self,
        base_params: dict,
        impact: dict
    ) -> dict:
        """
        Adjust strategy parameters for earnings environment
        """
        adjusted = base_params.copy()
        mult = impact['recommendation']['position_size_mult']

        # Reduce position size
        adjusted['risk_per_trade'] = base_params['risk_per_trade'] * mult

        # Widen stops (more volatility expected)
        if mult < 1.0:
            adjusted['stop_loss_mult'] = base_params.get('stop_loss_mult', 1.0) * 1.3

        # Increase entry threshold (require stronger signals)
        if mult < 0.7:
            adjusted['signal_threshold'] = base_params.get('signal_threshold', 2.0) * 1.4

        # Reduce max positions
        if mult < 0.5:
            adjusted['max_positions'] = max(1, base_params.get('max_positions', 5) // 2)

        return adjusted


class EarningsPlayFilter:
    """
    Filter for avoiding direct earnings plays
    (I don't trade earnings announcements directly)
    """

    def __init__(self):
        self.blackout_hours_before = 4
        self.blackout_hours_after = 24

    def is_in_blackout(
        self,
        symbol: str,
        current_time: datetime,
        earnings_events: List[EarningsEvent]
    ) -> bool:
        """
        Check if symbol is in earnings blackout window
        """
        for event in earnings_events:
            if event.symbol != symbol:
                continue

            # Calculate blackout windows
            if event.report_time == 'AMC':
                blackout_start = event.report_date.replace(hour=12)  # afternoon before
                blackout_end = event.report_date + timedelta(hours=24)
            else:  # BMO
                blackout_start = event.report_date - timedelta(hours=16)  # day before close
                blackout_end = event.report_date.replace(hour=12)  # morning after

            if blackout_start <= current_time <= blackout_end:
                return True

        return False

    def filter_signals(
        self,
        signals: List[dict],
        current_time: datetime,
        earnings_events: List[EarningsEvent]
    ) -> List[dict]:
        """
        Remove signals for symbols in earnings blackout
        """
        filtered = []

        for signal in signals:
            symbol = signal.get('symbol', '')

            if self.is_in_blackout(symbol, current_time, earnings_events):
                print(f"Filtered {symbol} - earnings blackout")
                continue

            filtered.append(signal)

        return filtered


# Usage this week
adapter = EarningsVolatilityAdapter()
adapter.load_earnings_calendar(
    datetime(2025, 7, 21),
    datetime(2025, 7, 27)
)

# Check impact for each day
for day_offset in range(7):
    check_date = datetime(2025, 7, 21) + timedelta(days=day_offset)
    impact = adapter.get_earnings_impact(check_date)
    print(f"\n{check_date.strftime('%A %m/%d')}:")
    print(f"  Same day: {impact['same_day_earnings']}")
    print(f"  Impact score: {impact['impact_score']}")
    print(f"  Recommendation: {impact['recommendation']['action']}")

this week’s earnings schedule
#

tuesday 7/22: GOOGL (AMC), TSLA (AMC)

wednesday 7/23: low impact day

thursday 7/24: META (AMC)

friday 7/25: low impact (processing META)

my adaptations this week
#

monday-tuesday morning:

  • position size: 60% normal
  • no new QQQ trades (GOOGL/TSLA impact)
  • wider stops (1.3x normal)

tuesday afternoon - wednesday:

  • position size: 30% normal
  • no SPX trades (market reaction to GOOGL/TSLA)
  • signal threshold: 1.4x normal

thursday morning:

  • resume 60% sizing
  • avoid META-heavy setups

thursday afternoon - friday:

  • back to 30% during META reaction
  • focus on non-tech setups

why i don’t trade earnings directly
#

the math doesn’t work for me:

expected move already priced into options.

need >60% directional accuracy to profit.

my edge is 55-60% in normal conditions.

earnings removes that edge.

what i do instead:

trade around earnings.

profit from volatility expansion before.

profit from volatility crush after.

never bet on direction through announcement.

lessons learned
#

been reading about earnings strategies on NexusFi options discussions since joining.

key insight that stuck:

“retail loses on earnings plays because they’re betting against people who know the numbers.”

I’m retail. I don’t know the numbers.

so I trade the volatility environment, not the direction.

tonight
#

earnings week adaptation live. GOOGL/TSLA tuesday, META thursday. position sizes 30-60% normal. wider stops, higher signal thresholds. not trading earnings direction - trading the volatility environment. algo adjustments automated based on earnings calendar impact scores.


2:42am thursday. earnings volatility adaptation. GOOGL/TSLA tuesday crushed normal signals. running 30-60% position sizes. algo automatically adjusts based on earnings calendar. not betting on direction - trading volatility environment around announcements.

-AK

Related

building volatility regime detection
need to stop trading when volatility spikes. building detection system. the problem # this week VIX spiked 18% in 2 days. my strategies got stopped out twice.
order flow analysis - adapting strategies for summer thin volume
summer volume creates different market microstructure. adapting order flow analysis to account for it. the summer volume problem # normal month volume: 4.2M SPX options contracts/day
adaptive position sizing - regime-based approach
position sizing makes or breaks algo trading. been refining adaptive approach last 6 months. finally working consistently. the problem with static sizing # most algo traders:
regime detection - walk-forward validation improving accuracy
regime detection upgraded. walk-forward validation running. accuracy improving. the problem # static regime parameters: optimized on historical data.
regime detection filtering framework - how i adapt to market conditions
august forcing me to rely on filters. figured worth explaining how regime detection works. learned a lot from options selling regime discussions on NexusFi about adapting to conditions. the problem # strategies don’t work in all conditions.
regime detection improvements - faster market adaptation working
week 3 april going strong. adaptive strategy crushing it. been refining regime detection logic. current performance (apr 1-17) # trades: 29