TradingAgents/tradingagents/domains/marketdata/models.py

663 lines
21 KiB
Python

"""
Market data models and type definitions for technical analysis.
"""
from dataclasses import dataclass
from datetime import date
from enum import Enum
from typing import Any, Literal
from proto.message import Optional
from pydantic import BaseModel
# Proper type definitions to eliminate Any types
IndicatorParamValue = int | float | str | bool
InputSpec = Literal["close", "ohlc", "ohlcv", "hl"]
OutputSpec = Literal["single", "double", "triple"]
ParamRanges = dict[str, tuple[int | float, int | float]]
class IndicatorConfig(BaseModel):
"""Configuration for technical indicators with proper typing."""
name: str
parameters: dict[str, IndicatorParamValue] # No more Any
input_types: list[InputSpec] # Specific requirements
output_format: OutputSpec # Precise specification
param_ranges: ParamRanges # Type-safe validation
default_params: dict[str, IndicatorParamValue]
talib_function: str # Direct TA-Lib function name
description: str
class TechnicalAnalysisError(Exception):
"""Clear, actionable error messages for TA-Lib issues."""
pass
class DataQuality(Enum):
"""Data quality levels for market data."""
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class TechnicalIndicatorData(BaseModel):
"""Technical indicator data point with proper typing."""
date: str
value: float | dict[str, float] # No more Any
indicator_type: str
parameters: dict[str, IndicatorParamValue] # Parameter context
confidence: float = 0.0 # Signal confidence
source: str = "talib" # Always TA-Lib
class MarketDataContext(BaseModel):
"""Market data context for trading analysis."""
symbol: str
period: dict[str, str] # {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
price_data: list[dict[str, Any]]
technical_indicators: dict[str, list[TechnicalIndicatorData]]
metadata: dict[str, Any]
class TAReportContext(BaseModel):
"""Technical Analysis Report context with enhanced configuration."""
symbol: str
period: dict[str, str] # {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
indicator: str
indicator_data: list[TechnicalIndicatorData]
analysis_summary: str
signal_strength: float # -1.0 to 1.0
recommendation: str # "BUY", "SELL", "HOLD"
indicator_config: IndicatorConfig # Full config used
parameter_summary: str # Human-readable params
metadata: dict[str, IndicatorParamValue] # Properly typed
class PriceDataContext(BaseModel):
"""Price Data context for historical price information."""
symbol: str
period: dict[str, str] # {"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}
price_data: list[dict[str, Any]]
latest_price: float
price_change: float
price_change_percent: float
volume_info: dict[str, Any]
metadata: dict[str, Any]
# Fundamental Data Models
class FinancialRatio(BaseModel):
"""Financial ratio with calculation metadata."""
name: str
value: float | None
formula: str
category: str # "profitability", "liquidity", "leverage", "efficiency"
interpretation: str
class BalanceSheetData(BaseModel):
"""Balance sheet line items."""
date: str
total_assets: float | None = None
current_assets: float | None = None
cash_and_equivalents: float | None = None
accounts_receivable: float | None = None
inventory: float | None = None
total_liabilities: float | None = None
current_liabilities: float | None = None
accounts_payable: float | None = None
short_term_debt: float | None = None
long_term_debt: float | None = None
total_equity: float | None = None
retained_earnings: float | None = None
class IncomeStatementData(BaseModel):
"""Income statement line items."""
date: str
revenue: float | None = None
gross_profit: float | None = None
operating_income: float | None = None
net_income: float | None = None
ebitda: float | None = None
cost_of_revenue: float | None = None
operating_expenses: float | None = None
interest_expense: float | None = None
tax_expense: float | None = None
shares_outstanding: float | None = None
eps: float | None = None
class CashFlowData(BaseModel):
"""Cash flow statement line items."""
date: str
operating_cash_flow: float | None = None
investing_cash_flow: float | None = None
financing_cash_flow: float | None = None
free_cash_flow: float | None = None
capital_expenditures: float | None = None
dividends_paid: float | None = None
stock_repurchases: float | None = None
class BalanceSheetContext(BaseModel):
"""Balance sheet context for fundamental analysis."""
symbol: str
start_date: date
end_date: date
balance_sheet_data: list[BalanceSheetData]
key_ratios: list[FinancialRatio]
data_quality: DataQuality
source: str
metadata: dict[str, str]
class IncomeStatementContext(BaseModel):
"""Income statement context for fundamental analysis."""
symbol: str
start_date: date
end_date: date
income_statement_data: list[IncomeStatementData]
key_ratios: list[FinancialRatio]
data_quality: DataQuality
source: str
metadata: dict[str, Any]
class CashFlowContext(BaseModel):
"""Cash flow context for fundamental analysis."""
symbol: str
start_date: date
end_date: date
cash_flow_data: list[CashFlowData]
key_ratios: list[FinancialRatio]
data_quality: DataQuality
source: str
metadata: dict[str, Any]
class FundamentalContext(BaseModel):
"""Comprehensive fundamental analysis context."""
symbol: str
start_date: date
end_date: date
balance_sheet: Optional[BalanceSheetContext] = None
income_statement: Optional[IncomeStatementContext] = None
cash_flow: Optional[CashFlowContext] = None
comprehensive_ratios: Optional[list[FinancialRatio]] = None
valuation_metrics: Optional[dict[str, float | None]] = None
financial_health_score: float = 0 # 0-100 composite score
data_quality: DataQuality = DataQuality.LOW
source: Optional[str] = None
metadata: Optional[dict[str, str]] = None
# Reported Financials Models (for Finnhub API responses)
@dataclass
class FinancialLineItem:
"""Individual financial statement line item from reported financials."""
concept: str
unit: str
label: str
value: float | int
@dataclass
class ReportedFinancialsData:
"""Financial statements data containing balance sheet, income statement, and cash flow."""
bs: list[FinancialLineItem] # Balance Sheet
ic: list[FinancialLineItem] # Income Statement
cf: list[FinancialLineItem] # Cash Flow Statement
@dataclass
class ReportedFinancialsResponse:
"""Complete response from Finnhub reported financials API."""
start_date: str
end_date: str
year: int
quarter: int
access_number: str
data: ReportedFinancialsData
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "ReportedFinancialsResponse":
"""Create ReportedFinancialsResponse from API response dictionary."""
financial_data = data.get("data", {})
# Convert line items for each statement type
bs_items = [
FinancialLineItem(
concept=item["concept"],
unit=item["unit"],
label=item["label"],
value=item["value"],
)
for item in financial_data.get("bs", [])
]
ic_items = [
FinancialLineItem(
concept=item["concept"],
unit=item["unit"],
label=item["label"],
value=item["value"],
)
for item in financial_data.get("ic", [])
]
cf_items = [
FinancialLineItem(
concept=item["concept"],
unit=item["unit"],
label=item["label"],
value=item["value"],
)
for item in financial_data.get("cf", [])
]
return cls(
start_date=data["start_date"],
end_date=data["end_date"],
year=data["year"],
quarter=data["quarter"],
access_number=data["access_number"],
data=ReportedFinancialsData(bs=bs_items, ic=ic_items, cf=cf_items),
)
# Insider Transactions Models
@dataclass
class InsiderTransaction:
"""Individual insider transaction record."""
name: str
share: int
change: int
filing_date: str
transaction_date: str
transaction_code: str
transaction_price: float
@dataclass
class InsiderTransactionsResponse:
"""Complete response from Finnhub insider transactions API."""
data: list[InsiderTransaction]
symbol: str
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "InsiderTransactionsResponse":
"""Create InsiderTransactionsResponse from API response dictionary."""
transactions = [
InsiderTransaction(
name=item["name"],
share=item["share"],
change=item["change"],
filing_date=item["filingDate"],
transaction_date=item["transactionDate"],
transaction_code=item["transactionCode"],
transaction_price=item["transactionPrice"],
)
for item in data.get("data", [])
]
return cls(data=transactions, symbol=data["symbol"])
# Insider Sentiment Models
@dataclass
class InsiderSentimentData:
"""Individual insider sentiment data point."""
symbol: str
year: int
month: int
change: int
mspr: float # Monthly Share Purchase Ratio
@dataclass
class InsiderSentimentResponse:
"""Complete response from Finnhub insider sentiment API."""
data: list[InsiderSentimentData]
symbol: str
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "InsiderSentimentResponse":
"""Create InsiderSentimentResponse from API response dictionary."""
sentiment_data = [
InsiderSentimentData(
symbol=item["symbol"],
year=item["year"],
month=item["month"],
change=item["change"],
mspr=item["mspr"],
)
for item in data.get("data", [])
]
return cls(data=sentiment_data, symbol=data["symbol"])
# Company Profile Models
@dataclass
class CompanyProfile:
"""Company profile information from Finnhub."""
country: str
currency: str
exchange: str
ipo: str
market_capitalization: float
name: str
phone: str
share_outstanding: float
ticker: str
weburl: str
logo: str
finnhub_industry: str
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "CompanyProfile":
"""Create CompanyProfile from API response dictionary."""
return cls(
country=data.get("country", ""),
currency=data.get("currency", ""),
exchange=data.get("exchange", ""),
ipo=data.get("ipo", ""),
market_capitalization=data.get("marketCapitalization", 0.0),
name=data.get("name", ""),
phone=data.get("phone", ""),
share_outstanding=data.get("shareOutstanding", 0.0),
ticker=data.get("ticker", ""),
weburl=data.get("weburl", ""),
logo=data.get("logo", ""),
finnhub_industry=data.get("finnhubIndustry", ""),
)
# Complete indicator definitions for 20 professional indicators
INDICATOR_DEFINITIONS = {
# Momentum Indicators (7)
"RSI": {
"talib_function": "talib.RSI",
"input_types": ["close"],
"default_params": {"timeperiod": 14},
"param_ranges": {"timeperiod": (2, 100)},
"output_format": "single",
"description": "Relative Strength Index",
},
"MACD": {
"talib_function": "talib.MACD",
"input_types": ["close"],
"default_params": {"fastperiod": 12, "slowperiod": 26, "signalperiod": 9},
"param_ranges": {
"fastperiod": (2, 50),
"slowperiod": (10, 200),
"signalperiod": (2, 50),
},
"output_format": "triple",
"description": "Moving Average Convergence Divergence",
},
"STOCH": {
"talib_function": "talib.STOCH",
"input_types": ["ohlc"],
"default_params": {"fastk_period": 14, "slowk_period": 3, "slowd_period": 3},
"param_ranges": {
"fastk_period": (1, 100),
"slowk_period": (1, 50),
"slowd_period": (1, 50),
},
"output_format": "double",
"description": "Stochastic Oscillator",
},
"WILLR": {
"talib_function": "talib.WILLR",
"input_types": ["ohlc"],
"default_params": {"timeperiod": 14},
"param_ranges": {"timeperiod": (2, 100)},
"output_format": "single",
"description": "Williams %R",
},
"CCI": {
"talib_function": "talib.CCI",
"input_types": ["ohlc"],
"default_params": {"timeperiod": 20},
"param_ranges": {"timeperiod": (2, 100)},
"output_format": "single",
"description": "Commodity Channel Index",
},
"ROC": {
"talib_function": "talib.ROC",
"input_types": ["close"],
"default_params": {"timeperiod": 12},
"param_ranges": {"timeperiod": (1, 100)},
"output_format": "single",
"description": "Rate of Change",
},
"MFI": {
"talib_function": "talib.MFI",
"input_types": ["ohlcv"],
"default_params": {"timeperiod": 14},
"param_ranges": {"timeperiod": (2, 100)},
"output_format": "single",
"description": "Money Flow Index",
},
# Trend Indicators (7)
"SMA": {
"talib_function": "talib.SMA",
"input_types": ["close"],
"default_params": {"timeperiod": 20},
"param_ranges": {"timeperiod": (2, 200)},
"output_format": "single",
"description": "Simple Moving Average",
},
"EMA": {
"talib_function": "talib.EMA",
"input_types": ["close"],
"default_params": {"timeperiod": 20},
"param_ranges": {"timeperiod": (2, 200)},
"output_format": "single",
"description": "Exponential Moving Average",
},
"BBANDS": {
"talib_function": "talib.BBANDS",
"input_types": ["close"],
"default_params": {"timeperiod": 20, "nbdevup": 2.0, "nbdevdn": 2.0},
"param_ranges": {
"timeperiod": (2, 100),
"nbdevup": (0.1, 5.0),
"nbdevdn": (0.1, 5.0),
},
"output_format": "triple",
"description": "Bollinger Bands",
},
"SAR": {
"talib_function": "talib.SAR",
"input_types": ["ohlc"],
"default_params": {"acceleration": 0.02, "maximum": 0.2},
"param_ranges": {"acceleration": (0.01, 0.1), "maximum": (0.1, 1.0)},
"output_format": "single",
"description": "Parabolic SAR",
},
"ADX": {
"talib_function": "talib.ADX",
"input_types": ["ohlc"],
"default_params": {"timeperiod": 14},
"param_ranges": {"timeperiod": (2, 100)},
"output_format": "single",
"description": "Average Directional Index",
},
"AROON": {
"talib_function": "talib.AROON",
"input_types": ["hl"],
"default_params": {"timeperiod": 25},
"param_ranges": {"timeperiod": (2, 100)},
"output_format": "double",
"description": "Aroon Oscillator",
},
"TEMA": {
"talib_function": "talib.TEMA",
"input_types": ["close"],
"default_params": {"timeperiod": 20},
"param_ranges": {"timeperiod": (2, 200)},
"output_format": "single",
"description": "Triple Exponential Moving Average",
},
# Volume Indicators (3)
"OBV": {
"talib_function": "talib.OBV",
"input_types": ["ohlcv"],
"default_params": {},
"param_ranges": {},
"output_format": "single",
"description": "On Balance Volume",
},
"AD": {
"talib_function": "talib.AD",
"input_types": ["ohlcv"],
"default_params": {},
"param_ranges": {},
"output_format": "single",
"description": "Accumulation/Distribution",
},
"ADOSC": {
"talib_function": "talib.ADOSC",
"input_types": ["ohlcv"],
"default_params": {"fastperiod": 3, "slowperiod": 10},
"param_ranges": {"fastperiod": (2, 50), "slowperiod": (5, 100)},
"output_format": "single",
"description": "Accumulation/Distribution Oscillator",
},
# Volatility Indicators (3)
"ATR": {
"talib_function": "talib.ATR",
"input_types": ["ohlc"],
"default_params": {"timeperiod": 14},
"param_ranges": {"timeperiod": (1, 100)},
"output_format": "single",
"description": "Average True Range",
},
"NATR": {
"talib_function": "talib.NATR",
"input_types": ["ohlc"],
"default_params": {"timeperiod": 14},
"param_ranges": {"timeperiod": (1, 100)},
"output_format": "single",
"description": "Normalized Average True Range",
},
"TRANGE": {
"talib_function": "talib.TRANGE",
"input_types": ["ohlc"],
"default_params": {},
"param_ranges": {},
"output_format": "single",
"description": "True Range",
},
}
class IndicatorPresets:
"""Professional indicator presets for different trading styles."""
@staticmethod
def get_scalping_presets() -> dict[str, dict[str, IndicatorParamValue]]:
"""Fast scalping presets (1-5 minute timeframes)."""
return {
"RSI_SCALPING": {"timeperiod": 5},
"MACD_SCALPING": {"fastperiod": 5, "slowperiod": 13, "signalperiod": 5},
"STOCH_SCALPING": {"fastk_period": 5, "slowk_period": 3, "slowd_period": 3},
"EMA_SCALPING": {"timeperiod": 9},
"BBANDS_TIGHT": {"timeperiod": 10, "nbdevup": 1.5, "nbdevdn": 1.5},
"ATR_SCALPING": {"timeperiod": 5},
}
@staticmethod
def get_day_trading_presets() -> dict[str, dict[str, IndicatorParamValue]]:
"""Day trading presets (5-60 minute timeframes)."""
return {
"RSI_DAY_TRADING": {"timeperiod": 14},
"MACD_DAY_TRADING": {"fastperiod": 12, "slowperiod": 26, "signalperiod": 9},
"STOCH_DAY_TRADING": {
"fastk_period": 14,
"slowk_period": 3,
"slowd_period": 3,
},
"EMA_DAY_TRADING": {"timeperiod": 20},
"BBANDS_STANDARD": {"timeperiod": 20, "nbdevup": 2.0, "nbdevdn": 2.0},
"ADX_DAY_TRADING": {"timeperiod": 14},
"ATR_DAY_TRADING": {"timeperiod": 14},
}
@staticmethod
def get_swing_trading_presets() -> dict[str, dict[str, IndicatorParamValue]]:
"""Swing trading presets (daily timeframes)."""
return {
"RSI_SWING": {"timeperiod": 21},
"MACD_SWING": {"fastperiod": 12, "slowperiod": 26, "signalperiod": 9},
"STOCH_SWING": {"fastk_period": 21, "slowk_period": 5, "slowd_period": 5},
"SMA_SWING_SHORT": {"timeperiod": 50},
"SMA_SWING_LONG": {"timeperiod": 200},
"EMA_SWING": {"timeperiod": 50},
"BBANDS_SWING": {"timeperiod": 20, "nbdevup": 2.5, "nbdevdn": 2.5},
"ADX_SWING": {"timeperiod": 21},
"AROON_SWING": {"timeperiod": 25},
}
@staticmethod
def get_position_trading_presets() -> dict[str, dict[str, IndicatorParamValue]]:
"""Position trading presets (weekly/monthly timeframes)."""
return {
"RSI_POSITION": {"timeperiod": 30},
"MACD_POSITION": {"fastperiod": 20, "slowperiod": 50, "signalperiod": 15},
"SMA_POSITION_SHORT": {"timeperiod": 100},
"SMA_POSITION_LONG": {"timeperiod": 300},
"EMA_POSITION": {"timeperiod": 100},
"BBANDS_POSITION": {"timeperiod": 50, "nbdevup": 3.0, "nbdevdn": 3.0},
"ADX_POSITION": {"timeperiod": 30},
"AROON_POSITION": {"timeperiod": 50},
}
@staticmethod
def get_all_presets() -> dict[str, dict[str, IndicatorParamValue]]:
"""Get all available presets combined."""
all_presets = {}
all_presets.update(IndicatorPresets.get_scalping_presets())
all_presets.update(IndicatorPresets.get_day_trading_presets())
all_presets.update(IndicatorPresets.get_swing_trading_presets())
all_presets.update(IndicatorPresets.get_position_trading_presets())
return all_presets
@staticmethod
def get_preset_for_style(style: str) -> dict[str, dict[str, IndicatorParamValue]]:
"""Get presets for a specific trading style."""
style_map = {
"scalping": IndicatorPresets.get_scalping_presets(),
"day_trading": IndicatorPresets.get_day_trading_presets(),
"swing": IndicatorPresets.get_swing_trading_presets(),
"position": IndicatorPresets.get_position_trading_presets(),
}
return style_map.get(style.lower(), {})