Trading-Bot-M20 / contracts.py
raghava4u's picture
Upload folder using huggingface_hub
d53dc44 verified
Raw History Blame Contribute Delete
5.15 kB
"""
contracts.py β€” Cross-module typed interfaces.
Every inter-module call MUST use these types. No raw dicts between modules.
"""
from __future__ import annotations
import datetime
from typing import Literal
import pandas as pd
from pydantic import BaseModel, Field, field_validator
# ── Exceptions ───────────────────────────────────────────────────────────────
class InsufficientDataError(Exception):
"""Raised when fewer than n_bars are available for a symbol/timeframe."""
class ConfigError(Exception):
"""Raised when configuration validation fails."""
class BrokerError(Exception):
"""Raised on unrecoverable broker API errors."""
class SafeModeError(Exception):
"""Raised when an operation is attempted while SAFE MODE is active."""
# ── Signal Contracts ─────────────────────────────────────────────────────────
class TechnicalScore(BaseModel):
"""Output of signals/technical.py for a single symbol at a point in time."""
model_config = {"arbitrary_types_allowed": True}
symbol: str
timestamp: datetime.datetime
n_signals_computed: int = 0
score: float = Field(ge=-1.0, le=1.0)
sub_signals: dict[str, float] = Field(default_factory=dict)
skip_reason: str | None = None
@field_validator("score", mode="before")
@classmethod
def force_zero_if_insufficient(cls, v: float, info) -> float:
n = info.data.get("n_signals_computed", 0)
if n < 3:
return 0.0
return v
class SentimentScore(BaseModel):
"""Output of signals/sentiment.py for a single symbol."""
symbol: str
score: float = Field(ge=-1.0, le=1.0)
cached_at: datetime.datetime
source_count: int = 0
stale: bool = False
@field_validator("stale", mode="before")
@classmethod
def compute_stale(cls, v: bool, info) -> bool:
cached = info.data.get("cached_at")
if cached is None:
return True
now = datetime.datetime.now(datetime.timezone.utc)
if cached.tzinfo is None:
cached = cached.replace(tzinfo=datetime.timezone.utc)
delta = (now - cached).total_seconds()
return delta > 35 * 60 # stale if > 35 minutes
class FinalScore(BaseModel):
"""Combined signal output from signals/combiner.py."""
symbol: str
timestamp: datetime.datetime
technical: float
sentiment: float
final: float
decision: Literal["BUY", "SELL", "HOLD"]
skip_reason: str | None = None
class PriceTarget(BaseModel):
"""Predicted price targets for dashboard display."""
symbol: str
current_price: float
predicted_direction: Literal["UP", "DOWN", "NEUTRAL"]
entry_price: float | None = None
stop_loss_price: float | None = None
take_profit_price: float | None = None
expected_profit_usd: float = 0.0
confidence: float = Field(ge=0.0, le=1.0, default=0.0)
# ── Order Contracts ──────────────────────────────────────────────────────────
class OrderRequest(BaseModel):
"""Internal contract for placing orders."""
symbol: str
side: Literal["buy", "sell"]
qty: float
limit_price: float
stop_price: float | None = None
tp_price: float | None = None
trail_percent: float | None = None
is_fractional: bool = False
dry_run: bool = False
class LinkedOrderGroup(BaseModel):
"""Tracks entry + stop + TP legs for fractional orders."""
symbol: str
entry_order_id: str
stop_order_id: str | None = None
tp_order_id: str | None = None
entry_filled: bool = False
stop_submitted: bool = False
tp_submitted: bool = False
is_fractional: bool = False
entry_price: float | None = None
orphaned: bool = False
# ── Data Contract ────────────────────────────────────────────────────────────
# get_bars(symbol, timeframe, n_bars, adjusted=True) β†’ pd.DataFrame
# Columns: timestamp (UTC index), open, high, low, close, volume, vwap, adjusted
# Raises InsufficientDataError if fewer than n_bars available.
BAR_COLUMNS = ["open", "high", "low", "close", "volume", "vwap", "adjusted"]
def validate_bar_dataframe(df: pd.DataFrame, symbol: str, n_bars: int) -> pd.DataFrame:
"""Validate that a bars DataFrame matches the contract."""
if len(df) < n_bars:
raise InsufficientDataError(
f"{symbol}: requested {n_bars} bars, got {len(df)}"
)
missing = [c for c in BAR_COLUMNS if c not in df.columns]
if missing:
raise ValueError(f"{symbol}: missing columns {missing}")
return df