Snapshot before the DuckDB->PostgreSQL migration. data/, archive/, the 232MB rebuild zip, the Windows .venv, and .env are excluded via .gitignore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
260 lines
7.1 KiB
Python
260 lines
7.1 KiB
Python
"""Module contracts and interfaces for the momentum trading system.
|
|
|
|
Every module must implement the ModuleContract protocol, defining:
|
|
- input_schema: What the module expects
|
|
- output_schema: What the module produces
|
|
- run(): Execute the module
|
|
- validate_input(): Ensure data meets contracts
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, date
|
|
from typing import Any, Protocol, TypeVar
|
|
|
|
T_Input = TypeVar("T_Input")
|
|
T_Output = TypeVar("T_Output")
|
|
|
|
|
|
@dataclass
|
|
class ModuleMetrics:
|
|
"""Standard metrics every module should track."""
|
|
|
|
module_name: str
|
|
timestamp: datetime
|
|
duration_ms: float
|
|
input_count: int = 0
|
|
output_count: int = 0
|
|
error_count: int = 0
|
|
warnings: list[str] = field(default_factory=list)
|
|
custom_metrics: dict[str, Any] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"module_name": self.module_name,
|
|
"timestamp": self.timestamp.isoformat(),
|
|
"duration_ms": self.duration_ms,
|
|
"input_count": self.input_count,
|
|
"output_count": self.output_count,
|
|
"error_count": self.error_count,
|
|
"warnings": self.warnings,
|
|
"custom_metrics": self.custom_metrics,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class ValidationFailure:
|
|
"""Single validation failure with context."""
|
|
|
|
field: str
|
|
value: Any
|
|
reason: str
|
|
severity: str = "error" # "error" or "warning"
|
|
|
|
|
|
@dataclass
|
|
class ModuleResult:
|
|
"""Base class for all module results."""
|
|
|
|
success: bool
|
|
timestamp: datetime
|
|
metrics: ModuleMetrics
|
|
validation_failures: list[ValidationFailure] = field(default_factory=list)
|
|
|
|
def has_errors(self) -> bool:
|
|
return bool(self.validation_failures or not self.success)
|
|
|
|
def error_summary(self) -> str:
|
|
"""Human-readable error summary."""
|
|
if not self.validation_failures:
|
|
return "No errors"
|
|
errors = [f for f in self.validation_failures if f.severity == "error"]
|
|
if not errors:
|
|
return f"{len(self.validation_failures)} warnings only"
|
|
return f"{len(errors)} error(s), {len([f for f in self.validation_failures if f.severity == 'warning'])} warning(s)"
|
|
|
|
|
|
class ModuleContract(Protocol):
|
|
"""Contract every module must fulfill."""
|
|
|
|
def run(self, input: Any) -> ModuleResult:
|
|
"""Execute the module.
|
|
|
|
Args:
|
|
input: Module-specific input conforming to input_schema
|
|
|
|
Returns:
|
|
Module-specific result inheriting from ModuleResult
|
|
"""
|
|
...
|
|
|
|
def validate_input(self, input: Any) -> tuple[bool, list[ValidationFailure]]:
|
|
"""Validate input before processing.
|
|
|
|
Args:
|
|
input: Data to validate
|
|
|
|
Returns:
|
|
(is_valid, failures)
|
|
"""
|
|
...
|
|
|
|
def get_metrics(self) -> ModuleMetrics:
|
|
"""Get last execution metrics."""
|
|
...
|
|
|
|
|
|
# ============================================================================
|
|
# INGESTION MODULE CONTRACTS
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class BarIngestRequest:
|
|
"""Input contract for ingestion module."""
|
|
|
|
symbols: list[str]
|
|
session_date: date
|
|
lookback_minutes: int = 120 # how many minutes of history to fetch
|
|
source: str = "alpaca" # "alpaca", "test", "file"
|
|
|
|
|
|
@dataclass
|
|
class BarIngestResult(ModuleResult):
|
|
"""Output contract for ingestion module."""
|
|
|
|
minute_rows_inserted: int = 0
|
|
symbols_updated: list[str] = field(default_factory=list)
|
|
source_health: dict[str, str] = field(default_factory=dict) # {source: "healthy"|"error"}
|
|
|
|
|
|
# ============================================================================
|
|
# RESEARCH MODULE CONTRACTS
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class ResearchRequest:
|
|
"""Input contract for research module."""
|
|
|
|
symbols: list[str]
|
|
session_date: date
|
|
lookback_days: int = 30 # for daily history
|
|
|
|
|
|
@dataclass
|
|
class ResearchResult(ModuleResult):
|
|
"""Output contract for research module."""
|
|
|
|
symbols_researched: list[str] = field(default_factory=list)
|
|
symbols_with_data: list[str] = field(default_factory=list)
|
|
symbols_missing_data: list[str] = field(default_factory=list)
|
|
total_bars_available: int = 0
|
|
|
|
|
|
# ============================================================================
|
|
# EVALUATION MODULE CONTRACTS
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class CriterionDetail:
|
|
"""Result of evaluating one criterion."""
|
|
|
|
name: str
|
|
passed: bool
|
|
value: float # actual metric value
|
|
threshold: float | None = None # minimum to pass
|
|
reason: str = ""
|
|
confidence: float = 1.0 # 0-1, how confident in this result
|
|
|
|
|
|
@dataclass
|
|
class EvaluationRequest:
|
|
"""Input contract for evaluation module."""
|
|
|
|
symbol: str
|
|
bars_df: Any # pd.DataFrame with {timestamp, open, high, low, close, volume}
|
|
previous_close: float
|
|
avg_daily_volume: float
|
|
session_date: date
|
|
|
|
|
|
@dataclass
|
|
class EvaluationResult(ModuleResult):
|
|
"""Output contract for evaluation module."""
|
|
|
|
symbol: str = ""
|
|
status: str = "late" # "ready", "blocked", "late", "error"
|
|
score: float = 0.0 # 0-100
|
|
criteria: list[CriterionDetail] = field(default_factory=list)
|
|
entry_price: float | None = None
|
|
stop_loss: float | None = None
|
|
target_price: float | None = None
|
|
quality_score: float = 0.0 # 0-1
|
|
blocking_reason: str | None = None
|
|
metrics_dict: dict[str, float] = field(default_factory=dict) # gap%, rvol, etc.
|
|
|
|
|
|
# ============================================================================
|
|
# EXECUTION MODULE CONTRACTS
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class ExecutionRequest:
|
|
"""Input contract for execution module."""
|
|
|
|
symbol: str
|
|
entry_price: float
|
|
stop_price: float
|
|
target_price: float
|
|
score: float
|
|
criteria_count: int
|
|
account_equity: float
|
|
current_positions: int
|
|
today_realized_pnl: float
|
|
|
|
|
|
@dataclass
|
|
class ExecutionResult(ModuleResult):
|
|
"""Output contract for execution module."""
|
|
|
|
symbol: str = ""
|
|
status: str = "" # "approved", "rejected", "pending_approval", "error"
|
|
reason: str = "" # why it was approved/rejected
|
|
order_id: str | None = None
|
|
shares: int | None = None
|
|
risk_dollars: float | None = None
|
|
risk_checks_applied: list[str] = field(default_factory=list)
|
|
|
|
|
|
# ============================================================================
|
|
# BROKER MODULE CONTRACTS
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class BrokerRequest:
|
|
"""Input contract for broker module."""
|
|
|
|
symbol: str
|
|
order_id: str
|
|
entry_price: float
|
|
stop_price: float
|
|
target_price: float
|
|
shares: int
|
|
order_type: str = "limit" # "limit" or "market"
|
|
|
|
|
|
@dataclass
|
|
class BrokerResult(ModuleResult):
|
|
"""Output contract for broker module."""
|
|
|
|
symbol: str = ""
|
|
status: str = "" # "submitted", "rejected", "error"
|
|
broker_order_id: str | None = None
|
|
submitted_at: datetime | None = None
|