473 lines
16 KiB
Python
473 lines
16 KiB
Python
"""
|
|
Database Models and Persistence Layer
|
|
=====================================
|
|
|
|
SQLAlchemy models for persistent storage of trading data.
|
|
Uses PostgreSQL with TimescaleDB extension for time-series optimization.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Optional, Dict, Any
|
|
import uuid
|
|
|
|
from sqlalchemy import (
|
|
Column, String, Integer, Float, Decimal as SQLDecimal,
|
|
DateTime, Boolean, JSON, Text, ForeignKey, Index,
|
|
UniqueConstraint, CheckConstraint, create_engine
|
|
)
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import relationship, sessionmaker, Session
|
|
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
|
from sqlalchemy.sql import func
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
class OrderStatus(str, Enum):
|
|
"""Order status enumeration"""
|
|
PENDING = "pending"
|
|
SUBMITTED = "submitted"
|
|
PARTIALLY_FILLED = "partially_filled"
|
|
FILLED = "filled"
|
|
CANCELLED = "cancelled"
|
|
REJECTED = "rejected"
|
|
FAILED = "failed"
|
|
|
|
|
|
class SignalType(str, Enum):
|
|
"""Signal type enumeration"""
|
|
CONGRESSIONAL = "congressional"
|
|
INSIDER = "insider"
|
|
TECHNICAL = "technical"
|
|
FUNDAMENTAL = "fundamental"
|
|
SENTIMENT = "sentiment"
|
|
AI_GENERATED = "ai_generated"
|
|
|
|
|
|
class AlertPriority(str, Enum):
|
|
"""Alert priority levels"""
|
|
CRITICAL = "critical"
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
INFO = "info"
|
|
|
|
|
|
# === Portfolio Models ===
|
|
|
|
class Position(Base):
|
|
"""Current portfolio positions"""
|
|
__tablename__ = "positions"
|
|
__table_args__ = (
|
|
Index('idx_position_ticker', 'ticker'),
|
|
Index('idx_position_updated', 'last_updated'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
ticker = Column(String(10), nullable=False, unique=True)
|
|
shares = Column(Integer, nullable=False)
|
|
avg_cost = Column(SQLDecimal(12, 4), nullable=False)
|
|
current_price = Column(SQLDecimal(12, 4))
|
|
market_value = Column(SQLDecimal(12, 2))
|
|
unrealized_pnl = Column(SQLDecimal(12, 2))
|
|
realized_pnl = Column(SQLDecimal(12, 2))
|
|
percent_change = Column(Float)
|
|
last_updated = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
# Relationships
|
|
orders = relationship("Order", back_populates="position")
|
|
snapshots = relationship("PortfolioSnapshot", back_populates="position")
|
|
|
|
|
|
class PortfolioSnapshot(Base):
|
|
"""Historical portfolio snapshots"""
|
|
__tablename__ = "portfolio_snapshots"
|
|
__table_args__ = (
|
|
Index('idx_snapshot_timestamp', 'timestamp'),
|
|
Index('idx_snapshot_ticker_time', 'ticker', 'timestamp'),
|
|
# TimescaleDB hypertable on timestamp
|
|
{'timescaledb_hypertable': {'time_column': 'timestamp'}}
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
timestamp = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
|
position_id = Column(Integer, ForeignKey('positions.id'))
|
|
ticker = Column(String(10), nullable=False)
|
|
shares = Column(Integer, nullable=False)
|
|
price = Column(SQLDecimal(12, 4), nullable=False)
|
|
value = Column(SQLDecimal(12, 2), nullable=False)
|
|
daily_pnl = Column(SQLDecimal(12, 2))
|
|
total_pnl = Column(SQLDecimal(12, 2))
|
|
|
|
# Account level metrics
|
|
total_value = Column(SQLDecimal(15, 2))
|
|
cash_balance = Column(SQLDecimal(15, 2))
|
|
buying_power = Column(SQLDecimal(15, 2))
|
|
|
|
position = relationship("Position", back_populates="snapshots")
|
|
|
|
|
|
# === Trading Models ===
|
|
|
|
class Order(Base):
|
|
"""Order tracking with full lifecycle"""
|
|
__tablename__ = "orders"
|
|
__table_args__ = (
|
|
Index('idx_order_ticker', 'ticker'),
|
|
Index('idx_order_status', 'status'),
|
|
Index('idx_order_created', 'created_at'),
|
|
UniqueConstraint('idempotency_key', name='uq_order_idempotency'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
order_id = Column(String(50), unique=True) # IBKR order ID
|
|
idempotency_key = Column(UUID(as_uuid=True), default=uuid.uuid4)
|
|
|
|
# Order details
|
|
ticker = Column(String(10), nullable=False)
|
|
position_id = Column(Integer, ForeignKey('positions.id'))
|
|
action = Column(String(10), nullable=False) # BUY/SELL
|
|
order_type = Column(String(20), nullable=False) # MARKET/LIMIT/STOP
|
|
quantity = Column(Integer, nullable=False)
|
|
limit_price = Column(SQLDecimal(12, 4))
|
|
stop_price = Column(SQLDecimal(12, 4))
|
|
|
|
# Status tracking
|
|
status = Column(String(20), nullable=False, default=OrderStatus.PENDING)
|
|
filled_quantity = Column(Integer, default=0)
|
|
avg_fill_price = Column(SQLDecimal(12, 4))
|
|
commission = Column(SQLDecimal(8, 2))
|
|
|
|
# Risk management
|
|
stop_loss_price = Column(SQLDecimal(12, 4))
|
|
take_profit_price = Column(SQLDecimal(12, 4))
|
|
parent_order_id = Column(Integer, ForeignKey('orders.id'))
|
|
|
|
# Metadata
|
|
signal_id = Column(Integer, ForeignKey('signals.id'))
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
submitted_at = Column(DateTime(timezone=True))
|
|
filled_at = Column(DateTime(timezone=True))
|
|
cancelled_at = Column(DateTime(timezone=True))
|
|
notes = Column(Text)
|
|
|
|
# Relationships
|
|
position = relationship("Position", back_populates="orders")
|
|
signal = relationship("Signal", back_populates="orders")
|
|
child_orders = relationship("Order", backref='parent_order')
|
|
|
|
|
|
class Trade(Base):
|
|
"""Executed trades (fills)"""
|
|
__tablename__ = "trades"
|
|
__table_args__ = (
|
|
Index('idx_trade_ticker', 'ticker'),
|
|
Index('idx_trade_executed', 'executed_at'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
order_id = Column(Integer, ForeignKey('orders.id'), nullable=False)
|
|
ticker = Column(String(10), nullable=False)
|
|
action = Column(String(10), nullable=False)
|
|
quantity = Column(Integer, nullable=False)
|
|
price = Column(SQLDecimal(12, 4), nullable=False)
|
|
commission = Column(SQLDecimal(8, 2))
|
|
executed_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
|
pnl = Column(SQLDecimal(12, 2))
|
|
|
|
|
|
# === Signal Models ===
|
|
|
|
class Signal(Base):
|
|
"""Trading signals generated by the system"""
|
|
__tablename__ = "signals"
|
|
__table_args__ = (
|
|
Index('idx_signal_ticker', 'ticker'),
|
|
Index('idx_signal_type', 'signal_type'),
|
|
Index('idx_signal_created', 'created_at'),
|
|
Index('idx_signal_confidence', 'confidence'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
ticker = Column(String(10), nullable=False)
|
|
signal_type = Column(String(20), nullable=False)
|
|
action = Column(String(10), nullable=False) # BUY/SELL/HOLD
|
|
confidence = Column(Float, nullable=False)
|
|
|
|
# Price targets
|
|
current_price = Column(SQLDecimal(12, 4))
|
|
entry_price_min = Column(SQLDecimal(12, 4))
|
|
entry_price_max = Column(SQLDecimal(12, 4))
|
|
target_price_1 = Column(SQLDecimal(12, 4))
|
|
target_price_2 = Column(SQLDecimal(12, 4))
|
|
stop_loss = Column(SQLDecimal(12, 4))
|
|
|
|
# Sizing
|
|
position_size = Column(Float) # Percentage of portfolio
|
|
risk_level = Column(String(10)) # LOW/MEDIUM/HIGH
|
|
|
|
# Metadata
|
|
reasoning = Column(Text)
|
|
data_sources = Column(JSONB) # JSON array of sources
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
expires_at = Column(DateTime(timezone=True))
|
|
|
|
# Tracking
|
|
acted_upon = Column(Boolean, default=False)
|
|
acted_at = Column(DateTime(timezone=True))
|
|
performance = Column(JSONB) # Track signal accuracy
|
|
|
|
# Relationships
|
|
orders = relationship("Order", back_populates="signal")
|
|
|
|
|
|
# === Alternative Data Models ===
|
|
|
|
class CongressionalTrade(Base):
|
|
"""Congressional trading activity"""
|
|
__tablename__ = "congressional_trades"
|
|
__table_args__ = (
|
|
Index('idx_congress_ticker', 'ticker'),
|
|
Index('idx_congress_politician', 'politician'),
|
|
Index('idx_congress_filed', 'filing_date'),
|
|
UniqueConstraint('politician', 'ticker', 'transaction_date',
|
|
name='uq_congress_trade'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
politician = Column(String(100), nullable=False)
|
|
ticker = Column(String(10), nullable=False)
|
|
action = Column(String(20), nullable=False) # purchase/sale
|
|
amount_range = Column(String(50))
|
|
transaction_date = Column(DateTime(timezone=True), nullable=False)
|
|
filing_date = Column(DateTime(timezone=True), nullable=False)
|
|
party = Column(String(20))
|
|
state = Column(String(5))
|
|
chamber = Column(String(10)) # house/senate
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
|
|
class InsiderTrade(Base):
|
|
"""Insider trading activity"""
|
|
__tablename__ = "insider_trades"
|
|
__table_args__ = (
|
|
Index('idx_insider_ticker', 'ticker'),
|
|
Index('idx_insider_name', 'insider_name'),
|
|
Index('idx_insider_date', 'transaction_date'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
insider_name = Column(String(100), nullable=False)
|
|
ticker = Column(String(10), nullable=False)
|
|
action = Column(String(10), nullable=False) # Buy/Sell
|
|
shares = Column(Integer)
|
|
value = Column(SQLDecimal(15, 2))
|
|
transaction_date = Column(DateTime(timezone=True), nullable=False)
|
|
position = Column(String(50)) # CEO/CFO/Director
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
|
|
# === Alert Models ===
|
|
|
|
class Alert(Base):
|
|
"""Alert history and tracking"""
|
|
__tablename__ = "alerts"
|
|
__table_args__ = (
|
|
Index('idx_alert_type', 'alert_type'),
|
|
Index('idx_alert_priority', 'priority'),
|
|
Index('idx_alert_created', 'created_at'),
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
title = Column(String(200), nullable=False)
|
|
message = Column(Text, nullable=False)
|
|
alert_type = Column(String(30), nullable=False)
|
|
priority = Column(String(10), nullable=False)
|
|
|
|
# Delivery tracking
|
|
channels = Column(JSONB) # ['discord', 'telegram', 'email']
|
|
sent_successfully = Column(Boolean, default=False)
|
|
send_attempts = Column(Integer, default=0)
|
|
error_message = Column(Text)
|
|
|
|
# Metadata
|
|
data = Column(JSONB)
|
|
ticker = Column(String(10))
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
sent_at = Column(DateTime(timezone=True))
|
|
|
|
# Deduplication
|
|
hash = Column(String(64), index=True)
|
|
|
|
|
|
# === Performance Models ===
|
|
|
|
class PerformanceMetric(Base):
|
|
"""Strategy performance tracking"""
|
|
__tablename__ = "performance_metrics"
|
|
__table_args__ = (
|
|
Index('idx_perf_date', 'date'),
|
|
{'timescaledb_hypertable': {'time_column': 'date'}}
|
|
)
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
date = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
|
|
|
# Daily metrics
|
|
total_pnl = Column(SQLDecimal(12, 2))
|
|
realized_pnl = Column(SQLDecimal(12, 2))
|
|
unrealized_pnl = Column(SQLDecimal(12, 2))
|
|
win_rate = Column(Float)
|
|
sharpe_ratio = Column(Float)
|
|
max_drawdown = Column(Float)
|
|
|
|
# Trade statistics
|
|
total_trades = Column(Integer)
|
|
winning_trades = Column(Integer)
|
|
losing_trades = Column(Integer)
|
|
avg_win = Column(SQLDecimal(10, 2))
|
|
avg_loss = Column(SQLDecimal(10, 2))
|
|
|
|
# Signal statistics
|
|
signals_generated = Column(Integer)
|
|
signals_acted = Column(Integer)
|
|
signal_accuracy = Column(Float)
|
|
|
|
# Risk metrics
|
|
portfolio_beta = Column(Float)
|
|
portfolio_volatility = Column(Float)
|
|
value_at_risk = Column(SQLDecimal(12, 2))
|
|
|
|
|
|
# === Database Manager ===
|
|
|
|
class DatabaseManager:
|
|
"""Database connection and session management"""
|
|
|
|
def __init__(self, connection_string: str):
|
|
"""
|
|
Initialize database manager
|
|
|
|
Args:
|
|
connection_string: PostgreSQL connection string
|
|
"""
|
|
self.engine = create_engine(
|
|
connection_string,
|
|
pool_size=20,
|
|
max_overflow=40,
|
|
pool_pre_ping=True, # Check connections before using
|
|
echo=False
|
|
)
|
|
self.SessionLocal = sessionmaker(
|
|
autocommit=False,
|
|
autoflush=False,
|
|
bind=self.engine
|
|
)
|
|
|
|
def init_database(self):
|
|
"""Create all tables and indexes"""
|
|
Base.metadata.create_all(bind=self.engine)
|
|
|
|
# Create TimescaleDB hypertables
|
|
with self.engine.connect() as conn:
|
|
# Convert tables to hypertables for time-series optimization
|
|
conn.execute("""
|
|
SELECT create_hypertable('portfolio_snapshots', 'timestamp',
|
|
if_not_exists => TRUE);
|
|
""")
|
|
conn.execute("""
|
|
SELECT create_hypertable('performance_metrics', 'date',
|
|
if_not_exists => TRUE);
|
|
""")
|
|
|
|
def get_session(self) -> Session:
|
|
"""Get a new database session"""
|
|
return self.SessionLocal()
|
|
|
|
def save_position(self, position_data: Dict[str, Any]) -> Position:
|
|
"""Save or update a position"""
|
|
with self.get_session() as session:
|
|
position = session.query(Position).filter_by(
|
|
ticker=position_data['ticker']
|
|
).first()
|
|
|
|
if position:
|
|
# Update existing
|
|
for key, value in position_data.items():
|
|
setattr(position, key, value)
|
|
else:
|
|
# Create new
|
|
position = Position(**position_data)
|
|
session.add(position)
|
|
|
|
session.commit()
|
|
session.refresh(position)
|
|
return position
|
|
|
|
def save_signal(self, signal_data: Dict[str, Any]) -> Signal:
|
|
"""Save a new signal"""
|
|
with self.get_session() as session:
|
|
signal = Signal(**signal_data)
|
|
session.add(signal)
|
|
session.commit()
|
|
session.refresh(signal)
|
|
return signal
|
|
|
|
def save_order(self, order_data: Dict[str, Any]) -> Order:
|
|
"""Save a new order with idempotency"""
|
|
with self.get_session() as session:
|
|
# Check idempotency
|
|
if 'idempotency_key' in order_data:
|
|
existing = session.query(Order).filter_by(
|
|
idempotency_key=order_data['idempotency_key']
|
|
).first()
|
|
if existing:
|
|
return existing
|
|
|
|
order = Order(**order_data)
|
|
session.add(order)
|
|
session.commit()
|
|
session.refresh(order)
|
|
return order
|
|
|
|
def update_order_status(self, order_id: str, status: OrderStatus,
|
|
**kwargs) -> Optional[Order]:
|
|
"""Update order status"""
|
|
with self.get_session() as session:
|
|
order = session.query(Order).filter_by(order_id=order_id).first()
|
|
if order:
|
|
order.status = status
|
|
for key, value in kwargs.items():
|
|
setattr(order, key, value)
|
|
session.commit()
|
|
session.refresh(order)
|
|
return order
|
|
|
|
def get_active_positions(self) -> list[Position]:
|
|
"""Get all active positions"""
|
|
with self.get_session() as session:
|
|
return session.query(Position).filter(
|
|
Position.shares > 0
|
|
).all()
|
|
|
|
def get_recent_signals(self, ticker: Optional[str] = None,
|
|
hours: int = 24) -> list[Signal]:
|
|
"""Get recent signals"""
|
|
with self.get_session() as session:
|
|
query = session.query(Signal).filter(
|
|
Signal.created_at >= func.now() - timedelta(hours=hours)
|
|
)
|
|
if ticker:
|
|
query = query.filter(Signal.ticker == ticker)
|
|
return query.order_by(Signal.confidence.desc()).all()
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
# Initialize database
|
|
db = DatabaseManager("postgresql://trader:password@localhost/trading_db")
|
|
db.init_database()
|
|
|
|
print("✅ Database initialized successfully") |