Files
foxhunt/migrations/016_trading_service_events.sql
jgrusewski 0d9f890aa6 🗄️ Wave 112: Migration cleanup and new schemas
- Deprecated/broken migration files archived
- New auth_schema migration (015)
- Trading service events migration (016)
- Migration renumbering utility
- Migration test suite
2025-10-05 22:23:37 +02:00

892 lines
39 KiB
PL/PgSQL

-- Migration: Comprehensive Event Storage for Trading Service
-- This migration implements comprehensive event storage for HFT compliance and audit trails
-- Designed for regulatory compliance (MiFID II, SOX, Dodd-Frank) with nanosecond precision
-- Enable required extensions for HFT event storage
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- =======================================================================================
-- TRADING EVENTS TABLE - Core order lifecycle events
-- =======================================================================================
CREATE TABLE IF NOT EXISTS trading_events (
id UUID NOT NULL DEFAULT uuid_generate_v4(),
event_id UUID NOT NULL DEFAULT uuid_generate_v4(), -- Unique event identifier
correlation_id UUID, -- Link related events together
event_type VARCHAR(50) NOT NULL, -- 'order_created', 'order_modified', 'order_cancelled', 'order_filled', 'order_expired', 'order_rejected'
event_subtype VARCHAR(50), -- Additional event classification
-- Core order information
order_id UUID, -- Reference to orders table
original_order_id UUID, -- For tracking order modifications
client_order_id VARCHAR(128), -- Client-provided identifier
exchange_order_id VARCHAR(128), -- Exchange-provided identifier
-- Instrument and trading details
symbol VARCHAR(32) NOT NULL,
instrument_id VARCHAR(64),
side VARCHAR(10) NOT NULL CHECK (side IN ('buy', 'sell')),
order_type VARCHAR(20) NOT NULL,
quantity BIGINT NOT NULL CHECK (quantity > 0),
price BIGINT, -- Price in fixed-point cents
filled_quantity BIGINT DEFAULT 0,
remaining_quantity BIGINT DEFAULT 0,
-- Execution details
execution_price BIGINT, -- Actual fill price
execution_quantity BIGINT, -- Quantity filled in this event
execution_id VARCHAR(128), -- Venue execution ID
venue VARCHAR(64), -- Execution venue
is_maker BOOLEAN, -- Maker/taker flag
fees BIGINT, -- Trading fees in fixed-point cents
fee_currency VARCHAR(10), -- Fee currency
-- Account and strategy information
account_id VARCHAR(64),
portfolio_id VARCHAR(64),
strategy_id VARCHAR(100),
trader_id VARCHAR(64),
-- Risk and compliance
risk_check_result VARCHAR(20), -- 'approved', 'rejected', 'warning'
risk_violations JSONB, -- Array of risk violations
compliance_flags JSONB, -- Regulatory compliance flags
-- Timing information (nanosecond precision for HFT)
event_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
market_timestamp TIMESTAMP WITH TIME ZONE, -- Exchange timestamp
received_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
gateway_timestamp TIMESTAMP WITH TIME ZONE, -- Gateway receive time
processing_start_timestamp TIMESTAMP WITH TIME ZONE, -- When processing started
processing_end_timestamp TIMESTAMP WITH TIME ZONE, -- When processing completed
latency_ns BIGINT, -- Processing latency in nanoseconds
-- Order state tracking
order_status VARCHAR(20), -- Current order status
previous_status VARCHAR(20), -- Previous order status
status_reason VARCHAR(255), -- Reason for status change
-- Market conditions
market_data_snapshot JSONB, -- Market data at time of event
bid_price BIGINT, -- Best bid at time of event
ask_price BIGINT, -- Best ask at time of event
last_price BIGINT, -- Last trade price
volume BIGINT, -- Market volume
-- System information
source_system VARCHAR(50) NOT NULL, -- System that generated the event
source_component VARCHAR(50), -- Component within system
version VARCHAR(20), -- Event schema version
-- Additional data
metadata JSONB, -- Flexible additional data
raw_message TEXT, -- Original message (if applicable)
PRIMARY KEY (id, event_timestamp) -- Composite key for partitioning
) PARTITION BY RANGE (event_timestamp);
-- Create partitions for trading events (6 months of partitions)
-- Only create monthly partitions if the table doesn't already have partitions (from migration 001)
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
existing_partitions INTEGER;
BEGIN
-- Check if partitions already exist (from migration 001 which creates daily partitions)
SELECT COUNT(*) INTO existing_partitions
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'trading_events';
-- Only create monthly partitions if no partitions exist yet
IF existing_partitions = 0 THEN
FOR i IN 0..5 LOOP
partition_start := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
partition_end := partition_start + INTERVAL '1 month';
partition_name := 'trading_events_' || to_char(partition_start, 'YYYY_MM');
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF trading_events
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
-- Add indexes to each partition
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON %I(order_id, event_timestamp)',
'idx_' || partition_name || '_order_id', partition_name);
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON %I(symbol, event_type, event_timestamp)',
'idx_' || partition_name || '_symbol_type', partition_name);
EXECUTE format('CREATE INDEX IF NOT EXISTS %I ON %I(account_id, event_timestamp)',
'idx_' || partition_name || '_account', partition_name);
END LOOP;
END IF;
END $$;
-- =======================================================================================
-- RISK EVENTS TABLE - Risk management decisions and alerts
-- =======================================================================================
CREATE TABLE IF NOT EXISTS risk_events (
id UUID NOT NULL DEFAULT uuid_generate_v4(),
event_id UUID NOT NULL DEFAULT uuid_generate_v4(),
correlation_id UUID, -- Link to trading events
-- Event classification
event_type VARCHAR(50) NOT NULL, -- 'limit_check', 'violation', 'alert', 'emergency_stop', 'position_limit', 'var_breach'
severity VARCHAR(20) NOT NULL CHECK (severity IN ('info', 'warning', 'error', 'critical')),
-- Risk details
risk_type VARCHAR(50) NOT NULL, -- 'position_size', 'concentration', 'var', 'leverage', 'drawdown', 'exposure'
violation_type VARCHAR(50), -- Specific violation type
-- Values and limits
current_value DECIMAL(20, 8), -- Current risk metric value
limit_value DECIMAL(20, 8), -- Risk limit value
breach_amount DECIMAL(20, 8), -- Amount of breach
breach_percentage DECIMAL(5, 4), -- Percentage of breach
-- Context information
symbol VARCHAR(32),
account_id VARCHAR(64),
portfolio_id VARCHAR(64),
strategy_id VARCHAR(100),
instrument_id VARCHAR(64),
-- Order context (if related to an order)
order_id UUID,
trade_id UUID,
position_id UUID,
-- Risk calculation details
calculation_method VARCHAR(50), -- 'real_time', 'batch', 'stress_test'
model_version VARCHAR(20), -- Risk model version
parameters JSONB, -- Risk calculation parameters
-- Actions taken
action_taken VARCHAR(100), -- 'blocked', 'warning_issued', 'position_reduced', 'trading_halted'
auto_action BOOLEAN DEFAULT false, -- Whether action was automatic
manual_override BOOLEAN DEFAULT false, -- Whether manually overridden
override_reason TEXT, -- Reason for manual override
override_user VARCHAR(64), -- User who performed override
-- Timing
event_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
detection_timestamp TIMESTAMP WITH TIME ZONE, -- When risk was detected
resolution_timestamp TIMESTAMP WITH TIME ZONE, -- When risk was resolved
-- Resolution
resolution_status VARCHAR(20) DEFAULT 'open' CHECK (resolution_status IN ('open', 'acknowledged', 'resolved', 'false_positive')),
resolution_notes TEXT,
resolved_by VARCHAR(64),
-- System information
source_system VARCHAR(50) NOT NULL,
risk_engine_version VARCHAR(20),
-- Additional data
metadata JSONB,
PRIMARY KEY (id, event_timestamp)
) PARTITION BY RANGE (event_timestamp);
-- Create partitions for risk events
-- Only create monthly partitions if the table doesn't already have partitions (from migration 002)
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
existing_partitions INTEGER;
BEGIN
-- Check if partitions already exist (from migration 002 which creates daily partitions)
SELECT COUNT(*) INTO existing_partitions
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'risk_events';
-- Only create monthly partitions if no partitions exist yet
IF existing_partitions = 0 THEN
FOR i IN 0..5 LOOP
partition_start := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
partition_end := partition_start + INTERVAL '1 month';
partition_name := 'risk_events_' || to_char(partition_start, 'YYYY_MM');
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF risk_events
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
END LOOP;
END IF;
END $$;
-- =======================================================================================
-- AUDIT TRAIL TABLE - Configuration changes and administrative actions
-- =======================================================================================
CREATE TABLE IF NOT EXISTS audit_trail (
id UUID NOT NULL DEFAULT uuid_generate_v4(),
event_id UUID NOT NULL DEFAULT uuid_generate_v4(),
-- Event classification
event_type VARCHAR(50) NOT NULL, -- 'config_change', 'user_action', 'system_action', 'data_modification'
action VARCHAR(100) NOT NULL, -- Specific action taken
-- Actor information
user_id VARCHAR(64), -- User who performed action
username VARCHAR(100), -- Username
user_role VARCHAR(50), -- User role/permission level
session_id VARCHAR(128), -- Session identifier
ip_address INET, -- Source IP address
user_agent TEXT, -- Browser/client information
-- Target information
target_type VARCHAR(50), -- 'user', 'account', 'configuration', 'strategy', 'limit'
target_id VARCHAR(128), -- ID of the target object
target_name VARCHAR(255), -- Human-readable name of target
-- Change details
operation VARCHAR(20) CHECK (operation IN ('CREATE', 'READ', 'UPDATE', 'DELETE')),
field_name VARCHAR(100), -- Specific field changed
old_value TEXT, -- Previous value
new_value TEXT, -- New value
change_reason TEXT, -- Reason for change
-- Context
system_name VARCHAR(50), -- System where change occurred
component VARCHAR(50), -- System component
environment VARCHAR(20), -- 'production', 'staging', 'development'
-- Timing
event_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
effective_timestamp TIMESTAMP WITH TIME ZONE, -- When change becomes effective
-- Approval workflow
approval_required BOOLEAN DEFAULT false,
approval_status VARCHAR(20), -- 'pending', 'approved', 'rejected'
approved_by VARCHAR(64), -- User who approved
approval_timestamp TIMESTAMP WITH TIME ZONE,
approval_comments TEXT,
-- Compliance
regulatory_impact BOOLEAN DEFAULT false, -- Whether change has regulatory impact
compliance_review_required BOOLEAN DEFAULT false,
compliance_reviewer VARCHAR(64),
compliance_review_date DATE,
-- Additional data
metadata JSONB,
request_payload JSONB, -- Full request data
response_payload JSONB, -- Full response data
PRIMARY KEY (id, event_timestamp)
) PARTITION BY RANGE (event_timestamp);
-- Create partitions for audit trail
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
existing_partitions INTEGER;
BEGIN
-- Check if partitions already exist (could be from migration 003)
SELECT COUNT(*) INTO existing_partitions
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'audit_trail';
-- Only create monthly partitions if no partitions exist yet
IF existing_partitions = 0 THEN
FOR i IN 0..11 LOOP
partition_start := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
partition_end := partition_start + INTERVAL '1 month';
partition_name := 'audit_trail_' || to_char(partition_start, 'YYYY_MM');
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF audit_trail
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
END LOOP;
END IF;
END $$;
-- =======================================================================================
-- ML SIGNALS TABLE - Machine learning predictions and signals
-- =======================================================================================
CREATE TABLE IF NOT EXISTS ml_signals (
id UUID NOT NULL DEFAULT uuid_generate_v4(),
signal_id UUID NOT NULL DEFAULT uuid_generate_v4(),
-- Model information
model_name VARCHAR(100) NOT NULL, -- 'dqn', 'ppo', 'transformer', 'mamba', 'tft'
model_version VARCHAR(20) NOT NULL,
model_type VARCHAR(50), -- 'reinforcement_learning', 'supervised', 'unsupervised'
algorithm VARCHAR(50), -- Specific algorithm used
-- Signal details
signal_type VARCHAR(50) NOT NULL, -- 'trade_signal', 'market_prediction', 'risk_alert', 'anomaly_detection'
signal_strength DECIMAL(5, 4), -- Signal strength 0.0-1.0
confidence DECIMAL(5, 4), -- Model confidence 0.0-1.0
-- Prediction/signal content
prediction_type VARCHAR(50), -- 'price_direction', 'volatility', 'volume', 'risk_level'
predicted_value DECIMAL(20, 8), -- Numerical prediction
predicted_direction VARCHAR(10), -- 'up', 'down', 'neutral'
time_horizon INTEGER, -- Prediction time horizon in minutes
-- Market context
symbol VARCHAR(32) NOT NULL,
market_data_snapshot JSONB, -- Market data used for prediction
feature_vector JSONB, -- Input features used
-- Trading context
account_id VARCHAR(64),
strategy_id VARCHAR(100),
-- Model performance tracking
execution_time_ms INTEGER, -- Model execution time
input_data_hash VARCHAR(64), -- Hash of input data for reproducibility
model_parameters JSONB, -- Model parameters used
-- Outcome tracking (filled after signal verification)
actual_outcome DECIMAL(20, 8), -- Actual result (for backtesting)
outcome_timestamp TIMESTAMP WITH TIME ZONE, -- When outcome was recorded
accuracy_score DECIMAL(5, 4), -- How accurate was the prediction
signal_pnl DECIMAL(20, 8), -- P&L attributed to this signal
-- Timing
signal_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
market_timestamp TIMESTAMP WITH TIME ZONE, -- Market data timestamp
-- Signal lifecycle
signal_status VARCHAR(20) DEFAULT 'active', -- 'active', 'executed', 'expired', 'cancelled'
expiry_timestamp TIMESTAMP WITH TIME ZONE,
-- System information
source_system VARCHAR(50) NOT NULL,
gpu_used BOOLEAN DEFAULT false,
cuda_version VARCHAR(20),
-- Additional data
metadata JSONB,
raw_features JSONB, -- Raw feature data
PRIMARY KEY (id, signal_timestamp)
) PARTITION BY RANGE (signal_timestamp);
-- Create partitions for ML signals
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
existing_partitions INTEGER;
BEGIN
-- Check if partitions already exist
SELECT COUNT(*) INTO existing_partitions
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'ml_signals';
-- Only create monthly partitions if no partitions exist yet
IF existing_partitions = 0 THEN
FOR i IN 0..2 LOOP
partition_start := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
partition_end := partition_start + INTERVAL '1 month';
partition_name := 'ml_signals_' || to_char(partition_start, 'YYYY_MM');
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF ml_signals
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
END LOOP;
END IF;
END $$;
-- =======================================================================================
-- SYSTEM EVENTS TABLE - Health, performance, and error events
-- =======================================================================================
CREATE TABLE IF NOT EXISTS system_events (
id UUID NOT NULL DEFAULT uuid_generate_v4(),
event_id UUID NOT NULL DEFAULT uuid_generate_v4(),
-- Event classification
event_type VARCHAR(50) NOT NULL, -- 'health_check', 'performance_metric', 'error', 'startup', 'shutdown', 'deployment'
severity VARCHAR(20) NOT NULL CHECK (severity IN ('debug', 'info', 'warning', 'error', 'critical')),
category VARCHAR(50), -- 'connectivity', 'latency', 'memory', 'disk', 'network', 'database'
-- System information
system_name VARCHAR(50) NOT NULL,
service_name VARCHAR(50),
component_name VARCHAR(50),
hostname VARCHAR(100),
instance_id VARCHAR(100),
version VARCHAR(20),
build_id VARCHAR(50),
-- Performance metrics
latency_ns BIGINT, -- Latency in nanoseconds
throughput_per_second INTEGER, -- Operations per second
memory_usage_mb INTEGER, -- Memory usage in MB
cpu_usage_percent DECIMAL(5, 2), -- CPU usage percentage
disk_usage_percent DECIMAL(5, 2), -- Disk usage percentage
network_bytes_in BIGINT, -- Network bytes received
network_bytes_out BIGINT, -- Network bytes sent
-- Health information
health_status VARCHAR(20), -- 'healthy', 'degraded', 'unhealthy', 'critical'
health_score DECIMAL(5, 2), -- Health score 0-100
uptime_seconds BIGINT, -- System uptime
-- Error information
error_code VARCHAR(50), -- Error code
error_message TEXT, -- Error message
stack_trace TEXT, -- Error stack trace
error_count INTEGER DEFAULT 1, -- Number of occurrences
-- Market data quality
market_data_lag_ms INTEGER, -- Market data lag
missing_ticks INTEGER, -- Number of missing market data ticks
data_quality_score DECIMAL(5, 2), -- Data quality score 0-100
-- Database performance
db_connection_count INTEGER, -- Active database connections
db_query_time_ms INTEGER, -- Database query time
db_connection_pool_usage DECIMAL(5, 2), -- Connection pool usage percentage
-- Timing
event_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metric_timestamp TIMESTAMP WITH TIME ZONE, -- When metric was measured
-- Resolution tracking
incident_id VARCHAR(100), -- Related incident ID
resolution_time_minutes INTEGER, -- Time to resolve issue
resolution_notes TEXT,
-- Additional data
metadata JSONB,
metrics JSONB, -- Additional metrics
environment_data JSONB, -- Environment variables, config
PRIMARY KEY (id, event_timestamp)
) PARTITION BY RANGE (event_timestamp);
-- Create partitions for system events
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
existing_partitions INTEGER;
BEGIN
-- Check if partitions already exist
SELECT COUNT(*) INTO existing_partitions
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'system_events';
-- Only create monthly partitions if no partitions exist yet
IF existing_partitions = 0 THEN
FOR i IN 0..2 LOOP
partition_start := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
partition_end := partition_start + INTERVAL '1 month';
partition_name := 'system_events_' || to_char(partition_start, 'YYYY_MM');
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF system_events
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
END LOOP;
END IF;
END $$;
-- =======================================================================================
-- INDEXES FOR HIGH-PERFORMANCE QUERIES
-- =======================================================================================
-- Trading Events Indexes (only create if columns exist - these may be from migration 001 or this migration)
DO $$
BEGIN
-- Index for order_id if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'trading_events' AND column_name = 'order_id') THEN
CREATE INDEX IF NOT EXISTS idx_trading_events_order_id ON trading_events(order_id, event_timestamp);
END IF;
-- Index for compliance_flags if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'trading_events' AND column_name = 'compliance_flags') THEN
CREATE INDEX IF NOT EXISTS idx_trading_events_compliance ON trading_events USING GIN(compliance_flags) WHERE compliance_flags IS NOT NULL;
END IF;
-- Index for risk_violations if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'trading_events' AND column_name = 'risk_violations') THEN
CREATE INDEX IF NOT EXISTS idx_trading_events_risk ON trading_events USING GIN(risk_violations) WHERE risk_violations IS NOT NULL;
END IF;
-- Index for latency_ns if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'trading_events' AND column_name = 'latency_ns') THEN
CREATE INDEX IF NOT EXISTS idx_trading_events_latency ON trading_events(latency_ns) WHERE latency_ns IS NOT NULL;
END IF;
END $$;
-- These indexes should work with both migration 001 and 016 schemas
CREATE INDEX IF NOT EXISTS idx_trading_events_symbol_type ON trading_events(symbol, event_type, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_trading_events_account ON trading_events(account_id, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_trading_events_strategy ON trading_events(strategy_id, event_timestamp) WHERE strategy_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_trading_events_correlation ON trading_events(correlation_id) WHERE correlation_id IS NOT NULL;
-- Risk Events Indexes (conditional on column existence)
DO $$
BEGIN
-- risk_type may not exist in migration 001 schema
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'risk_events' AND column_name = 'risk_type') THEN
CREATE INDEX IF NOT EXISTS idx_risk_events_type_severity ON risk_events(risk_type, severity, event_timestamp);
ELSE
-- Use event_type instead if risk_type doesn't exist
CREATE INDEX IF NOT EXISTS idx_risk_events_type_severity ON risk_events(event_type, severity, event_timestamp);
END IF;
-- resolution_status if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'risk_events' AND column_name = 'resolution_status') THEN
CREATE INDEX IF NOT EXISTS idx_risk_events_resolution ON risk_events(resolution_status, event_timestamp);
END IF;
-- order_id if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'risk_events' AND column_name = 'order_id') THEN
CREATE INDEX IF NOT EXISTS idx_risk_events_order ON risk_events(order_id) WHERE order_id IS NOT NULL;
END IF;
-- breach_amount if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'risk_events' AND column_name = 'breach_amount') THEN
CREATE INDEX IF NOT EXISTS idx_risk_events_breach_amount ON risk_events(breach_amount) WHERE breach_amount IS NOT NULL;
END IF;
END $$;
-- These indexes should work with both schemas
CREATE INDEX IF NOT EXISTS idx_risk_events_account ON risk_events(account_id, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_risk_events_symbol ON risk_events(symbol, event_timestamp) WHERE symbol IS NOT NULL;
-- Audit Trail Indexes
CREATE INDEX IF NOT EXISTS idx_audit_trail_user ON audit_trail(user_id, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_trail_target ON audit_trail(target_type, target_id, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_trail_operation ON audit_trail(operation, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_trail_approval ON audit_trail(approval_status, event_timestamp) WHERE approval_required = true;
CREATE INDEX IF NOT EXISTS idx_audit_trail_compliance ON audit_trail(regulatory_impact, event_timestamp) WHERE regulatory_impact = true;
CREATE INDEX IF NOT EXISTS idx_audit_trail_ip ON audit_trail(ip_address, event_timestamp);
-- ML Signals Indexes
CREATE INDEX IF NOT EXISTS idx_ml_signals_model ON ml_signals(model_name, model_version, signal_timestamp);
CREATE INDEX IF NOT EXISTS idx_ml_signals_symbol ON ml_signals(symbol, signal_timestamp);
CREATE INDEX IF NOT EXISTS idx_ml_signals_strategy ON ml_signals(strategy_id, signal_timestamp) WHERE strategy_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_ml_signals_confidence ON ml_signals(confidence, signal_timestamp);
CREATE INDEX IF NOT EXISTS idx_ml_signals_status ON ml_signals(signal_status, expiry_timestamp);
CREATE INDEX IF NOT EXISTS idx_ml_signals_accuracy ON ml_signals(accuracy_score) WHERE accuracy_score IS NOT NULL;
-- System Events Indexes (conditional on column existence)
DO $$
BEGIN
-- system_name if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'system_events' AND column_name = 'system_name') THEN
CREATE INDEX IF NOT EXISTS idx_system_events_system ON system_events(system_name, service_name, event_timestamp);
END IF;
-- error_code if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'system_events' AND column_name = 'error_code') THEN
CREATE INDEX IF NOT EXISTS idx_system_events_error ON system_events(error_code, event_timestamp) WHERE error_code IS NOT NULL;
END IF;
-- latency_ns if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'system_events' AND column_name = 'latency_ns') THEN
CREATE INDEX IF NOT EXISTS idx_system_events_latency ON system_events(latency_ns) WHERE latency_ns IS NOT NULL;
END IF;
-- incident_id if it exists
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'system_events' AND column_name = 'incident_id') THEN
CREATE INDEX IF NOT EXISTS idx_system_events_incident ON system_events(incident_id) WHERE incident_id IS NOT NULL;
END IF;
END $$;
-- These indexes should work with both schemas
CREATE INDEX IF NOT EXISTS idx_system_events_severity ON system_events(severity, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_system_events_health ON system_events(health_status, event_timestamp) WHERE health_status IS NOT NULL;
-- =======================================================================================
-- DATA RETENTION POLICIES
-- =======================================================================================
-- Create function to manage partition retention
CREATE OR REPLACE FUNCTION manage_event_partitions()
RETURNS VOID AS $$
DECLARE
table_names TEXT[] := ARRAY['trading_events', 'risk_events', 'audit_trail', 'ml_signals', 'system_events'];
retention_months INTEGER[] := ARRAY[24, 12, 84, 6, 3]; -- Retention periods in months
table_name TEXT;
retention_period INTEGER;
cutoff_date DATE;
partition_name TEXT;
partition_record RECORD;
i INTEGER;
BEGIN
FOR i IN 1..array_length(table_names, 1) LOOP
table_name := table_names[i];
retention_period := retention_months[i];
cutoff_date := CURRENT_DATE - (retention_period || ' months')::INTERVAL;
-- Drop old partitions
FOR partition_record IN
SELECT schemaname, tablename
FROM pg_tables
WHERE tablename LIKE table_name || '_%'
AND tablename ~ '\d{4}_\d{2}$'
LOOP
-- Extract date from partition name
partition_name := partition_record.tablename;
-- Drop if older than retention period
EXECUTE format('SELECT to_date(substring(%L from ''%s_(\d{4}_\d{2})$''), ''YYYY_MM'') < %L',
partition_name, table_name, cutoff_date)
INTO partition_record;
IF partition_record IS NOT NULL THEN
EXECUTE format('DROP TABLE IF EXISTS %I CASCADE', partition_name);
RAISE NOTICE 'Dropped old partition: %', partition_name;
END IF;
END LOOP;
-- Create future partitions (next 3 months)
FOR i IN 1..3 LOOP
DECLARE
partition_start DATE := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
partition_end DATE := partition_start + INTERVAL '1 month';
new_partition_name TEXT := table_name || '_' || to_char(partition_start, 'YYYY_MM');
BEGIN
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF %I
FOR VALUES FROM (%L) TO (%L)',
new_partition_name, table_name, partition_start, partition_end);
END;
END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- =======================================================================================
-- UTILITY FUNCTIONS
-- =======================================================================================
-- Function to get event statistics
CREATE OR REPLACE FUNCTION get_event_statistics(
start_date DATE DEFAULT CURRENT_DATE - INTERVAL '7 days',
end_date DATE DEFAULT CURRENT_DATE
)
RETURNS TABLE(
table_name TEXT,
event_count BIGINT,
avg_events_per_hour NUMERIC,
max_events_per_hour BIGINT,
data_size_mb NUMERIC
) AS $$
BEGIN
RETURN QUERY
WITH event_stats AS (
SELECT 'trading_events'::TEXT as tbl,
COUNT(*) as cnt,
EXTRACT(EPOCH FROM (end_date - start_date)) / 3600 as hours
FROM trading_events
WHERE event_timestamp BETWEEN start_date AND end_date
UNION ALL
SELECT 'risk_events'::TEXT,
COUNT(*),
EXTRACT(EPOCH FROM (end_date - start_date)) / 3600
FROM risk_events
WHERE event_timestamp BETWEEN start_date AND end_date
UNION ALL
SELECT 'audit_trail'::TEXT,
COUNT(*),
EXTRACT(EPOCH FROM (end_date - start_date)) / 3600
FROM audit_trail
WHERE event_timestamp BETWEEN start_date AND end_date
UNION ALL
SELECT 'ml_signals'::TEXT,
COUNT(*),
EXTRACT(EPOCH FROM (end_date - start_date)) / 3600
FROM ml_signals
WHERE signal_timestamp BETWEEN start_date AND end_date
UNION ALL
SELECT 'system_events'::TEXT,
COUNT(*),
EXTRACT(EPOCH FROM (end_date - start_date)) / 3600
FROM system_events
WHERE event_timestamp BETWEEN start_date AND end_date
)
SELECT
s.tbl,
s.cnt,
CASE WHEN s.hours > 0 THEN s.cnt / s.hours ELSE 0 END,
0::BIGINT, -- Placeholder for max events per hour
0::NUMERIC -- Placeholder for data size
FROM event_stats s;
END;
$$ LANGUAGE plpgsql;
-- Function to create event with correlation
CREATE OR REPLACE FUNCTION create_correlated_event(
p_table_name TEXT,
p_correlation_id UUID,
p_event_data JSONB
)
RETURNS UUID AS $$
DECLARE
new_event_id UUID := uuid_generate_v4();
BEGIN
-- This is a template function - specific implementations would be created
-- for each event type with proper validation and insertion logic
RETURN new_event_id;
END;
$$ LANGUAGE plpgsql;
-- =======================================================================================
-- TRIGGERS FOR DATA INTEGRITY
-- =======================================================================================
-- Function to validate trading event data
CREATE OR REPLACE FUNCTION validate_trading_event()
RETURNS TRIGGER AS $$
BEGIN
-- Validate required fields based on event type
IF NEW.event_type = 'order_filled' AND NEW.execution_price IS NULL THEN
RAISE EXCEPTION 'Fill events must have execution price';
END IF;
IF NEW.event_type = 'order_filled' AND NEW.execution_quantity IS NULL THEN
RAISE EXCEPTION 'Fill events must have execution quantity';
END IF;
-- Calculate latency if timestamps are available
IF NEW.processing_start_timestamp IS NOT NULL AND NEW.processing_end_timestamp IS NOT NULL THEN
NEW.latency_ns := EXTRACT(EPOCH FROM (NEW.processing_end_timestamp - NEW.processing_start_timestamp)) * 1000000000;
END IF;
-- Set remaining quantity
IF NEW.quantity IS NOT NULL AND NEW.filled_quantity IS NOT NULL THEN
NEW.remaining_quantity := NEW.quantity - NEW.filled_quantity;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply validation trigger to trading events
CREATE TRIGGER trigger_validate_trading_events
BEFORE INSERT OR UPDATE ON trading_events
FOR EACH ROW
EXECUTE FUNCTION validate_trading_event();
-- =======================================================================================
-- MATERIALIZED VIEWS FOR REPORTING
-- =======================================================================================
-- Real-time trading activity summary
-- Note: ns_timestamp is BIGINT (nanoseconds), convert to timestamptz via to_timestamp
-- Uses actual trading_event_type enum values from migration 001
CREATE MATERIALIZED VIEW IF NOT EXISTS trading_activity_summary AS
SELECT
date_trunc('hour', to_timestamp(event_timestamp::bigint / 1000000000.0)) as hour,
symbol,
COUNT(*) as total_events,
COUNT(*) FILTER (WHERE event_type = 'order_submitted') as orders_submitted,
COUNT(*) FILTER (WHERE event_type = 'order_filled') as orders_filled,
COUNT(*) FILTER (WHERE event_type = 'order_cancelled') as orders_cancelled,
SUM((event_data->>'quantity')::numeric) FILTER (WHERE event_type = 'order_submitted') as total_quantity,
SUM((event_data->>'quantity')::numeric) FILTER (WHERE event_type = 'order_filled') as filled_quantity,
AVG((event_data->>'latency_ns')::bigint) FILTER (WHERE event_data->>'latency_ns' IS NOT NULL) as avg_latency_ns,
MAX((event_data->>'latency_ns')::bigint) FILTER (WHERE event_data->>'latency_ns' IS NOT NULL) as max_latency_ns
FROM trading_events
WHERE to_timestamp(event_timestamp::bigint / 1000000000.0) >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY date_trunc('hour', to_timestamp(event_timestamp::bigint / 1000000000.0)), symbol;
-- Risk events summary
-- Note: ns_timestamp is BIGINT (nanoseconds), convert to timestamptz via to_timestamp
CREATE MATERIALIZED VIEW IF NOT EXISTS risk_events_summary AS
SELECT
date_trunc('day', to_timestamp(event_timestamp::bigint / 1000000000.0)) as day,
event_type as risk_type, -- migration 001 schema uses event_type not risk_type
severity,
COUNT(*) as event_count,
COUNT(*) FILTER (WHERE resolved_timestamp IS NOT NULL) as resolved_count,
AVG(EXTRACT(EPOCH FROM (
to_timestamp(resolved_timestamp::bigint / 1000000000.0) -
to_timestamp(event_timestamp::bigint / 1000000000.0)
))/60) FILTER (WHERE resolved_timestamp IS NOT NULL) as avg_resolution_minutes
FROM risk_events
WHERE to_timestamp(event_timestamp::bigint / 1000000000.0) >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY date_trunc('day', to_timestamp(event_timestamp::bigint / 1000000000.0)), event_type, severity;
-- Create unique indexes on materialized views
CREATE UNIQUE INDEX IF NOT EXISTS idx_trading_activity_summary_hour_symbol
ON trading_activity_summary(hour, symbol);
CREATE UNIQUE INDEX IF NOT EXISTS idx_risk_events_summary_day_type_severity
ON risk_events_summary(day, risk_type, severity);
-- =======================================================================================
-- COMMENTS FOR DOCUMENTATION
-- =======================================================================================
COMMENT ON TABLE trading_events IS 'Comprehensive audit trail of all trading lifecycle events with nanosecond precision for HFT compliance';
COMMENT ON TABLE risk_events IS 'Risk management events, violations, and alerts with automated resolution tracking';
COMMENT ON TABLE audit_trail IS 'Complete audit trail of system changes and administrative actions for regulatory compliance';
COMMENT ON TABLE ml_signals IS 'Machine learning model predictions and signals with performance tracking';
COMMENT ON TABLE system_events IS 'System health, performance metrics, and operational events';
-- Column comments (only add if columns exist)
DO $$
BEGIN
-- trading_events comments
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'trading_events' AND column_name = 'latency_ns') THEN
COMMENT ON COLUMN trading_events.latency_ns IS 'Processing latency in nanoseconds for HFT performance monitoring';
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'trading_events' AND column_name = 'correlation_id') THEN
COMMENT ON COLUMN trading_events.correlation_id IS 'Links related events together for complete transaction tracking';
END IF;
-- risk_events comments
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'risk_events' AND column_name = 'breach_percentage') THEN
COMMENT ON COLUMN risk_events.breach_percentage IS 'Percentage by which limit was breached (>1.0 = breach)';
END IF;
-- audit_trail comments
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'audit_trail' AND column_name = 'regulatory_impact') THEN
COMMENT ON COLUMN audit_trail.regulatory_impact IS 'Flags changes that have regulatory compliance implications';
END IF;
-- ml_signals comments
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'ml_signals' AND column_name = 'confidence') THEN
COMMENT ON COLUMN ml_signals.confidence IS 'Model confidence in prediction (0.0-1.0)';
END IF;
-- system_events comments
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'system_events' AND column_name = 'latency_ns') THEN
COMMENT ON COLUMN system_events.latency_ns IS 'System operation latency in nanoseconds';
END IF;
END $$;
-- Grant appropriate permissions
-- GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA public TO foxhunt_trading_service;
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO foxhunt_trading_service;
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO foxhunt_trading_service;
-- Schedule partition management (requires pg_cron extension)
-- SELECT cron.schedule('manage-event-partitions', '0 2 * * 0', 'SELECT manage_event_partitions();');