from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from decimal import Decimal from enum import Enum from typing import Generic, Optional, TypeVar from typing_extensions import TypedDict T = TypeVar("T") class ErrorType(str, Enum): """Categorisation for envelope failures.""" AUTHENTICATION = "AUTHENTICATION" NETWORK = "NETWORK" PARSING = "PARSING" VALIDATION = "VALIDATION" UNKNOWN = "UNKNOWN" class Envelope(TypedDict, Generic[T]): """Standard response envelope for unified API operations.""" success: bool data: Optional[T] error: Optional[str] error_type: Optional[ErrorType] retryable: bool def ok(data: T) -> Envelope[T]: """Create a success envelope containing the provided data.""" return { "success": True, "data": data, "error": None, "error_type": None, "retryable": False, } def fail( error: str, error_type: ErrorType | str = ErrorType.UNKNOWN, retryable: bool = False, ) -> Envelope[None]: """Create a failure envelope with error metadata.""" resolved_error_type: ErrorType if isinstance(error_type, ErrorType): resolved_error_type = error_type else: try: resolved_error_type = ErrorType(error_type) except ValueError: resolved_error_type = ErrorType.UNKNOWN return { "success": False, "data": None, "error": error, "error_type": resolved_error_type, "retryable": retryable, } @dataclass(slots=True) class SessionStatus: """Represents the current authentication session state.""" logged_in: bool session_age_minutes: Optional[int] = None last_refresh: Optional[datetime] = None needs_mfa: bool = False cookies_valid: bool = True @dataclass(slots=True) class AccountSummary: """Summary details for a Schwab account.""" id: str label: str type: str last4: Optional[str] = None is_margin: bool = False @dataclass(slots=True) class AccountOverview: """Aggregated balance snapshot for an account.""" account: AccountSummary total_value: Optional[Decimal] = None day_change: Optional[Decimal] = None day_change_pct: Optional[float] = None cash: Optional[Decimal] = None settled_cash: Optional[Decimal] = None buying_power: Optional[Decimal] = None margin_balance: Optional[Decimal] = None @dataclass(slots=True) class Lot: """Individual lot information within a position.""" acquired_date: Optional[str] = None quantity: Optional[float] = None cost_basis: Optional[Decimal] = None lot_id: Optional[str] = None @dataclass(slots=True) class Position: """Holding data for a specific security.""" symbol: str description: Optional[str] = None asset_type: Optional[str] = None quantity: Optional[float] = None market_price: Optional[Decimal] = None market_value: Optional[Decimal] = None cost_basis_total: Optional[Decimal] = None unrealized_gain: Optional[Decimal] = None unrealized_gain_pct: Optional[float] = None lots: list[Lot] = field(default_factory=list) @dataclass(slots=True) class PortfolioSnapshot: """Aggregated view of equity holdings across accounts.""" equities: list[Position] total_value: Optional[Decimal] = None count: int = 0 @dataclass(slots=True) class MorningstarData: """Unified Morningstar data payload (existing equity fields).""" ticker: str company_name: Optional[str] = None previous_dividend_payment: Optional[str] = None previous_pay_date: Optional[str] = None previous_ex_date: Optional[str] = None frequency: Optional[str] = None annual_dividend_rate: Optional[str] = None annual_dividend_yield: Optional[str] = None fair_value: Optional[str] = None economic_moat: Optional[str] = None capital_allocation: Optional[str] = None rating: Optional[int] = None one_star_price: Optional[str] = None five_star_price: Optional[str] = None assessment: Optional[str] = None range_52_week: Optional[str] = None dividend_yield: Optional[str] = None investment_style: Optional[str] = None report_url: Optional[str] = None report_date: Optional[str] = None source: Optional[str] = None @dataclass(slots=True) class Transaction: """Normalized transaction record matching transactions feature.""" date: str action: str symbol: Optional[str] description: str quantity: Optional[str] price: Optional[str] fees_comm: Optional[str] amount: Optional[str] # Phase 1 Data Structures @dataclass(slots=True) class QuoteData: """Quote and price data from symbol bar.""" price: Optional[float] = None change: Optional[float] = None change_percent: Optional[float] = None after_hours_price: Optional[float] = None after_hours_change: Optional[float] = None after_hours_change_percent: Optional[float] = None bid: Optional[float] = None ask: Optional[float] = None bid_ask_size: Optional[str] = None previous_close: Optional[float] = None open: Optional[float] = None volume: Optional[int] = None volume_vs_avg: Optional[str] = None day_range_low: Optional[float] = None day_range_high: Optional[float] = None week_52_low: Optional[float] = None week_52_high: Optional[float] = None market_cap: Optional[str] = None sector: Optional[str] = None exchange: Optional[str] = None @dataclass(slots=True) class EnhancedDividends: """Enhanced dividend data including forward-looking information.""" # Forward-looking data (Phase 1) next_payment: Optional[float] = None next_pay_date: Optional[str] = None next_ex_date: Optional[str] = None # Existing data frequency: Optional[str] = None annual_rate: Optional[float] = None annual_yield: Optional[float] = None previous_payment: Optional[float] = None previous_pay_date: Optional[str] = None previous_ex_date: Optional[str] = None @dataclass(slots=True) class EarningsData: """Core earnings metrics and forecasts.""" # Upcoming earnings next_announcement_date: Optional[str] = None announcement_timing: Optional[str] = None analysts_covering: Optional[int] = None consensus_estimate: Optional[float] = None estimate_high: Optional[float] = None estimate_low: Optional[float] = None # Historical earnings eps_ttm: Optional[float] = None revenue_ttm: Optional[float] = None # Stored in dollars pe_ttm: Optional[float] = None forward_pe: Optional[float] = None peg_ratio: Optional[float] = None # Beat/miss history (simplified for Phase 1) recent_beats: list[dict] = field(default_factory=list) future_estimates: list[dict] = field(default_factory=list) @dataclass(slots=True) class CalculatedMetrics: """Calculated metrics derived from other data.""" payout_ratio: Optional[float] = None @dataclass(slots=True) class EquityPhase1Data: """Complete Phase 1 enhanced equity data.""" ticker: str quote: Optional[QuoteData] = None dividends: Optional[EnhancedDividends] = None earnings: Optional[EarningsData] = None calculated_metrics: Optional[CalculatedMetrics] = None