All checks were successful
Build and Push Docker Image / build (push) Successful in 34s
272 lines
7.2 KiB
Python
272 lines
7.2 KiB
Python
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
|
|
|
|
|