Add objective external data sources to enhance trading decisions beyond market prices and user input. ## New Modules ### src/data/news_api.py - News sentiment analysis with Alpha Vantage and NewsAPI support - Sentiment scoring (-1.0 to +1.0) per article and aggregated - 5-minute caching to minimize API quota usage - Graceful degradation when APIs unavailable ### src/data/economic_calendar.py - Track major economic events (FOMC, GDP, CPI) - Earnings calendar per stock - Event proximity checking for high-volatility periods - Hardcoded major events for 2026 (no API required) ### src/data/market_data.py - Market sentiment indicators (Fear & Greed equivalent) - Market breadth (advance/decline ratios) - Sector performance tracking - Fear/Greed score calculation ## Integration Enhanced GeminiClient to seamlessly integrate external data: - Optional news_api, economic_calendar, and market_data parameters - Async build_prompt() includes external context when available - Backward-compatible build_prompt_sync() for existing code - Graceful fallback when external data unavailable External data automatically added to AI prompts: - News sentiment with top articles - Upcoming high-impact economic events - Market sentiment and breadth indicators ## Configuration Added optional settings to config.py: - NEWS_API_KEY: API key for news provider - NEWS_API_PROVIDER: "alphavantage" or "newsapi" - MARKET_DATA_API_KEY: API key for market data ## Testing Comprehensive test suite with 38 tests: - NewsAPI caching, sentiment parsing, API integration - EconomicCalendar event filtering, earnings lookup - MarketData sentiment and breadth calculations - GeminiClient integration with external data sources - All tests use mocks (no real API keys required) - 81% coverage for src/data module (exceeds 80% requirement) ## Circular Import Fix Fixed circular dependency between gemini_client.py and cache.py: - Use TYPE_CHECKING for imports in cache.py - String annotations for TradeDecision type hints All 195 existing tests pass. No breaking changes to existing functionality. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
220 lines
6.5 KiB
Python
220 lines
6.5 KiB
Python
"""Economic calendar integration for major market events.
|
|
|
|
Tracks FOMC meetings, GDP releases, CPI, earnings calendars, and other
|
|
market-moving events.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class EconomicEvent:
|
|
"""Single economic event."""
|
|
|
|
name: str
|
|
event_type: str # "FOMC", "GDP", "CPI", "EARNINGS", etc.
|
|
datetime: datetime
|
|
impact: str # "HIGH", "MEDIUM", "LOW"
|
|
country: str
|
|
description: str
|
|
|
|
|
|
@dataclass
|
|
class UpcomingEvents:
|
|
"""Collection of upcoming economic events."""
|
|
|
|
events: list[EconomicEvent]
|
|
high_impact_count: int
|
|
next_major_event: EconomicEvent | None
|
|
|
|
|
|
class EconomicCalendar:
|
|
"""Economic calendar with event tracking and impact scoring."""
|
|
|
|
def __init__(self, api_key: str | None = None) -> None:
|
|
"""Initialize economic calendar.
|
|
|
|
Args:
|
|
api_key: API key for calendar provider (None for testing/hardcoded)
|
|
"""
|
|
self._api_key = api_key
|
|
# For now, use hardcoded major events (can be extended with API)
|
|
self._events: list[EconomicEvent] = []
|
|
|
|
# ------------------------------------------------------------------
|
|
# Public API
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_upcoming_events(
|
|
self, days_ahead: int = 7, min_impact: str = "MEDIUM"
|
|
) -> UpcomingEvents:
|
|
"""Get upcoming economic events within specified timeframe.
|
|
|
|
Args:
|
|
days_ahead: Number of days to look ahead
|
|
min_impact: Minimum impact level ("LOW", "MEDIUM", "HIGH")
|
|
|
|
Returns:
|
|
UpcomingEvents with filtered events
|
|
"""
|
|
now = datetime.now()
|
|
end_date = now + timedelta(days=days_ahead)
|
|
|
|
# Filter events by timeframe and impact
|
|
upcoming = [
|
|
event
|
|
for event in self._events
|
|
if now <= event.datetime <= end_date
|
|
and self._impact_level(event.impact) >= self._impact_level(min_impact)
|
|
]
|
|
|
|
# Sort by datetime
|
|
upcoming.sort(key=lambda e: e.datetime)
|
|
|
|
# Count high-impact events
|
|
high_impact_count = sum(1 for e in upcoming if e.impact == "HIGH")
|
|
|
|
# Get next major event
|
|
next_major = None
|
|
for event in upcoming:
|
|
if event.impact == "HIGH":
|
|
next_major = event
|
|
break
|
|
|
|
return UpcomingEvents(
|
|
events=upcoming,
|
|
high_impact_count=high_impact_count,
|
|
next_major_event=next_major,
|
|
)
|
|
|
|
def add_event(self, event: EconomicEvent) -> None:
|
|
"""Add an economic event to the calendar."""
|
|
self._events.append(event)
|
|
|
|
def clear_events(self) -> None:
|
|
"""Clear all events (useful for testing)."""
|
|
self._events.clear()
|
|
|
|
def get_earnings_date(self, stock_code: str) -> datetime | None:
|
|
"""Get next earnings date for a stock.
|
|
|
|
Args:
|
|
stock_code: Stock ticker symbol
|
|
|
|
Returns:
|
|
Next earnings datetime or None if not found
|
|
"""
|
|
now = datetime.now()
|
|
earnings_events = [
|
|
event
|
|
for event in self._events
|
|
if event.event_type == "EARNINGS"
|
|
and stock_code.upper() in event.name.upper()
|
|
and event.datetime > now
|
|
]
|
|
|
|
if not earnings_events:
|
|
return None
|
|
|
|
# Return earliest upcoming earnings
|
|
earnings_events.sort(key=lambda e: e.datetime)
|
|
return earnings_events[0].datetime
|
|
|
|
def load_hardcoded_events(self) -> None:
|
|
"""Load hardcoded major economic events for 2026.
|
|
|
|
This is a fallback when no API is available.
|
|
"""
|
|
# Major FOMC meetings in 2026 (estimated)
|
|
fomc_dates = [
|
|
datetime(2026, 3, 18),
|
|
datetime(2026, 5, 6),
|
|
datetime(2026, 6, 17),
|
|
datetime(2026, 7, 29),
|
|
datetime(2026, 9, 16),
|
|
datetime(2026, 11, 4),
|
|
datetime(2026, 12, 16),
|
|
]
|
|
|
|
for date in fomc_dates:
|
|
self.add_event(
|
|
EconomicEvent(
|
|
name="FOMC Meeting",
|
|
event_type="FOMC",
|
|
datetime=date,
|
|
impact="HIGH",
|
|
country="US",
|
|
description="Federal Reserve interest rate decision",
|
|
)
|
|
)
|
|
|
|
# Quarterly GDP releases (estimated)
|
|
gdp_dates = [
|
|
datetime(2026, 4, 28),
|
|
datetime(2026, 7, 30),
|
|
datetime(2026, 10, 29),
|
|
]
|
|
|
|
for date in gdp_dates:
|
|
self.add_event(
|
|
EconomicEvent(
|
|
name="US GDP Release",
|
|
event_type="GDP",
|
|
datetime=date,
|
|
impact="HIGH",
|
|
country="US",
|
|
description="Quarterly GDP growth rate",
|
|
)
|
|
)
|
|
|
|
# Monthly CPI releases (12th of each month, estimated)
|
|
for month in range(1, 13):
|
|
try:
|
|
cpi_date = datetime(2026, month, 12)
|
|
self.add_event(
|
|
EconomicEvent(
|
|
name="US CPI Release",
|
|
event_type="CPI",
|
|
datetime=cpi_date,
|
|
impact="HIGH",
|
|
country="US",
|
|
description="Consumer Price Index inflation data",
|
|
)
|
|
)
|
|
except ValueError:
|
|
continue
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _impact_level(self, impact: str) -> int:
|
|
"""Convert impact string to numeric level."""
|
|
levels = {"LOW": 1, "MEDIUM": 2, "HIGH": 3}
|
|
return levels.get(impact.upper(), 0)
|
|
|
|
def is_high_volatility_period(self, hours_ahead: int = 24) -> bool:
|
|
"""Check if we're near a high-impact event.
|
|
|
|
Args:
|
|
hours_ahead: Number of hours to look ahead
|
|
|
|
Returns:
|
|
True if high-impact event is imminent
|
|
"""
|
|
now = datetime.now()
|
|
threshold = now + timedelta(hours=hours_ahead)
|
|
|
|
for event in self._events:
|
|
if event.impact == "HIGH" and now <= event.datetime <= threshold:
|
|
return True
|
|
|
|
return False
|