diff --git a/backend/alembic/versions/6c09aa4368e5_add_signals_table.py b/backend/alembic/versions/6c09aa4368e5_add_signals_table.py new file mode 100644 index 0000000..260c667 --- /dev/null +++ b/backend/alembic/versions/6c09aa4368e5_add_signals_table.py @@ -0,0 +1,47 @@ +"""add signals table + +Revision ID: 6c09aa4368e5 +Revises: 882512221354 +Create Date: 2026-02-19 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '6c09aa4368e5' +down_revision: Union[str, None] = '882512221354' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table('signals', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('ticker', sa.String(length=20), nullable=False), + sa.Column('name', sa.String(length=100), nullable=True), + sa.Column('signal_type', sa.Enum('BUY', 'SELL', 'PARTIAL_SELL', name='signaltype'), nullable=False), + sa.Column('entry_price', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('target_price', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('stop_loss_price', sa.Numeric(precision=12, scale=2), nullable=True), + sa.Column('reason', sa.String(length=200), nullable=True), + sa.Column('status', sa.Enum('ACTIVE', 'EXECUTED', 'EXPIRED', name='signalstatus'), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_signals_date'), 'signals', ['date'], unique=False) + op.create_index(op.f('ix_signals_id'), 'signals', ['id'], unique=False) + op.create_index(op.f('ix_signals_ticker'), 'signals', ['ticker'], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f('ix_signals_ticker'), table_name='signals') + op.drop_index(op.f('ix_signals_id'), table_name='signals') + op.drop_index(op.f('ix_signals_date'), table_name='signals') + op.drop_table('signals') + sa.Enum('BUY', 'SELL', 'PARTIAL_SELL', name='signaltype').drop(op.get_bind(), checkfirst=True) + sa.Enum('ACTIVE', 'EXECUTED', 'EXPIRED', name='signalstatus').drop(op.get_bind(), checkfirst=True) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 0926640..2d01477 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -22,6 +22,7 @@ from app.models.stock import ( AssetClass, JobLog, ) +from app.models.signal import Signal, SignalType, SignalStatus from app.models.backtest import ( Backtest, BacktestStatus, @@ -60,4 +61,7 @@ __all__ = [ "BacktestEquityCurve", "BacktestHolding", "BacktestTransaction", + "Signal", + "SignalType", + "SignalStatus", ] diff --git a/backend/app/models/signal.py b/backend/app/models/signal.py new file mode 100644 index 0000000..fb8e4e7 --- /dev/null +++ b/backend/app/models/signal.py @@ -0,0 +1,40 @@ +""" +Trading signal models. +""" +import enum +from datetime import datetime + +from sqlalchemy import ( + Column, Integer, String, Numeric, DateTime, Date, + Text, Enum as SQLEnum, +) + +from app.core.database import Base + + +class SignalType(str, enum.Enum): + BUY = "buy" + SELL = "sell" + PARTIAL_SELL = "partial_sell" + + +class SignalStatus(str, enum.Enum): + ACTIVE = "active" + EXECUTED = "executed" + EXPIRED = "expired" + + +class Signal(Base): + __tablename__ = "signals" + + id = Column(Integer, primary_key=True, index=True) + date = Column(Date, nullable=False, index=True) + ticker = Column(String(20), nullable=False, index=True) + name = Column(String(100)) + signal_type = Column(SQLEnum(SignalType), nullable=False) + entry_price = Column(Numeric(12, 2)) + target_price = Column(Numeric(12, 2)) + stop_loss_price = Column(Numeric(12, 2)) + reason = Column(String(200)) + status = Column(SQLEnum(SignalStatus), default=SignalStatus.ACTIVE) + created_at = Column(DateTime, default=datetime.utcnow)