Vasily.onl 028371a0e1 Refactor database operations and enhance repository structure
- Introduced a modular repository structure by creating separate repository classes for `Bot`, `MarketData`, and `RawTrade`, improving code organization and maintainability.
- Updated the `DatabaseOperations` class to utilize the new repository classes, enhancing the abstraction of database interactions.
- Refactored the `.env` file to update database connection parameters and add new logging and health monitoring configurations.
- Modified the `okx_config.json` to change default timeframes for trading pairs, aligning with updated requirements.
- Added comprehensive unit tests for the new repository classes, ensuring robust functionality and reliability.

These changes improve the overall architecture of the database layer, making it more scalable and easier to manage.
2025-06-06 21:54:45 +08:00

293 lines
11 KiB
Python

"""
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"<MarketData({self.symbol} {self.timeframe} {self.timestamp} O:{self.open} C:{self.close})>"
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"<RawTrade({self.symbol} {self.data_type} {self.timestamp})>"
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"<Bot({self.name} - {self.strategy_name} - {self.status})>"
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"<Signal({self.signal_type} - {self.price} - {self.confidence})>"
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"<Trade({self.side} {self.quantity} @ {self.price} - PnL: {self.pnl})>"
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"<BotPerformance(Bot {self.bot_id} - Value: {self.total_value} - Win Rate: {self.win_rate:.2f}%)>"
# 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"<SupportedTimeframe({self.timeframe} - {self.description})>"
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"<SupportedExchange({self.exchange} - {self.name} - Enabled: {self.enabled})>"
# 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,
'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)