""" Database Models for Crypto Trading Bot Platform SQLAlchemy models corresponding to the database schema_clean.sql """ from datetime import datetime from decimal import Decimal from typing import Optional, Dict, Any from sqlalchemy import ( Boolean, Column, DateTime, ForeignKey, Integer, String, Text, DECIMAL, Index, CheckConstraint, UniqueConstraint, text ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.sql import func # Create base class for all models Base = declarative_base() class MarketData(Base): """OHLCV Market Data - Primary table for bot operations""" __tablename__ = 'market_data' id = Column(Integer, primary_key=True) exchange = Column(String(50), nullable=False, default='okx') symbol = Column(String(20), nullable=False) timeframe = Column(String(5), nullable=False) # 1m, 5m, 15m, 1h, 4h, 1d timestamp = Column(DateTime(timezone=True), nullable=False) open = Column(DECIMAL(18, 8), nullable=False) high = Column(DECIMAL(18, 8), nullable=False) low = Column(DECIMAL(18, 8), nullable=False) close = Column(DECIMAL(18, 8), nullable=False) volume = Column(DECIMAL(18, 8), nullable=False) trades_count = Column(Integer) created_at = Column(DateTime(timezone=True), default=func.now()) # Constraints __table_args__ = ( UniqueConstraint('exchange', 'symbol', 'timeframe', 'timestamp', name='unique_market_data'), Index('idx_market_data_lookup', 'symbol', 'timeframe', 'timestamp'), Index('idx_market_data_recent', 'timestamp'), Index('idx_market_data_symbol', 'symbol'), Index('idx_market_data_timeframe', 'timeframe'), ) def __repr__(self): return f"" class RawTrade(Base): """Raw Trade Data - For debugging, compliance, and detailed backtesting""" __tablename__ = 'raw_trades' id = Column(Integer, primary_key=True) exchange = Column(String(50), nullable=False, default='okx') symbol = Column(String(20), nullable=False) timestamp = Column(DateTime(timezone=True), nullable=False) data_type = Column(String(20), nullable=False) # ticker, trade, orderbook, candle, balance raw_data = Column(JSONB, nullable=False) # Complete API response created_at = Column(DateTime(timezone=True), default=func.now()) __table_args__ = ( Index('idx_raw_trades_symbol_time', 'symbol', 'timestamp'), Index('idx_raw_trades_type', 'data_type'), Index('idx_raw_trades_timestamp', 'timestamp'), Index('idx_raw_trades_recent', 'created_at'), ) def __repr__(self): return f"" class Bot(Base): """Bot Management - Bot instances with configuration""" __tablename__ = 'bots' id = Column(Integer, primary_key=True) name = Column(String(100), nullable=False) strategy_name = Column(String(50), nullable=False) symbol = Column(String(20), nullable=False) timeframe = Column(String(5), nullable=False) status = Column(String(20), nullable=False, default='inactive') config_file = Column(String(200)) # Path to JSON config virtual_balance = Column(DECIMAL(18, 8), default=Decimal('10000')) current_balance = Column(DECIMAL(18, 8), default=Decimal('10000')) last_heartbeat = Column(DateTime(timezone=True)) created_at = Column(DateTime(timezone=True), default=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # Relationships signals = relationship("Signal", back_populates="bot", cascade="all, delete-orphan") trades = relationship("Trade", back_populates="bot", cascade="all, delete-orphan") performance = relationship("BotPerformance", back_populates="bot", cascade="all, delete-orphan") __table_args__ = ( CheckConstraint("status IN ('active', 'inactive', 'error', 'paused')", name='chk_bot_status'), Index('idx_bots_status', 'status'), Index('idx_bots_symbol', 'symbol'), Index('idx_bots_strategy', 'strategy_name'), Index('idx_bots_last_heartbeat', 'last_heartbeat'), ) @property def pnl(self) -> Decimal: """Calculate current profit/loss""" return self.current_balance - self.virtual_balance @property def is_active(self) -> bool: """Check if bot is currently active""" return self.status == 'active' def __repr__(self): return f"" class Signal(Base): """Trading Signals - Generated by strategies for analysis""" __tablename__ = 'signals' id = Column(Integer, primary_key=True) bot_id = Column(Integer, ForeignKey('bots.id', ondelete='CASCADE'), nullable=False) timestamp = Column(DateTime(timezone=True), nullable=False) signal_type = Column(String(10), nullable=False) # buy, sell, hold price = Column(DECIMAL(18, 8)) confidence = Column(DECIMAL(5, 4)) # 0.0000 to 1.0000 indicators = Column(JSONB) # Technical indicator values (using JSONB to match schema_clean.sql) created_at = Column(DateTime(timezone=True), default=func.now()) # Relationships bot = relationship("Bot", back_populates="signals") trades = relationship("Trade", back_populates="signal") __table_args__ = ( CheckConstraint("signal_type IN ('buy', 'sell', 'hold')", name='chk_signal_type'), CheckConstraint("confidence >= 0 AND confidence <= 1", name='chk_confidence'), Index('idx_signals_bot_time', 'bot_id', 'timestamp'), Index('idx_signals_type', 'signal_type'), Index('idx_signals_timestamp', 'timestamp'), ) def __repr__(self): return f"" class Trade(Base): """Trade Execution Records - Virtual trading results""" __tablename__ = 'trades' id = Column(Integer, primary_key=True) bot_id = Column(Integer, ForeignKey('bots.id', ondelete='CASCADE'), nullable=False) signal_id = Column(Integer, ForeignKey('signals.id', ondelete='SET NULL')) timestamp = Column(DateTime(timezone=True), nullable=False) side = Column(String(5), nullable=False) # buy, sell price = Column(DECIMAL(18, 8), nullable=False) quantity = Column(DECIMAL(18, 8), nullable=False) fees = Column(DECIMAL(18, 8), default=Decimal('0')) pnl = Column(DECIMAL(18, 8)) # Profit/loss for this trade balance_after = Column(DECIMAL(18, 8)) # Portfolio balance after trade created_at = Column(DateTime(timezone=True), default=func.now()) # Relationships bot = relationship("Bot", back_populates="trades") signal = relationship("Signal", back_populates="trades") __table_args__ = ( CheckConstraint("side IN ('buy', 'sell')", name='chk_trade_side'), CheckConstraint("price > 0", name='chk_positive_price'), CheckConstraint("quantity > 0", name='chk_positive_quantity'), CheckConstraint("fees >= 0", name='chk_non_negative_fees'), Index('idx_trades_bot_time', 'bot_id', 'timestamp'), Index('idx_trades_side', 'side'), Index('idx_trades_timestamp', 'timestamp'), ) @property def trade_value(self) -> Decimal: """Calculate the total value of this trade""" return self.price * self.quantity @property def net_pnl(self) -> Decimal: """Calculate net PnL after fees""" return (self.pnl or Decimal('0')) - self.fees def __repr__(self): return f"" class BotPerformance(Base): """Bot Performance Snapshots - For portfolio visualization""" __tablename__ = 'bot_performance' id = Column(Integer, primary_key=True) bot_id = Column(Integer, ForeignKey('bots.id', ondelete='CASCADE'), nullable=False) timestamp = Column(DateTime(timezone=True), nullable=False) total_value = Column(DECIMAL(18, 8), nullable=False) cash_balance = Column(DECIMAL(18, 8), nullable=False) crypto_balance = Column(DECIMAL(18, 8), nullable=False) total_trades = Column(Integer, default=0) winning_trades = Column(Integer, default=0) total_fees = Column(DECIMAL(18, 8), default=Decimal('0')) created_at = Column(DateTime(timezone=True), default=func.now()) # Relationships bot = relationship("Bot", back_populates="performance") __table_args__ = ( CheckConstraint( "total_value >= 0 AND cash_balance >= 0 AND crypto_balance >= 0 AND " "total_trades >= 0 AND winning_trades >= 0 AND total_fees >= 0", name='chk_non_negative_values' ), CheckConstraint("winning_trades <= total_trades", name='chk_winning_trades_logic'), Index('idx_bot_performance_bot_time', 'bot_id', 'timestamp'), Index('idx_bot_performance_timestamp', 'timestamp'), ) @property def win_rate(self) -> float: """Calculate win rate percentage""" if self.total_trades == 0: return 0.0 return (self.winning_trades / self.total_trades) * 100 @property def portfolio_allocation(self) -> Dict[str, float]: """Calculate portfolio allocation percentages""" if self.total_value == 0: return {"cash": 0.0, "crypto": 0.0} cash_pct = float(self.cash_balance / self.total_value * 100) crypto_pct = float(self.crypto_balance / self.total_value * 100) return {"cash": cash_pct, "crypto": crypto_pct} def __repr__(self): return f"" class StrategyRun(Base): """Strategy Execution Sessions - For tracking strategy backtesting and analysis runs""" __tablename__ = 'strategy_runs' id = Column(Integer, primary_key=True) strategy_name = Column(String(100), nullable=False) symbol = Column(String(20), nullable=False) timeframe = Column(String(5), nullable=False) start_time = Column(DateTime(timezone=True), nullable=False) end_time = Column(DateTime(timezone=True)) status = Column(String(20), nullable=False, default='running') # running, completed, failed config = Column(JSONB) # Strategy configuration parameters run_metadata = Column(JSONB) # Run metadata (backtesting params, etc.) total_signals = Column(Integer, default=0) created_at = Column(DateTime(timezone=True), default=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # Relationships strategy_signals = relationship("StrategySignal", back_populates="strategy_run", cascade="all, delete-orphan") __table_args__ = ( CheckConstraint("status IN ('running', 'completed', 'failed')", name='chk_strategy_run_status'), Index('idx_strategy_runs_strategy_time', 'strategy_name', 'start_time'), Index('idx_strategy_runs_symbol', 'symbol'), Index('idx_strategy_runs_status', 'status'), Index('idx_strategy_runs_timeframe', 'timeframe'), ) def __repr__(self): return f"" class StrategySignal(Base): """Strategy Analysis Signals - Generated by strategies for analysis and backtesting (separate from bot signals)""" __tablename__ = 'strategy_signals' id = Column(Integer, primary_key=True) run_id = Column(Integer, ForeignKey('strategy_runs.id', ondelete='CASCADE'), nullable=False) strategy_name = Column(String(100), nullable=False) strategy_config = Column(JSONB) # Strategy configuration used for this signal symbol = Column(String(20), nullable=False) timeframe = Column(String(5), nullable=False) timestamp = Column(DateTime(timezone=True), nullable=False) signal_type = Column(String(20), nullable=False) # buy, sell, hold, entry_long, exit_long, etc. price = Column(DECIMAL(18, 8)) confidence = Column(DECIMAL(5, 4)) # 0.0000 to 1.0000 signal_metadata = Column(JSONB) # Additional signal metadata (indicator values, etc.) created_at = Column(DateTime(timezone=True), default=func.now()) # Relationships strategy_run = relationship("StrategyRun", back_populates="strategy_signals") __table_args__ = ( CheckConstraint("signal_type IN ('buy', 'sell', 'hold', 'entry_long', 'exit_long', 'entry_short', 'exit_short', 'stop_loss', 'take_profit')", name='chk_strategy_signal_type'), CheckConstraint("confidence >= 0 AND confidence <= 1", name='chk_strategy_confidence'), Index('idx_strategy_signals_strategy_time', 'strategy_name', 'timestamp'), Index('idx_strategy_signals_run_time', 'run_id', 'timestamp'), Index('idx_strategy_signals_symbol_timeframe', 'symbol', 'timeframe'), Index('idx_strategy_signals_type', 'signal_type'), Index('idx_strategy_signals_timestamp', 'timestamp'), ) def __repr__(self): return f"" # Reference tables for system configuration class SupportedTimeframe(Base): """Supported timeframes configuration""" __tablename__ = 'supported_timeframes' timeframe = Column(String(5), primary_key=True) description = Column(String(50)) minutes = Column(Integer) def __repr__(self): return f"" class SupportedExchange(Base): """Supported exchanges configuration""" __tablename__ = 'supported_exchanges' exchange = Column(String(50), primary_key=True) name = Column(String(100)) api_url = Column(String(200)) enabled = Column(Boolean, default=True) def __repr__(self): return f"" # Helper functions for model operations def get_model_by_table_name(table_name: str): """Get model class by table name""" table_to_model = { 'market_data': MarketData, 'raw_trades': RawTrade, 'bots': Bot, 'signals': Signal, 'trades': Trade, 'bot_performance': BotPerformance, 'strategy_runs': StrategyRun, 'strategy_signals': StrategySignal, 'supported_timeframes': SupportedTimeframe, 'supported_exchanges': SupportedExchange, } return table_to_model.get(table_name) def create_all_tables(engine): """Create all tables in the database""" Base.metadata.create_all(engine) def drop_all_tables(engine): """Drop all tables from the database""" Base.metadata.drop_all(engine)