trading/shared/models/trading.py
Viktor Barzin 82dc622544 feat(kevin): reconcile Alpaca bracket auto-closes + order status
Bracket stop-loss/take-profit legs fill at Alpaca without passing through the executor, so those closes (and their P&L) were invisible locally.

- broker: add get_order(nested) + list_orders to BaseBroker/AlpacaBroker (+ SimulatedBroker); BrokerOrder carries child legs

- Trade gains broker_order_id (migration f6a7b8c9d0e1); executor stamps the entry order id

- new api_gateway trade-reconcile loop: books a closing SELL + realized P&L when a bracket leg fills (idempotent on the leg order id), syncs PENDING->terminal status, logs drift; runs alongside portfolio_sync

[ci skip]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:31:24 +00:00

138 lines
5.4 KiB
Python

"""Trading domain models: Strategy, Signal, Trade, Position, StrategyWeightHistory."""
import enum
import uuid
from sqlalchemy import Boolean, Float, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import JSON, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from shared.models.base import Base, TimestampMixin
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class TradeSide(str, enum.Enum):
BUY = "BUY"
SELL = "SELL"
class TradeStatus(str, enum.Enum):
PENDING = "PENDING"
FILLED = "FILLED"
CANCELLED = "CANCELLED"
REJECTED = "REJECTED"
class SignalDirection(str, enum.Enum):
LONG = "LONG"
SHORT = "SHORT"
NEUTRAL = "NEUTRAL"
# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------
class Strategy(TimestampMixin, Base):
__tablename__ = "strategies"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
current_weight: Mapped[float] = mapped_column(Float, nullable=False, default=0.333)
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
# Relationships
trades: Mapped[list["Trade"]] = relationship(back_populates="strategy")
signals: Mapped[list["Signal"]] = relationship(back_populates="strategy", foreign_keys="Signal.strategy_id", viewonly=True)
weight_history: Mapped[list["StrategyWeightHistory"]] = relationship(back_populates="strategy")
class Signal(TimestampMixin, Base):
__tablename__ = "signals"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
ticker: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
direction: Mapped[SignalDirection] = mapped_column(nullable=False)
strength: Mapped[float] = mapped_column(Float, nullable=False)
strategy_sources: Mapped[dict | None] = mapped_column(JSON, nullable=True)
sentiment_score: Mapped[float | None] = mapped_column(Float, nullable=True)
acted_on: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
strategy_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("strategies.id"), nullable=True
)
# Relationships
strategy: Mapped[Strategy | None] = relationship(back_populates="signals", foreign_keys=[strategy_id])
trades: Mapped[list["Trade"]] = relationship(back_populates="signal")
class Trade(TimestampMixin, Base):
__tablename__ = "trades"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
ticker: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
side: Mapped[TradeSide] = mapped_column(nullable=False)
qty: Mapped[float] = mapped_column(Float, nullable=False)
price: Mapped[float] = mapped_column(Float, nullable=False)
timestamp: Mapped[str | None] = mapped_column(String, nullable=True)
strategy_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("strategies.id"), nullable=True
)
signal_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("signals.id"), nullable=True
)
status: Mapped[TradeStatus] = mapped_column(nullable=False, default=TradeStatus.PENDING)
pnl: Mapped[float | None] = mapped_column(Float, nullable=True)
broker_order_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
# Relationships
strategy: Mapped[Strategy | None] = relationship(back_populates="trades")
signal: Mapped[Signal | None] = relationship(back_populates="trades")
outcome: Mapped["TradeOutcome | None"] = relationship(
"TradeOutcome", back_populates="trade", uselist=False
)
class Position(TimestampMixin, Base):
__tablename__ = "positions"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
ticker: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
qty: Mapped[float] = mapped_column(Float, nullable=False)
avg_entry: Mapped[float] = mapped_column(Float, nullable=False)
unrealized_pnl: Mapped[float | None] = mapped_column(Float, nullable=True)
stop_loss: Mapped[float | None] = mapped_column(Float, nullable=True)
take_profit: Mapped[float | None] = mapped_column(Float, nullable=True)
class StrategyWeightHistory(TimestampMixin, Base):
__tablename__ = "strategy_weight_history"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
strategy_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("strategies.id"), nullable=False
)
old_weight: Mapped[float] = mapped_column(Float, nullable=False)
new_weight: Mapped[float] = mapped_column(Float, nullable=False)
reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
# Relationships
strategy: Mapped[Strategy] = relationship(back_populates="weight_history")
# Avoid circular import — TradeOutcome is defined in learning.py
from shared.models.learning import TradeOutcome # noqa: E402, F401