feat: database models and alembic migrations — all tables per design
- shared/db.py: async engine + session factory - shared/models/base.py: DeclarativeBase + TimestampMixin - shared/models/trading.py: Strategy, Signal, Trade, Position, StrategyWeightHistory - shared/models/news.py: Article, ArticleSentiment - shared/models/learning.py: TradeOutcome, LearningAdjustment - shared/models/auth.py: User, UserCredential - shared/models/timeseries.py: MarketData, PortfolioSnapshot, StrategyMetric - Alembic async env.py with initial migration including TimescaleDB hypertables - 21 model tests covering enums, instantiation, metadata registration
This commit is contained in:
parent
ae5b3f89d1
commit
72cb1b6fe5
23 changed files with 1283 additions and 0 deletions
137
shared/models/trading.py
Normal file
137
shared/models/trading.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""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)
|
||||
|
||||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue