""" Signal Engine Database Schema Audit-grade SQLite schema with full batch commitment ledger """ from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, Boolean, Text, ForeignKey, Index, JSON from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from datetime import datetime import hashlib import json Base = declarative_base() class Symbol(Base): __tablename__ = 'symbols' id = Column(Integer, primary_key=True) symbol = Column(String(50), unique=True, nullable=False, index=True) name = Column(String(100)) min_price = Column(Float, default=0.10) min_volume_24h = Column(Float, default=50000.0) active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) candles = relationship("Candle", back_populates="symbol_obj", primaryjoin="Symbol.symbol == Candle.symbol") predictions = relationship("Prediction", back_populates="symbol_obj", primaryjoin="Symbol.symbol == Prediction.symbol") class Candle(Base): __tablename__ = 'candles' id = Column(Integer, primary_key=True) symbol = Column(String(50), ForeignKey('symbols.symbol'), nullable=False, index=True) timestamp = Column(DateTime, nullable=False, index=True) open = Column(Float, nullable=False) high = Column(Float, nullable=False) low = Column(Float, nullable=False) close = Column(Float, nullable=False) volume = Column(Float, nullable=False) quote_volume = Column(Float, nullable=False) symbol_obj = relationship("Symbol", back_populates="candles") __table_args__ = ( Index('idx_symbol_timestamp', 'symbol', 'timestamp'), ) class PredictionBatch(Base): __tablename__ = 'prediction_batches' id = Column(Integer, primary_key=True) batch_id = Column(String(64), unique=True, nullable=False, index=True) target_hour = Column(DateTime, nullable=False, index=True) committed_at = Column(DateTime, default=datetime.utcnow, nullable=False) # Full batch commitment hash (SHA-256 of canonical JSON) batch_hash = Column(String(64), nullable=False, index=True) # Previous batch hash for chain verification prev_batch_hash = Column(String(64), nullable=True, index=True) # Model version info model_version = Column(String(50)) ensemble_weights = Column(JSON) # Batch metadata num_predictions = Column(Integer, default=0) avg_confidence = Column(Float) # Scoring (filled later) scored_at = Column(DateTime, nullable=True) accuracy = Column(Float, nullable=True) brier_score = Column(Float, nullable=True) hypothetical_pnl_bps = Column(Float, nullable=True) predictions = relationship("Prediction", back_populates="batch") __table_args__ = ( Index('idx_target_hour', 'target_hour'), Index('idx_committed_at', 'committed_at'), ) class Prediction(Base): __tablename__ = 'predictions' id = Column(Integer, primary_key=True) batch_id = Column(String(64), ForeignKey('prediction_batches.batch_id'), nullable=False, index=True) symbol = Column(String(50), ForeignKey('symbols.symbol'), nullable=False, index=True) # Target info target_hour = Column(DateTime, nullable=False, index=True) entry_price = Column(Float, nullable=False) # Model output direction = Column(String(10), nullable=False) # LONG, SHORT, FLAT probability_up = Column(Float, nullable=False) confidence = Column(Float, nullable=False) suggested_position = Column(Float, nullable=False) # -1 to 1 # Feature hash for reproducibility feature_hash = Column(String(64), nullable=False) # Scoring (filled later) exit_price = Column(Float, nullable=True) actual_return = Column(Float, nullable=True) correct = Column(Boolean, nullable=True) scored_at = Column(DateTime, nullable=True) batch = relationship("PredictionBatch", back_populates="predictions") symbol_obj = relationship("Symbol", back_populates="predictions") __table_args__ = ( Index('idx_symbol_target', 'symbol', 'target_hour'), ) class LedgerEntry(Base): __tablename__ = 'ledger' id = Column(Integer, primary_key=True) entry_hash = Column(String(64), unique=True, nullable=False, index=True) prev_hash = Column(String(64), nullable=True, index=True) entry_type = Column(String(20), nullable=False) # BATCH_COMMIT, BATCH_SCORE, KEY_ISSUE, etc. timestamp = Column(DateTime, default=datetime.utcnow, nullable=False, index=True) # Full payload for audit payload = Column(Text, nullable=False) payload_hash = Column(String(64), nullable=False) # Signature (if applicable) signature = Column(String(128), nullable=True) __table_args__ = ( Index('idx_timestamp', 'timestamp'), Index('idx_entry_type', 'entry_type'), ) class Subscriber(Base): __tablename__ = 'subscribers' id = Column(Integer, primary_key=True) email = Column(String(255), unique=True, nullable=True, index=True) wallet_address = Column(String(255), unique=True, nullable=True, index=True) stripe_customer_id = Column(String(100), nullable=True, index=True) # Subscription status tier = Column(String(20), default='free') # free, pro, enterprise active = Column(Boolean, default=True) expires_at = Column(DateTime, nullable=True) # API access api_key = Column(String(64), unique=True, nullable=True, index=True) api_key_hash = Column(String(64), nullable=True, index=True) rate_limit_per_hour = Column(Integer, default=100) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class ApiUsage(Base): __tablename__ = 'api_usage' id = Column(Integer, primary_key=True) subscriber_id = Column(Integer, ForeignKey('subscribers.id'), nullable=False, index=True) endpoint = Column(String(100), nullable=False) timestamp = Column(DateTime, default=datetime.utcnow, index=True) status_code = Column(Integer, nullable=False) __table_args__ = ( Index('idx_subscriber_timestamp', 'subscriber_id', 'timestamp'), ) class ModelState(Base): __tablename__ = 'model_state' id = Column(Integer, primary_key=True) model_name = Column(String(50), unique=True, nullable=False, index=True) version = Column(String(50), nullable=False) # Serialized model (pickle bytes) model_blob = Column(Text, nullable=False) # Model metadata trained_at = Column(DateTime, nullable=False) training_samples = Column(Integer, default=0) feature_importance = Column(JSON, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) def compute_batch_hash(predictions: list, metadata: dict) -> str: """Compute SHA-256 hash of canonical prediction batch""" canonical = { 'predictions': sorted(predictions, key=lambda x: (x['symbol'], x['target_hour'])), 'metadata': metadata } canonical_str = json.dumps(canonical, sort_keys=True, separators=(',', ':')) return hashlib.sha256(canonical_str.encode()).hexdigest() def compute_ledger_entry(entry_type: str, payload: dict, prev_hash: str = None) -> tuple: """Compute ledger entry hash and return entry data""" canonical = { 'type': entry_type, 'timestamp': datetime.utcnow().isoformat(), 'payload': payload, 'prev_hash': prev_hash } canonical_str = json.dumps(canonical, sort_keys=True, separators=(',', ':')) entry_hash = hashlib.sha256(canonical_str.encode()).hexdigest() payload_hash = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest() return entry_hash, payload_hash, canonical_str def init_db(db_url: str = "sqlite:///signal_engine.db"): """Initialize database with all tables""" engine = create_engine(db_url) Base.metadata.create_all(engine) return engine def get_session(db_url: str = "sqlite:///signal_engine.db"): """Get database session""" engine = create_engine(db_url) SessionLocal = sessionmaker(bind=engine) return SessionLocal()