🔧 Wave 112: Migration consolidation and cleanup

- Removed 9 deprecated migration files (001-006 up/down, auth_schema, trading_service_events)
- Updated 12 core migrations with improved constraints and indexing
- Consolidated schema from 22 migrations to 18 clean migrations
- All migrations tested and applied successfully (Agent 32 validation)
- Zero migration errors in production database
This commit is contained in:
jgrusewski
2025-10-05 19:42:08 +02:00
parent 5993cdc385
commit 36c9c7cfe3
21 changed files with 360 additions and 3488 deletions

View File

@@ -59,7 +59,7 @@ CREATE TYPE time_in_force AS ENUM ('day', 'gtc', 'ioc', 'fok', 'gtd');
-- ================================================================================================
CREATE TABLE trading_events (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
event_id BIGSERIAL NOT NULL, -- Sequential event ID for ordering
correlation_id UUID NOT NULL, -- Links related events
@@ -100,18 +100,36 @@ CREATE TABLE trading_events (
parent_hash VARCHAR(64), -- Hash of previous related event
-- Partition key for performance
event_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(event_timestamp / 1000000000.0))) STORED,
event_date DATE NOT NULL,
-- Constraints
CONSTRAINT chk_timestamps CHECK (
received_timestamp >= event_timestamp AND
processing_timestamp >= received_timestamp
)
),
-- Composite primary key including partition column
PRIMARY KEY (id, event_date)
) PARTITION BY RANGE (event_date);
-- Create table comment
COMMENT ON TABLE trading_events IS 'Immutable event store for all trading activities with nanosecond precision and compliance features';
-- Trigger function to set event_date from event_timestamp (nanoseconds)
CREATE OR REPLACE FUNCTION set_trading_event_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.event_date := DATE(TO_TIMESTAMP(NEW.event_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Trigger to auto-populate event_date on insert
CREATE TRIGGER tg_set_trading_event_date
BEFORE INSERT ON trading_events
FOR EACH ROW
EXECUTE FUNCTION set_trading_event_date();
-- ================================================================================================
-- ORDERS TABLE (CURRENT STATE)
-- Mutable state table for current order status (derived from events)
@@ -132,7 +150,7 @@ CREATE TABLE orders (
-- Quantities (in base units, scaled for precision)
quantity BIGINT NOT NULL CHECK (quantity > 0),
filled_quantity BIGINT NOT NULL DEFAULT 0 CHECK (filled_quantity >= 0),
remaining_quantity BIGINT GENERATED ALWAYS AS (quantity - filled_quantity) STORED,
remaining_quantity BIGINT NOT NULL DEFAULT 0,
-- Pricing (in cents or smallest currency unit)
limit_price BIGINT, -- NULL for market orders
@@ -175,6 +193,21 @@ CREATE TABLE orders (
)
);
-- Trigger function to calculate remaining_quantity
CREATE OR REPLACE FUNCTION set_order_remaining_quantity()
RETURNS TRIGGER AS $$
BEGIN
NEW.remaining_quantity := NEW.quantity - NEW.filled_quantity;
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Trigger to auto-populate remaining_quantity on insert/update
CREATE TRIGGER tg_set_order_remaining_quantity
BEFORE INSERT OR UPDATE OF quantity, filled_quantity ON orders
FOR EACH ROW
EXECUTE FUNCTION set_order_remaining_quantity();
-- ================================================================================================
-- FILLS TABLE (TRADE EXECUTIONS)
-- Immutable record of trade executions
@@ -247,7 +280,7 @@ CREATE TABLE positions (
-- Market data
last_price BIGINT NOT NULL DEFAULT 0, -- Last known market price
market_value BIGINT GENERATED ALWAYS AS (ABS(quantity) * last_price) STORED,
market_value BIGINT NOT NULL DEFAULT 0,
-- Risk metrics
var_1d BIGINT, -- 1-day Value at Risk
@@ -261,7 +294,7 @@ CREATE TABLE positions (
-- Position limits
max_position BIGINT, -- Maximum allowed position size
current_exposure BIGINT GENERATED ALWAYS AS (ABS(quantity * last_price)) STORED,
current_exposure BIGINT NOT NULL DEFAULT 0,
-- Audit
version INTEGER NOT NULL DEFAULT 1, -- Optimistic locking version
@@ -275,6 +308,22 @@ CREATE TABLE positions (
)
);
-- Trigger function to calculate market_value and current_exposure
CREATE OR REPLACE FUNCTION set_position_calculated_fields()
RETURNS TRIGGER AS $$
BEGIN
NEW.market_value := ABS(NEW.quantity) * NEW.last_price;
NEW.current_exposure := ABS(NEW.quantity * NEW.last_price);
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Trigger to auto-populate calculated fields on insert/update
CREATE TRIGGER tg_set_position_calculated_fields
BEFORE INSERT OR UPDATE OF quantity, last_price ON positions
FOR EACH ROW
EXECUTE FUNCTION set_position_calculated_fields();
-- ================================================================================================
-- HIGH-PERFORMANCE INDEXES
-- Optimized for HFT query patterns and real-time operations

View File

@@ -1,272 +0,0 @@
-- Migration 001: Create core trading tables for HFT system
-- This migration establishes the foundational tables for orders, fills, positions, and market data
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
-- Orders table - core trading orders with optimized indexing
CREATE TABLE IF NOT EXISTS orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
symbol VARCHAR(32) NOT NULL,
side VARCHAR(10) NOT NULL CHECK (side IN ('buy', 'sell')),
order_type VARCHAR(20) NOT NULL CHECK (order_type IN ('market', 'limit', 'stop', 'stop_limit')),
quantity BIGINT NOT NULL CHECK (quantity > 0),
price BIGINT, -- Fixed-point price in cents, nullable for market orders
filled_quantity BIGINT NOT NULL DEFAULT 0 CHECK (filled_quantity >= 0),
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'partial', 'filled', 'cancelled', 'expired')),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE,
client_order_id VARCHAR(128), -- Client-provided identifier
account_id VARCHAR(64), -- Account identifier
metadata JSONB -- Additional order metadata
);
-- Fills table - trade executions with foreign key to orders
CREATE TABLE IF NOT EXISTS fills (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
symbol VARCHAR(32) NOT NULL,
side VARCHAR(10) NOT NULL CHECK (side IN ('buy', 'sell')),
quantity BIGINT NOT NULL CHECK (quantity > 0),
price BIGINT NOT NULL CHECK (price > 0), -- Execution price in fixed-point cents
fee BIGINT, -- Trading fee in fixed-point cents
fee_currency VARCHAR(10), -- Fee currency
execution_time TIMESTAMP WITH TIME ZONE NOT NULL,
venue VARCHAR(64), -- Execution venue
execution_id VARCHAR(128), -- Venue-specific execution ID
is_maker BOOLEAN, -- Maker/taker classification
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB -- Additional fill metadata
);
-- Positions table - current holdings by symbol and account
CREATE TABLE IF NOT EXISTS positions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
symbol VARCHAR(32) NOT NULL,
account_id VARCHAR(64), -- Account identifier
quantity BIGINT NOT NULL DEFAULT 0, -- Signed quantity (positive = long, negative = short)
avg_price BIGINT NOT NULL DEFAULT 0, -- Average entry price in fixed-point cents
market_value BIGINT NOT NULL DEFAULT 0, -- Current market value in fixed-point cents
unrealized_pnl BIGINT NOT NULL DEFAULT 0, -- Unrealized P&L in fixed-point cents
realized_pnl BIGINT NOT NULL DEFAULT 0, -- Realized P&L in fixed-point cents
total_cost BIGINT NOT NULL DEFAULT 0, -- Total cost basis in fixed-point cents
last_price BIGINT NOT NULL DEFAULT 0, -- Last known market price
trade_count INTEGER NOT NULL DEFAULT 0, -- Number of trades that created this position
first_trade_time TIMESTAMP WITH TIME ZONE, -- Time of first trade
last_updated TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB, -- Additional position metadata
-- Ensure unique position per symbol-account combination
UNIQUE(symbol, account_id)
);
-- Market data table - high-frequency tick data with partitioning support
CREATE TABLE IF NOT EXISTS market_data (
id UUID NOT NULL DEFAULT uuid_generate_v4(),
symbol VARCHAR(32) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL, -- Market timestamp
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), -- When we received the data
bid BIGINT, -- Best bid price in fixed-point cents
ask BIGINT, -- Best ask price in fixed-point cents
last BIGINT, -- Last trade price in fixed-point cents
volume BIGINT, -- Volume
bid_size BIGINT, -- Best bid size
ask_size BIGINT, -- Best ask size
trade_count INTEGER, -- Number of trades
vwap BIGINT, -- Volume weighted average price
open BIGINT, -- Opening price
high BIGINT, -- High price
low BIGINT, -- Low price
close BIGINT, -- Closing price
data_type VARCHAR(20) NOT NULL DEFAULT 'tick' CHECK (data_type IN ('tick', 'quote', 'trade', 'bar')),
source VARCHAR(64) NOT NULL, -- Data provider
metadata JSONB, -- Additional market data
PRIMARY KEY (id, timestamp) -- Composite primary key for partitioning
) PARTITION BY RANGE (timestamp);
-- Create initial partition for market data (current month)
DO $$
DECLARE
partition_start DATE := date_trunc('month', CURRENT_DATE);
partition_end DATE := partition_start + INTERVAL '1 month';
partition_name TEXT := 'market_data_' || to_char(partition_start, 'YYYY_MM');
BEGIN
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF market_data
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
END $$;
-- Bars table - aggregated OHLCV data
CREATE TABLE IF NOT EXISTS bars (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
symbol VARCHAR(32) NOT NULL,
timeframe VARCHAR(10) NOT NULL, -- "1m", "5m", "1h", "1d", etc.
timestamp TIMESTAMP WITH TIME ZONE NOT NULL, -- Bar start time
open BIGINT NOT NULL, -- Opening price in fixed-point cents
high BIGINT NOT NULL, -- High price in fixed-point cents
low BIGINT NOT NULL, -- Low price in fixed-point cents
close BIGINT NOT NULL, -- Closing price in fixed-point cents
volume BIGINT NOT NULL DEFAULT 0, -- Volume
trade_count INTEGER NOT NULL DEFAULT 0, -- Number of trades
vwap BIGINT, -- Volume weighted average price
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Ensure unique bar per symbol-timeframe-timestamp combination
UNIQUE(symbol, timeframe, timestamp)
);
-- Create high-performance indexes for HFT queries
-- Orders table indexes (optimized for order management)
CREATE INDEX IF NOT EXISTS idx_orders_symbol_status ON orders(symbol, status);
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at);
CREATE INDEX IF NOT EXISTS idx_orders_account_id ON orders(account_id) WHERE account_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_orders_client_order_id ON orders(client_order_id) WHERE client_order_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_orders_symbol_status_created ON orders(symbol, status, created_at);
-- Fills table indexes (optimized for execution tracking)
CREATE INDEX IF NOT EXISTS idx_fills_order_id ON fills(order_id);
CREATE INDEX IF NOT EXISTS idx_fills_symbol_execution_time ON fills(symbol, execution_time);
CREATE INDEX IF NOT EXISTS idx_fills_execution_time ON fills(execution_time);
CREATE INDEX IF NOT EXISTS idx_fills_venue ON fills(venue) WHERE venue IS NOT NULL;
-- Positions table indexes (optimized for position tracking)
CREATE INDEX IF NOT EXISTS idx_positions_symbol ON positions(symbol);
CREATE INDEX IF NOT EXISTS idx_positions_account_id ON positions(account_id) WHERE account_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_positions_last_updated ON positions(last_updated);
-- Market data table indexes (optimized for time-series queries)
CREATE INDEX IF NOT EXISTS idx_market_data_symbol_timestamp ON market_data(symbol, timestamp);
CREATE INDEX IF NOT EXISTS idx_market_data_timestamp ON market_data(timestamp);
CREATE INDEX IF NOT EXISTS idx_market_data_source ON market_data(source);
CREATE INDEX IF NOT EXISTS idx_market_data_received_at ON market_data(received_at);
-- Bars table indexes (optimized for chart data queries)
CREATE INDEX IF NOT EXISTS idx_bars_symbol_timeframe_timestamp ON bars(symbol, timeframe, timestamp);
CREATE INDEX IF NOT EXISTS idx_bars_timestamp ON bars(timestamp);
-- Create functions for automatic timestamp updates
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers for automatic timestamp updates
CREATE TRIGGER trigger_orders_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_positions_updated_at
BEFORE UPDATE ON positions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Create function to automatically create market data partitions
CREATE OR REPLACE FUNCTION create_market_data_partition_if_not_exists(target_date DATE)
RETURNS VOID AS $$
DECLARE
partition_start DATE := date_trunc('month', target_date);
partition_end DATE := partition_start + INTERVAL '1 month';
partition_name TEXT := 'market_data_' || to_char(partition_start, 'YYYY_MM');
BEGIN
-- Check if partition exists
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = partition_name
) THEN
EXECUTE format('CREATE TABLE %I PARTITION OF market_data
FOR VALUES FROM (%L) TO (%L)',
partition_name, partition_start, partition_end);
-- Add indexes to the new partition
EXECUTE format('CREATE INDEX %I ON %I(symbol, timestamp)',
'idx_' || partition_name || '_symbol_timestamp', partition_name);
EXECUTE format('CREATE INDEX %I ON %I(timestamp)',
'idx_' || partition_name || '_timestamp', partition_name);
END IF;
END;
$$ LANGUAGE plpgsql;
-- Create function to validate order constraints
CREATE OR REPLACE FUNCTION validate_order_constraints()
RETURNS TRIGGER AS $$
BEGIN
-- Validate that limit orders have a price
IF NEW.order_type = 'limit' AND NEW.price IS NULL THEN
RAISE EXCEPTION 'Limit orders must have a price';
END IF;
-- Validate that filled quantity doesn't exceed order quantity
IF NEW.filled_quantity > NEW.quantity THEN
RAISE EXCEPTION 'Filled quantity cannot exceed order quantity';
END IF;
-- Update status based on filled quantity
IF NEW.filled_quantity = 0 THEN
NEW.status = 'pending';
ELSIF NEW.filled_quantity = NEW.quantity THEN
NEW.status = 'filled';
ELSIF NEW.filled_quantity < NEW.quantity THEN
NEW.status = 'partial';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger for order validation
CREATE TRIGGER trigger_validate_orders
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION validate_order_constraints();
-- Create materialized view for fast position summaries
CREATE MATERIALIZED VIEW IF NOT EXISTS position_summaries AS
SELECT
symbol,
account_id,
SUM(quantity) as total_quantity,
COUNT(*) as position_count,
SUM(unrealized_pnl) as total_unrealized_pnl,
SUM(realized_pnl) as total_realized_pnl,
AVG(avg_price) as weighted_avg_price,
MAX(last_updated) as last_updated
FROM positions
WHERE quantity != 0
GROUP BY symbol, account_id;
-- Create unique index on the materialized view
CREATE UNIQUE INDEX IF NOT EXISTS idx_position_summaries_symbol_account
ON position_summaries(symbol, account_id);
-- Create function to refresh position summaries
CREATE OR REPLACE FUNCTION refresh_position_summaries()
RETURNS VOID AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY position_summaries;
END;
$$ LANGUAGE plpgsql;
-- Add comments for documentation
COMMENT ON TABLE orders IS 'Core trading orders with ACID compliance';
COMMENT ON TABLE fills IS 'Trade executions linked to orders';
COMMENT ON TABLE positions IS 'Current holdings by symbol and account';
COMMENT ON TABLE market_data IS 'High-frequency tick data with automatic partitioning';
COMMENT ON TABLE bars IS 'Aggregated OHLCV bars for charting';
COMMENT ON COLUMN orders.price IS 'Price in fixed-point cents (divide by 100 for dollars)';
COMMENT ON COLUMN orders.quantity IS 'Order quantity in shares/units';
COMMENT ON COLUMN fills.price IS 'Execution price in fixed-point cents';
COMMENT ON COLUMN positions.quantity IS 'Signed quantity: positive=long, negative=short';
-- Grant appropriate permissions (adjust as needed for your setup)
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO foxhunt_app;
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO foxhunt_app;

View File

@@ -78,7 +78,7 @@ CREATE TYPE risk_metric_type AS ENUM (
-- ================================================================================================
CREATE TABLE risk_events (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
event_id BIGSERIAL NOT NULL,
correlation_id UUID NOT NULL,
@@ -132,9 +132,10 @@ CREATE TABLE risk_events (
metadata JSONB,
-- Partition key
event_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(event_timestamp / 1000000000.0))) STORED,
event_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, event_date),
CONSTRAINT chk_risk_timestamps CHECK (
detected_timestamp >= event_timestamp AND
(acknowledged_timestamp IS NULL OR acknowledged_timestamp >= detected_timestamp) AND
@@ -151,7 +152,7 @@ CREATE TABLE risk_events (
-- ================================================================================================
CREATE TABLE risk_metrics (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
metric_name risk_metric_type NOT NULL,
-- Scope identifiers
@@ -193,9 +194,10 @@ CREATE TABLE risk_metrics (
calculation_details JSONB, -- Model parameters and inputs
-- Partition key
metric_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(calculation_timestamp / 1000000000.0))) STORED,
metric_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, metric_date),
CONSTRAINT chk_confidence_level CHECK (confidence_level > 0 AND confidence_level <= 1),
CONSTRAINT chk_time_horizons CHECK (
time_horizon_days IS NULL OR time_horizon_days > 0
@@ -257,8 +259,7 @@ CREATE TABLE risk_limits (
limit_details JSONB, -- Additional limit parameters
override_permissions TEXT[], -- Who can override this limit
-- Constraints
CONSTRAINT uk_risk_limits_unique UNIQUE (limit_type, scope_level, COALESCE(account_id, ''), COALESCE(strategy_id, ''), COALESCE(symbol, '')),
-- Constraints (unique constraint moved to expression index below)
CONSTRAINT chk_threshold_order CHECK (
warning_threshold IS NULL OR breach_threshold IS NULL OR warning_threshold <= breach_threshold
),
@@ -270,6 +271,15 @@ CREATE TABLE risk_limits (
)
);
-- Expression index for risk_limits uniqueness
CREATE UNIQUE INDEX uk_risk_limits_unique ON risk_limits (
limit_type,
scope_level,
COALESCE(account_id, ''),
COALESCE(strategy_id, ''),
COALESCE(symbol, '')
);
-- ================================================================================================
-- STRESS TEST SCENARIOS TABLE
-- Predefined stress test scenarios and results
@@ -308,7 +318,7 @@ CREATE TABLE stress_test_scenarios (
-- ================================================================================================
CREATE TABLE stress_test_results (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
scenario_id UUID NOT NULL REFERENCES stress_test_scenarios(id),
execution_id UUID NOT NULL, -- Groups results from same execution
@@ -344,7 +354,10 @@ CREATE TABLE stress_test_results (
calculation_time_ms INTEGER, -- How long the calculation took
-- Partition key
execution_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(execution_timestamp / 1000000000.0))) STORED
execution_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, execution_date)
) PARTITION BY RANGE (execution_date);
-- ================================================================================================
@@ -449,6 +462,52 @@ CREATE INDEX idx_stress_test_results_execution ON stress_test_results USING BTRE
CREATE INDEX idx_stress_test_results_scenario ON stress_test_results USING BTREE (scenario_id, execution_timestamp);
CREATE INDEX idx_stress_test_results_account ON stress_test_results USING BTREE (account_id, execution_timestamp) WHERE account_id IS NOT NULL;
-- ================================================================================================
-- TRIGGER FUNCTIONS FOR GENERATED COLUMNS (converted from GENERATED ALWAYS)
-- ================================================================================================
-- Set event_date for risk_events
CREATE OR REPLACE FUNCTION set_risk_event_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.event_date := DATE(TO_TIMESTAMP(NEW.event_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_risk_event_date
BEFORE INSERT OR UPDATE ON risk_events
FOR EACH ROW
EXECUTE FUNCTION set_risk_event_date();
-- Set metric_date for risk_metrics
CREATE OR REPLACE FUNCTION set_risk_metric_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.metric_date := DATE(TO_TIMESTAMP(NEW.calculation_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_risk_metric_date
BEFORE INSERT OR UPDATE ON risk_metrics
FOR EACH ROW
EXECUTE FUNCTION set_risk_metric_date();
-- Set execution_date for stress_test_results
CREATE OR REPLACE FUNCTION set_stress_test_execution_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.execution_date := DATE(TO_TIMESTAMP(NEW.execution_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_stress_test_execution_date
BEFORE INSERT OR UPDATE ON stress_test_results
FOR EACH ROW
EXECUTE FUNCTION set_stress_test_execution_date();
-- ================================================================================================
-- AUTOMATIC PARTITIONING
-- ================================================================================================
@@ -523,12 +582,30 @@ BEGIN
-- Create risk_metrics partitions for current and next 2 months
FOR i IN 0..2 LOOP
PERFORM create_risk_metrics_partition(CURRENT_DATE + (i || ' months')::INTERVAL);
PERFORM create_risk_metrics_partition((CURRENT_DATE + (i || ' months')::INTERVAL)::DATE);
END LOOP;
-- Create stress_test_results partitions
-- Create stress_test_results partitions (daily, same as risk_events)
FOR i IN 0..7 LOOP
PERFORM create_trading_events_partition(CURRENT_DATE + i); -- Reuse function with same logic
-- Create stress_test_results partitions using similar logic
DECLARE
partition_name TEXT;
start_date DATE;
end_date DATE;
BEGIN
start_date := CURRENT_DATE + i;
end_date := start_date + INTERVAL '1 day';
partition_name := 'stress_test_results_' || to_char(start_date, 'YYYY_MM_DD');
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = partition_name
) THEN
EXECUTE format('CREATE TABLE %I PARTITION OF stress_test_results
FOR VALUES FROM (%L) TO (%L)',
partition_name, start_date, end_date);
END IF;
END;
END LOOP;
END $$;
@@ -604,31 +681,31 @@ BEGIN
IF NEW.value > COALESCE(applicable_limit.emergency_threshold, applicable_limit.breach_threshold) THEN
breach_detected := TRUE;
severity_level := 'emergency';
event_type_val := CASE NEW.metric_name
WHEN 'var_1d', 'var_10d' THEN 'var_breach'
WHEN 'exposure_gross', 'exposure_net' THEN 'exposure_limit_breach'
WHEN 'leverage_ratio' THEN 'leverage_excess'
WHEN 'concentration_single', 'concentration_sector' THEN 'concentration_risk'
event_type_val := CASE
WHEN NEW.metric_name IN ('var_1d', 'var_10d') THEN 'var_breach'
WHEN NEW.metric_name IN ('exposure_gross', 'exposure_net') THEN 'exposure_limit_breach'
WHEN NEW.metric_name = 'leverage_ratio' THEN 'leverage_excess'
WHEN NEW.metric_name IN ('concentration_single', 'concentration_sector') THEN 'concentration_risk'
ELSE 'stress_test_failure'
END;
ELSIF NEW.value > applicable_limit.breach_threshold THEN
breach_detected := TRUE;
severity_level := 'critical';
event_type_val := CASE NEW.metric_name
WHEN 'var_1d', 'var_10d' THEN 'var_breach'
WHEN 'exposure_gross', 'exposure_net' THEN 'exposure_limit_breach'
WHEN 'leverage_ratio' THEN 'leverage_excess'
WHEN 'concentration_single', 'concentration_sector' THEN 'concentration_risk'
event_type_val := CASE
WHEN NEW.metric_name IN ('var_1d', 'var_10d') THEN 'var_breach'
WHEN NEW.metric_name IN ('exposure_gross', 'exposure_net') THEN 'exposure_limit_breach'
WHEN NEW.metric_name = 'leverage_ratio' THEN 'leverage_excess'
WHEN NEW.metric_name IN ('concentration_single', 'concentration_sector') THEN 'concentration_risk'
ELSE 'stress_test_failure'
END;
ELSIF NEW.value > COALESCE(applicable_limit.warning_threshold, applicable_limit.breach_threshold * 0.8) THEN
breach_detected := TRUE;
severity_level := 'medium';
event_type_val := CASE NEW.metric_name
WHEN 'var_1d', 'var_10d' THEN 'var_breach'
WHEN 'exposure_gross', 'exposure_net' THEN 'exposure_limit_breach'
WHEN 'leverage_ratio' THEN 'leverage_excess'
WHEN 'concentration_single', 'concentration_sector' THEN 'concentration_risk'
event_type_val := CASE
WHEN NEW.metric_name IN ('var_1d', 'var_10d') THEN 'var_breach'
WHEN NEW.metric_name IN ('exposure_gross', 'exposure_net') THEN 'exposure_limit_breach'
WHEN NEW.metric_name = 'leverage_ratio' THEN 'leverage_excess'
WHEN NEW.metric_name IN ('concentration_single', 'concentration_sector') THEN 'concentration_risk'
ELSE 'stress_test_failure'
END;
END IF;
@@ -763,7 +840,7 @@ $$ LANGUAGE plpgsql;
-- Function to get active risk alerts
CREATE OR REPLACE FUNCTION get_active_risk_alerts(
p_severity risk_severity[] DEFAULT ARRAY['high', 'critical', 'emergency']
p_severity risk_severity[] DEFAULT ARRAY['high'::risk_severity, 'critical'::risk_severity, 'emergency'::risk_severity]
) RETURNS TABLE (
event_id UUID,
event_type risk_event_type,

View File

@@ -1,369 +0,0 @@
-- Migration 002: Create risk management and performance tracking tables
-- This migration adds comprehensive risk monitoring and performance metrics capabilities
-- Risk metrics table - comprehensive risk tracking
CREATE TABLE IF NOT EXISTS risk_metrics (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id VARCHAR(64), -- Account identifier
metric_type VARCHAR(50) NOT NULL CHECK (metric_type IN (
'exposure', 'var', 'drawdown', 'violation', 'concentration',
'leverage', 'margin', 'volatility', 'beta', 'correlation'
)),
symbol VARCHAR(32), -- NULL for portfolio-level metrics
value DECIMAL(20, 8) NOT NULL, -- Metric value with high precision
threshold DECIMAL(20, 8), -- Risk threshold
severity VARCHAR(20) NOT NULL DEFAULT 'low' CHECK (severity IN ('low', 'medium', 'high', 'critical')),
description TEXT NOT NULL, -- Human-readable description
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB -- Additional risk data
);
-- Risk violations table - audit trail of risk breaches
CREATE TABLE IF NOT EXISTS risk_violations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id VARCHAR(64),
violation_type VARCHAR(50) NOT NULL,
symbol VARCHAR(32),
threshold_value DECIMAL(20, 8) NOT NULL,
actual_value DECIMAL(20, 8) NOT NULL,
severity VARCHAR(20) NOT NULL CHECK (severity IN ('warning', 'error', 'critical')),
description TEXT NOT NULL,
action_taken VARCHAR(100), -- Action taken in response
resolved_at TIMESTAMP WITH TIME ZONE, -- When violation was resolved
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- Daily statistics table - comprehensive daily trading metrics
CREATE TABLE IF NOT EXISTS daily_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id VARCHAR(64),
date DATE NOT NULL, -- Trading date
total_trades INTEGER NOT NULL DEFAULT 0,
total_volume BIGINT NOT NULL DEFAULT 0,
gross_pnl BIGINT NOT NULL DEFAULT 0, -- Gross P&L in fixed-point cents
net_pnl BIGINT NOT NULL DEFAULT 0, -- Net P&L after fees in fixed-point cents
fees_paid BIGINT NOT NULL DEFAULT 0, -- Total fees paid in fixed-point cents
winning_trades INTEGER NOT NULL DEFAULT 0,
losing_trades INTEGER NOT NULL DEFAULT 0,
largest_win BIGINT NOT NULL DEFAULT 0, -- Largest winning trade
largest_loss BIGINT NOT NULL DEFAULT 0, -- Largest losing trade (negative)
max_drawdown DECIMAL(10, 4) NOT NULL DEFAULT 0, -- Maximum drawdown percentage
max_position_size BIGINT NOT NULL DEFAULT 0, -- Maximum position size held
avg_trade_size BIGINT NOT NULL DEFAULT 0, -- Average trade size
sharpe_ratio DECIMAL(10, 4), -- Sharpe ratio
win_rate DECIMAL(5, 4), -- Win rate percentage (0-1)
profit_factor DECIMAL(10, 4), -- Profit factor
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Ensure unique stats per account-date combination
UNIQUE(account_id, date)
);
-- Performance metrics table - system and trading performance tracking
CREATE TABLE IF NOT EXISTS performance_metrics (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
metric_name VARCHAR(100) NOT NULL, -- e.g., "latency_p50", "latency_p95", "throughput"
metric_value DECIMAL(20, 8) NOT NULL, -- Metric value
unit VARCHAR(50) NOT NULL, -- "nanoseconds", "ops_per_second", "percent", etc.
component VARCHAR(100) NOT NULL, -- "order_processing", "market_data", "risk_engine"
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
tags JSONB, -- Additional tags for grouping/filtering
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
-- Composite indexes will be created after table creation
);
-- Audit logs table - comprehensive audit trail for compliance
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_type VARCHAR(50) NOT NULL, -- "order_placed", "trade_executed", "position_updated"
entity_type VARCHAR(50) NOT NULL, -- "order", "fill", "position", "risk_metric"
entity_id UUID NOT NULL, -- ID of the affected entity
account_id VARCHAR(64),
user_id VARCHAR(64),
action VARCHAR(50) NOT NULL, -- "create", "update", "delete", "execute"
old_values JSONB, -- Previous state (for updates)
new_values JSONB, -- New state
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
source VARCHAR(100) NOT NULL, -- System component that generated the event
correlation_id UUID, -- For tracking related events
session_id VARCHAR(128), -- User session identifier
ip_address INET, -- Client IP address
user_agent TEXT, -- Client user agent
metadata JSONB, -- Additional audit metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Strategy performance table - track individual strategy performance
CREATE TABLE IF NOT EXISTS strategy_performance (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
strategy_name VARCHAR(100) NOT NULL,
account_id VARCHAR(64),
date DATE NOT NULL,
trades_count INTEGER NOT NULL DEFAULT 0,
total_pnl BIGINT NOT NULL DEFAULT 0, -- P&L in fixed-point cents
win_rate DECIMAL(5, 4), -- Win rate (0-1)
sharpe_ratio DECIMAL(10, 4),
max_drawdown DECIMAL(10, 4),
avg_trade_duration INTERVAL, -- Average time positions are held
total_volume BIGINT NOT NULL DEFAULT 0,
risk_adjusted_return DECIMAL(10, 4),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(strategy_name, account_id, date)
);
-- Symbol statistics table - per-symbol performance and characteristics
CREATE TABLE IF NOT EXISTS symbol_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
symbol VARCHAR(32) NOT NULL,
date DATE NOT NULL,
open_price BIGINT, -- Opening price in fixed-point cents
high_price BIGINT, -- High price
low_price BIGINT, -- Low price
close_price BIGINT, -- Closing price
volume BIGINT NOT NULL DEFAULT 0,
trade_count INTEGER NOT NULL DEFAULT 0,
vwap BIGINT, -- Volume-weighted average price
volatility DECIMAL(10, 6), -- Daily volatility
beta DECIMAL(10, 4), -- Beta relative to market
correlation_spy DECIMAL(10, 4), -- Correlation to SPY
avg_spread BIGINT, -- Average bid-ask spread
liquidity_score DECIMAL(5, 2), -- Liquidity scoring
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(symbol, date)
);
-- Create composite indexes for risk_metrics table
CREATE INDEX IF NOT EXISTS idx_risk_metrics_composite ON risk_metrics(metric_type, severity, timestamp);
CREATE INDEX IF NOT EXISTS idx_risk_metrics_symbol_type ON risk_metrics(symbol, metric_type);
CREATE INDEX IF NOT EXISTS idx_risk_metrics_account_timestamp ON risk_metrics(account_id, timestamp);
-- Create composite indexes for performance_metrics table
CREATE INDEX IF NOT EXISTS idx_performance_metrics_component_name_timestamp ON performance_metrics(component, metric_name, timestamp);
CREATE INDEX IF NOT EXISTS idx_performance_metrics_timestamp ON performance_metrics(timestamp);
-- Create optimized indexes for performance queries
-- Risk metrics indexes
CREATE INDEX IF NOT EXISTS idx_risk_metrics_timestamp ON risk_metrics(timestamp);
CREATE INDEX IF NOT EXISTS idx_risk_metrics_account_id ON risk_metrics(account_id) WHERE account_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_risk_violations_timestamp ON risk_violations(timestamp);
CREATE INDEX IF NOT EXISTS idx_risk_violations_severity ON risk_violations(severity);
-- Daily stats indexes
CREATE INDEX IF NOT EXISTS idx_daily_stats_date ON daily_stats(date);
CREATE INDEX IF NOT EXISTS idx_daily_stats_account_date ON daily_stats(account_id, date);
-- Performance metrics indexes (optimized for time-series analysis)
CREATE INDEX IF NOT EXISTS idx_performance_metrics_name_timestamp ON performance_metrics(metric_name, timestamp);
-- Audit logs indexes (optimized for compliance queries)
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_account_id ON audit_logs(account_id) WHERE account_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_audit_logs_correlation_id ON audit_logs(correlation_id) WHERE correlation_id IS NOT NULL;
-- Strategy performance indexes
CREATE INDEX IF NOT EXISTS idx_strategy_performance_name_date ON strategy_performance(strategy_name, date);
CREATE INDEX IF NOT EXISTS idx_strategy_performance_account_date ON strategy_performance(account_id, date);
-- Symbol stats indexes
CREATE INDEX IF NOT EXISTS idx_symbol_stats_symbol_date ON symbol_stats(symbol, date);
CREATE INDEX IF NOT EXISTS idx_symbol_stats_date ON symbol_stats(date);
-- Create triggers for automatic timestamp updates
CREATE TRIGGER trigger_daily_stats_updated_at
BEFORE UPDATE ON daily_stats
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_strategy_performance_updated_at
BEFORE UPDATE ON strategy_performance
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Create materialized view for real-time risk dashboard
CREATE MATERIALIZED VIEW IF NOT EXISTS risk_dashboard AS
SELECT
account_id,
metric_type,
symbol,
AVG(value) as avg_value,
MAX(value) as max_value,
MIN(value) as min_value,
COUNT(*) as measurement_count,
COUNT(*) FILTER (WHERE severity IN ('high', 'critical')) as high_risk_count,
MAX(timestamp) as last_updated
FROM risk_metrics
WHERE timestamp >= NOW() - INTERVAL '1 day'
GROUP BY account_id, metric_type, symbol;
-- Create unique index on risk dashboard materialized view
CREATE UNIQUE INDEX IF NOT EXISTS idx_risk_dashboard_unique
ON risk_dashboard(account_id, metric_type, COALESCE(symbol, ''));
-- Create materialized view for performance summary
CREATE MATERIALIZED VIEW IF NOT EXISTS performance_summary AS
SELECT
component,
metric_name,
unit,
AVG(metric_value) as avg_value,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY metric_value) as p50_value,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY metric_value) as p95_value,
PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY metric_value) as p99_value,
MAX(metric_value) as max_value,
MIN(metric_value) as min_value,
COUNT(*) as sample_count,
MAX(timestamp) as last_updated
FROM performance_metrics
WHERE timestamp >= NOW() - INTERVAL '1 hour'
GROUP BY component, metric_name, unit;
-- Create unique index on performance summary
CREATE UNIQUE INDEX IF NOT EXISTS idx_performance_summary_unique
ON performance_summary(component, metric_name, unit);
-- Create functions for risk management
-- Function to calculate portfolio exposure
CREATE OR REPLACE FUNCTION calculate_portfolio_exposure(p_account_id VARCHAR(64) DEFAULT NULL)
RETURNS DECIMAL(20, 2) AS $$
DECLARE
total_exposure DECIMAL(20, 2) := 0;
BEGIN
SELECT COALESCE(SUM(ABS(quantity * last_price) / 100.0), 0)
INTO total_exposure
FROM positions
WHERE (p_account_id IS NULL OR account_id = p_account_id)
AND quantity != 0;
RETURN total_exposure;
END;
$$ LANGUAGE plpgsql;
-- Function to calculate daily P&L
CREATE OR REPLACE FUNCTION calculate_daily_pnl(p_date DATE, p_account_id VARCHAR(64) DEFAULT NULL)
RETURNS DECIMAL(20, 2) AS $$
DECLARE
total_pnl DECIMAL(20, 2) := 0;
BEGIN
SELECT COALESCE(SUM(
(CASE WHEN f.side = 'buy' THEN -1 ELSE 1 END) * f.quantity * f.price / 100.0
), 0)
INTO total_pnl
FROM fills f
WHERE DATE(f.execution_time) = p_date
AND (p_account_id IS NULL OR EXISTS (
SELECT 1 FROM orders o
WHERE o.id = f.order_id
AND o.account_id = p_account_id
));
RETURN total_pnl;
END;
$$ LANGUAGE plpgsql;
-- Function to update daily statistics
CREATE OR REPLACE FUNCTION update_daily_stats(p_date DATE, p_account_id VARCHAR(64) DEFAULT NULL)
RETURNS VOID AS $$
DECLARE
v_total_trades INTEGER;
v_total_volume BIGINT;
v_gross_pnl BIGINT;
v_winning_trades INTEGER;
v_losing_trades INTEGER;
v_largest_win BIGINT;
v_largest_loss BIGINT;
v_avg_trade_size BIGINT;
v_win_rate DECIMAL(5, 4);
v_profit_factor DECIMAL(10, 4);
v_gross_wins DECIMAL(20, 2);
v_gross_losses DECIMAL(20, 2);
BEGIN
-- Calculate basic stats
SELECT
COUNT(*),
SUM(quantity),
SUM((CASE WHEN side = 'buy' THEN -1 ELSE 1 END) * quantity * price),
AVG(quantity * price)
INTO v_total_trades, v_total_volume, v_gross_pnl, v_avg_trade_size
FROM fills f
JOIN orders o ON f.order_id = o.id
WHERE DATE(f.execution_time) = p_date
AND (p_account_id IS NULL OR o.account_id = p_account_id);
-- Calculate win/loss statistics
SELECT
COUNT(*) FILTER (WHERE pnl > 0),
COUNT(*) FILTER (WHERE pnl < 0),
MAX(pnl),
MIN(pnl),
SUM(pnl) FILTER (WHERE pnl > 0),
ABS(SUM(pnl) FILTER (WHERE pnl < 0))
INTO v_winning_trades, v_losing_trades, v_largest_win, v_largest_loss, v_gross_wins, v_gross_losses
FROM (
SELECT (CASE WHEN f.side = 'buy' THEN -1 ELSE 1 END) * f.quantity * f.price as pnl
FROM fills f
JOIN orders o ON f.order_id = o.id
WHERE DATE(f.execution_time) = p_date
AND (p_account_id IS NULL OR o.account_id = p_account_id)
) trade_pnl;
-- Calculate derived metrics
v_win_rate := CASE
WHEN v_total_trades > 0 THEN v_winning_trades::DECIMAL / v_total_trades
ELSE 0
END;
v_profit_factor := CASE
WHEN v_gross_losses > 0 THEN v_gross_wins / v_gross_losses
ELSE NULL
END;
-- Insert or update daily stats
INSERT INTO daily_stats (
account_id, date, total_trades, total_volume, gross_pnl,
winning_trades, losing_trades, largest_win, largest_loss,
avg_trade_size, win_rate, profit_factor
) VALUES (
p_account_id, p_date, COALESCE(v_total_trades, 0), COALESCE(v_total_volume, 0), COALESCE(v_gross_pnl, 0),
COALESCE(v_winning_trades, 0), COALESCE(v_losing_trades, 0), COALESCE(v_largest_win, 0), COALESCE(v_largest_loss, 0),
COALESCE(v_avg_trade_size, 0), v_win_rate, v_profit_factor
)
ON CONFLICT (account_id, date)
DO UPDATE SET
total_trades = EXCLUDED.total_trades,
total_volume = EXCLUDED.total_volume,
gross_pnl = EXCLUDED.gross_pnl,
winning_trades = EXCLUDED.winning_trades,
losing_trades = EXCLUDED.losing_trades,
largest_win = EXCLUDED.largest_win,
largest_loss = EXCLUDED.largest_loss,
avg_trade_size = EXCLUDED.avg_trade_size,
win_rate = EXCLUDED.win_rate,
profit_factor = EXCLUDED.profit_factor,
updated_at = NOW();
END;
$$ LANGUAGE plpgsql;
-- Add comments for documentation
COMMENT ON TABLE risk_metrics IS 'Comprehensive risk metrics tracking with real-time monitoring';
COMMENT ON TABLE risk_violations IS 'Audit trail of risk limit breaches for compliance';
COMMENT ON TABLE daily_stats IS 'Daily trading statistics and performance metrics';
COMMENT ON TABLE performance_metrics IS 'System performance metrics for latency and throughput monitoring';
COMMENT ON TABLE audit_logs IS 'Complete audit trail for regulatory compliance';
COMMENT ON TABLE strategy_performance IS 'Individual strategy performance tracking';
COMMENT ON TABLE symbol_stats IS 'Per-symbol market characteristics and performance';
COMMENT ON FUNCTION calculate_portfolio_exposure IS 'Calculate total portfolio exposure in dollars';
COMMENT ON FUNCTION calculate_daily_pnl IS 'Calculate daily P&L for specified date and account';
COMMENT ON FUNCTION update_daily_stats IS 'Update daily statistics from fill data';

View File

@@ -105,7 +105,7 @@ CREATE TYPE system_component AS ENUM (
-- ================================================================================================
CREATE TABLE audit_log (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
audit_id BIGSERIAL NOT NULL, -- Sequential audit ID for ordering
correlation_id UUID, -- Links related audit entries
@@ -183,9 +183,10 @@ CREATE TABLE audit_log (
stack_trace TEXT,
-- Partition key for performance
audit_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(event_timestamp / 1000000000.0))) STORED,
audit_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, audit_date),
CONSTRAINT chk_audit_timestamps CHECK (
recorded_timestamp >= event_timestamp AND
processing_timestamp >= recorded_timestamp
@@ -200,7 +201,7 @@ CREATE TABLE audit_log (
-- ================================================================================================
CREATE TABLE ml_events (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
model_id VARCHAR(200) NOT NULL, -- Unique model identifier
model_version VARCHAR(50) NOT NULL,
@@ -270,7 +271,10 @@ CREATE TABLE ml_events (
custom_metrics JSONB, -- Domain-specific metrics
-- Partition key
event_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(event_timestamp / 1000000000.0))) STORED
event_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, event_date)
) PARTITION BY RANGE (event_date);
-- ================================================================================================
@@ -279,7 +283,7 @@ CREATE TABLE ml_events (
-- ================================================================================================
CREATE TABLE system_events (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
event_timestamp ns_timestamp NOT NULL,
-- Event classification
@@ -336,7 +340,10 @@ CREATE TABLE system_events (
namespace VARCHAR(100), -- Kubernetes namespace
-- Partition key
event_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(event_timestamp / 1000000000.0))) STORED
event_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, event_date)
) PARTITION BY RANGE (event_date);
-- ================================================================================================
@@ -345,7 +352,7 @@ CREATE TABLE system_events (
-- ================================================================================================
CREATE TABLE change_tracking (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID DEFAULT uuid_generate_v4(),
change_timestamp ns_timestamp NOT NULL,
-- Change context
@@ -377,7 +384,10 @@ CREATE TABLE change_tracking (
checksum VARCHAR(64) NOT NULL, -- Integrity check
-- Partition key
change_date DATE GENERATED ALWAYS AS (DATE(TO_TIMESTAMP(change_timestamp / 1000000000.0))) STORED
change_date DATE NOT NULL,
-- Constraints
PRIMARY KEY (id, change_date)
) PARTITION BY RANGE (change_date);
-- ================================================================================================
@@ -387,7 +397,7 @@ CREATE TABLE change_tracking (
CREATE TABLE compliance_annotations (
-- Primary identifiers
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
audit_log_id UUID NOT NULL REFERENCES audit_log(id),
audit_log_id UUID NOT NULL, -- FK removed: audit_log has composite PK (id, audit_date)
-- Compliance framework
regulation_name VARCHAR(100) NOT NULL, -- MiFID II, GDPR, SOX, etc.
@@ -779,6 +789,66 @@ BEGIN
END;
$$ LANGUAGE plpgsql;
-- ================================================================================================
-- TRIGGER FUNCTIONS FOR GENERATED COLUMNS (converted from GENERATED ALWAYS)
-- ================================================================================================
-- Set audit_date for audit_log
CREATE OR REPLACE FUNCTION set_audit_log_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.audit_date := DATE(TO_TIMESTAMP(NEW.event_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_audit_log_date
BEFORE INSERT OR UPDATE ON audit_log
FOR EACH ROW
EXECUTE FUNCTION set_audit_log_date();
-- Set event_date for ml_events
CREATE OR REPLACE FUNCTION set_ml_event_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.event_date := DATE(TO_TIMESTAMP(NEW.event_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_ml_event_date
BEFORE INSERT OR UPDATE ON ml_events
FOR EACH ROW
EXECUTE FUNCTION set_ml_event_date();
-- Set event_date for system_events
CREATE OR REPLACE FUNCTION set_system_event_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.event_date := DATE(TO_TIMESTAMP(NEW.event_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_system_event_date
BEFORE INSERT OR UPDATE ON system_events
FOR EACH ROW
EXECUTE FUNCTION set_system_event_date();
-- Set change_date for change_tracking
CREATE OR REPLACE FUNCTION set_change_tracking_date()
RETURNS TRIGGER AS $$
BEGIN
NEW.change_date := DATE(TO_TIMESTAMP(NEW.change_timestamp / 1000000000.0));
RETURN NEW;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE TRIGGER tg_set_change_tracking_date
BEFORE INSERT OR UPDATE ON change_tracking
FOR EACH ROW
EXECUTE FUNCTION set_change_tracking_date();
-- ================================================================================================
-- CREATE TRIGGERS FOR CHANGE TRACKING
-- ================================================================================================

View File

@@ -1,198 +0,0 @@
-- WAL checkpoint table for write-ahead logging and recovery
CREATE TABLE IF NOT EXISTS wal_checkpoints (
id UUID PRIMARY KEY,
sequence_number BIGINT NOT NULL UNIQUE,
checkpoint_timestamp TIMESTAMPTZ NOT NULL,
database_state_hash TEXT NOT NULL,
entries_count BIGINT NOT NULL DEFAULT 0,
file_path TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for efficient checkpoint queries
CREATE INDEX IF NOT EXISTS idx_wal_checkpoints_sequence ON wal_checkpoints(sequence_number DESC);
CREATE INDEX IF NOT EXISTS idx_wal_checkpoints_timestamp ON wal_checkpoints(checkpoint_timestamp DESC);
-- Add sequence number to audit_logs for WAL ordering
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS sequence_number BIGINT;
-- Create sequence for audit log entries if not exists
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_sequences WHERE sequencename = 'audit_logs_sequence_seq') THEN
CREATE SEQUENCE audit_logs_sequence_seq START 1;
END IF;
END $$;
-- Set default for sequence number
ALTER TABLE audit_logs ALTER COLUMN sequence_number SET DEFAULT nextval('audit_logs_sequence_seq');
-- Update existing audit_logs entries to have sequence numbers
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM audit_logs WHERE sequence_number IS NULL LIMIT 1) THEN
WITH ordered_logs AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at ASC) as seq_num
FROM audit_logs
WHERE sequence_number IS NULL
)
UPDATE audit_logs
SET sequence_number = ordered_logs.seq_num
FROM ordered_logs
WHERE audit_logs.id = ordered_logs.id;
-- Update sequence to continue from max value
PERFORM setval('audit_logs_sequence_seq', (SELECT COALESCE(MAX(sequence_number), 0) FROM audit_logs));
END IF;
END $$;
-- Make sequence_number NOT NULL after populating existing data
ALTER TABLE audit_logs ALTER COLUMN sequence_number SET NOT NULL;
-- Create unique index on sequence number
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_logs_sequence_unique ON audit_logs(sequence_number);
-- Additional indexes for WAL operations
CREATE INDEX IF NOT EXISTS idx_audit_logs_session_sequence ON audit_logs(session_id, sequence_number);
-- Function to get next WAL sequence number (atomic)
CREATE OR REPLACE FUNCTION get_next_wal_sequence()
RETURNS BIGINT
LANGUAGE plpgsql
AS $$
DECLARE
next_seq BIGINT;
BEGIN
SELECT nextval('audit_logs_sequence_seq') INTO next_seq;
RETURN next_seq;
END;
$$;
-- Function to create WAL checkpoint
CREATE OR REPLACE FUNCTION create_wal_checkpoint(
p_checkpoint_id UUID,
p_database_state_hash TEXT,
p_file_path TEXT
)
RETURNS TABLE(
checkpoint_id UUID,
sequence_number BIGINT,
checkpoint_timestamp TIMESTAMPTZ,
entries_count BIGINT
)
LANGUAGE plpgsql
AS $$
DECLARE
current_sequence BIGINT;
current_count BIGINT;
checkpoint_time TIMESTAMPTZ := NOW();
BEGIN
-- Get current WAL position
SELECT COALESCE(MAX(audit_logs.sequence_number), 0) INTO current_sequence FROM audit_logs;
SELECT COUNT(*) INTO current_count FROM audit_logs;
-- Insert checkpoint
INSERT INTO wal_checkpoints (
id, sequence_number, checkpoint_timestamp, database_state_hash,
entries_count, file_path, created_at
) VALUES (
p_checkpoint_id, current_sequence, checkpoint_time,
p_database_state_hash, current_count, p_file_path, checkpoint_time
);
-- Return checkpoint info
RETURN QUERY SELECT
p_checkpoint_id,
current_sequence,
checkpoint_time,
current_count;
END;
$$;
-- Function to verify WAL integrity
CREATE OR REPLACE FUNCTION verify_wal_integrity(
p_start_sequence BIGINT,
p_end_sequence BIGINT
)
RETURNS TABLE(
sequence_number BIGINT,
is_valid BOOLEAN,
expected_checksum TEXT,
actual_checksum TEXT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
al.sequence_number,
TRUE as is_valid, -- Simplified integrity check
'' as expected_checksum,
'' as actual_checksum
FROM audit_logs al
WHERE al.sequence_number >= p_start_sequence
AND al.sequence_number <= p_end_sequence
ORDER BY al.sequence_number;
END;
$$;
-- Function to cleanup old WAL entries before checkpoint
CREATE OR REPLACE FUNCTION cleanup_wal_before_checkpoint(p_checkpoint_id UUID)
RETURNS BIGINT
LANGUAGE plpgsql
AS $$
DECLARE
checkpoint_sequence BIGINT;
deleted_count BIGINT;
BEGIN
-- Get checkpoint sequence number
SELECT wc.sequence_number INTO checkpoint_sequence
FROM wal_checkpoints wc
WHERE wc.id = p_checkpoint_id;
IF checkpoint_sequence IS NULL THEN
RAISE EXCEPTION 'Checkpoint not found: %', p_checkpoint_id;
END IF;
-- Delete audit logs before checkpoint, keeping some safety margin
DELETE FROM audit_logs
WHERE sequence_number < (checkpoint_sequence - 1000); -- Keep 1000 entries as safety margin
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$;
-- WAL statistics view for monitoring
CREATE OR REPLACE VIEW wal_statistics AS
SELECT
COUNT(*) as total_entries,
MIN(timestamp) as oldest_entry_timestamp,
MAX(timestamp) as newest_entry_timestamp,
MIN(sequence_number) as min_sequence,
MAX(sequence_number) as max_sequence,
pg_size_pretty(pg_total_relation_size('audit_logs')) as table_size,
(SELECT COUNT(*) FROM wal_checkpoints) as checkpoint_count,
(SELECT MAX(sequence_number) FROM wal_checkpoints) as last_checkpoint_sequence,
CASE
WHEN MAX(timestamp) > MIN(timestamp)
THEN COUNT(*)::FLOAT / EXTRACT(epoch FROM (MAX(timestamp) - MIN(timestamp)))
ELSE 0
END as avg_entries_per_second
FROM audit_logs;
-- Grant permissions for trading engine user
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'foxhunt_trader') THEN
GRANT SELECT, INSERT ON wal_checkpoints TO foxhunt_trader;
GRANT SELECT, INSERT, UPDATE ON audit_logs TO foxhunt_trader;
GRANT USAGE ON SEQUENCE audit_logs_sequence_seq TO foxhunt_trader;
GRANT SELECT ON wal_statistics TO foxhunt_trader;
GRANT EXECUTE ON FUNCTION get_next_wal_sequence() TO foxhunt_trader;
GRANT EXECUTE ON FUNCTION create_wal_checkpoint(UUID, TEXT, TEXT) TO foxhunt_trader;
GRANT EXECUTE ON FUNCTION verify_wal_integrity(BIGINT, BIGINT) TO foxhunt_trader;
GRANT EXECUTE ON FUNCTION cleanup_wal_before_checkpoint(UUID) TO foxhunt_trader;
END IF;
END $$;

View File

@@ -376,8 +376,9 @@ SELECT
string_agg(DISTINCT al.action, ', ') as processing_purposes,
-- Data minimization assessment
COUNT(DISTINCT jsonb_object_keys(al.event_data)) as data_fields_processed,
AVG(jsonb_array_length(COALESCE(al.affected_fields, '[]'::text[]))) as avg_fields_per_operation
-- Note: Using jsonb field count estimation (not exact unique keys across group)
AVG((SELECT COUNT(*) FROM jsonb_each(al.event_data))::numeric) as avg_data_fields_processed,
AVG(array_length(COALESCE(al.affected_fields, ARRAY[]::text[]), 1)) as avg_fields_per_operation
FROM audit_log al
WHERE al.event_timestamp >= EXTRACT(EPOCH FROM (CURRENT_DATE - INTERVAL '12 months')) * 1000000000
@@ -556,7 +557,7 @@ SELECT
-- Risk and compliance alerts
(SELECT COUNT(*) FROM risk_events WHERE event_timestamp >= EXTRACT(EPOCH FROM CURRENT_DATE) * 1000000000 AND severity IN ('critical', 'emergency')) as critical_risk_events,
(SELECT COUNT(*) FROM audit_log WHERE event_timestamp >= EXTRACT(EPOCH FROM CURRENT_DATE) * 1000000000 AND severity = 'error') as system_errors,
(SELECT COUNT(*) FROM audit_log WHERE event_timestamp >= EXTRACT(EPOCH FROM CURRENT_DATE) * 1000000000 AND event_type = 'compliance_violation') as compliance_violations,
(SELECT COUNT(*) FROM audit_log WHERE event_timestamp >= EXTRACT(EPOCH FROM CURRENT_DATE) * 1000000000 AND event_type = 'compliance_check_failed') as compliance_violations,
-- Regulatory reporting status
(SELECT COUNT(*) FROM report_generation_log WHERE generation_started_at >= CURRENT_DATE AND generation_status = 'completed') as reports_completed_today,

View File

@@ -1,509 +0,0 @@
-- Migration 004: User Management and Authentication
-- This migration creates comprehensive user management for HFT trading systems
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Users table - comprehensive user management
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(64) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
phone VARCHAR(20),
time_zone VARCHAR(50) DEFAULT 'UTC',
language VARCHAR(10) DEFAULT 'en',
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended', 'locked')),
role VARCHAR(50) NOT NULL DEFAULT 'trader' CHECK (role IN ('admin', 'trader', 'risk_manager', 'analyst', 'readonly')),
last_login TIMESTAMP WITH TIME ZONE,
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
locked_until TIMESTAMP WITH TIME ZONE,
password_changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
password_expires_at TIMESTAMP WITH TIME ZONE,
two_factor_enabled BOOLEAN NOT NULL DEFAULT false,
two_factor_secret TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id),
metadata JSONB
);
-- Sessions table - track user sessions for security
CREATE TABLE IF NOT EXISTS user_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
session_token TEXT NOT NULL UNIQUE,
refresh_token TEXT,
ip_address INET NOT NULL,
user_agent TEXT,
location JSONB,
is_active BOOLEAN NOT NULL DEFAULT true,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- API Keys table - for programmatic access
CREATE TABLE IF NOT EXISTS api_keys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
key_name VARCHAR(100) NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
key_prefix VARCHAR(20) NOT NULL,
permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
rate_limit INTEGER DEFAULT 1000, -- requests per minute
is_active BOOLEAN NOT NULL DEFAULT true,
last_used_at TIMESTAMP WITH TIME ZONE,
usage_count BIGINT NOT NULL DEFAULT 0,
expires_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- Accounts table - trading accounts linked to users
CREATE TABLE IF NOT EXISTS accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_number VARCHAR(64) NOT NULL UNIQUE,
account_name VARCHAR(100) NOT NULL,
user_id UUID NOT NULL REFERENCES users(id),
account_type VARCHAR(20) NOT NULL DEFAULT 'individual' CHECK (account_type IN ('individual', 'corporate', 'institutional', 'demo')),
base_currency VARCHAR(10) NOT NULL DEFAULT 'USD',
initial_balance DECIMAL(20, 8) NOT NULL DEFAULT 0,
current_balance DECIMAL(20, 8) NOT NULL DEFAULT 0,
available_balance DECIMAL(20, 8) NOT NULL DEFAULT 0,
margin_balance DECIMAL(20, 8) NOT NULL DEFAULT 0,
equity DECIMAL(20, 8) NOT NULL DEFAULT 0,
free_margin DECIMAL(20, 8) NOT NULL DEFAULT 0,
margin_level DECIMAL(10, 4) NOT NULL DEFAULT 0, -- Margin level percentage
leverage DECIMAL(10, 2) NOT NULL DEFAULT 1.00,
max_leverage DECIMAL(10, 2) NOT NULL DEFAULT 100.00,
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended', 'closed')),
risk_profile VARCHAR(20) NOT NULL DEFAULT 'medium' CHECK (risk_profile IN ('conservative', 'medium', 'aggressive', 'high_frequency')),
broker VARCHAR(100),
broker_account_id VARCHAR(100),
is_demo BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- Account permissions - granular access control
CREATE TABLE IF NOT EXISTS account_permissions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
permission_type VARCHAR(50) NOT NULL, -- 'read', 'trade', 'admin', 'risk_override'
granted_by UUID NOT NULL REFERENCES users(id),
granted_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN NOT NULL DEFAULT true,
metadata JSONB,
UNIQUE(user_id, account_id, permission_type)
);
-- Brokers table - external broker connections
CREATE TABLE IF NOT EXISTS brokers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL UNIQUE,
broker_type VARCHAR(50) NOT NULL, -- 'mt4', 'mt5', 'ctrader', 'fix', 'rest'
api_endpoint TEXT,
fix_settings JSONB,
connection_settings JSONB NOT NULL DEFAULT '{}'::jsonb,
credentials_encrypted TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
is_demo BOOLEAN NOT NULL DEFAULT false,
supported_symbols TEXT[], -- Array of supported symbols
commission_settings JSONB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- Broker connections - track live connections
CREATE TABLE IF NOT EXISTS broker_connections (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
broker_id UUID NOT NULL REFERENCES brokers(id) ON DELETE CASCADE,
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
connection_id VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'disconnected' CHECK (status IN ('connected', 'connecting', 'disconnected', 'error')),
last_heartbeat TIMESTAMP WITH TIME ZONE,
latency_ms INTEGER,
error_message TEXT,
connection_attempts INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB,
UNIQUE(broker_id, account_id)
);
-- Compliance profiles - regulatory requirements
CREATE TABLE IF NOT EXISTS compliance_profiles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
profile_name VARCHAR(100) NOT NULL UNIQUE,
jurisdiction VARCHAR(10) NOT NULL, -- 'US', 'EU', 'UK', 'APAC'
regulations JSONB NOT NULL DEFAULT '{}'::jsonb,
requirements JSONB NOT NULL DEFAULT '{}'::jsonb,
reporting_requirements JSONB NOT NULL DEFAULT '{}'::jsonb,
retention_periods JSONB NOT NULL DEFAULT '{}'::jsonb,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- User compliance assignments
CREATE TABLE IF NOT EXISTS user_compliance (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
compliance_profile_id UUID NOT NULL REFERENCES compliance_profiles(id),
assigned_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
assigned_by UUID NOT NULL REFERENCES users(id),
is_active BOOLEAN NOT NULL DEFAULT true,
metadata JSONB,
UNIQUE(user_id, compliance_profile_id)
);
-- Create optimized indexes for HFT performance
-- Users table indexes
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login);
-- Sessions table indexes
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token);
CREATE INDEX IF NOT EXISTS idx_user_sessions_active ON user_sessions(is_active, expires_at);
CREATE INDEX IF NOT EXISTS idx_user_sessions_ip_address ON user_sessions(ip_address);
-- API Keys table indexes
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(key_prefix);
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active, expires_at);
-- Accounts table indexes
CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts(user_id);
CREATE INDEX IF NOT EXISTS idx_accounts_number ON accounts(account_number);
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status);
CREATE INDEX IF NOT EXISTS idx_accounts_type ON accounts(account_type);
CREATE INDEX IF NOT EXISTS idx_accounts_broker ON accounts(broker);
-- Account permissions indexes
CREATE INDEX IF NOT EXISTS idx_account_permissions_user_account ON account_permissions(user_id, account_id);
CREATE INDEX IF NOT EXISTS idx_account_permissions_type ON account_permissions(permission_type);
CREATE INDEX IF NOT EXISTS idx_account_permissions_active ON account_permissions(is_active, expires_at);
-- Brokers table indexes
CREATE INDEX IF NOT EXISTS idx_brokers_name ON brokers(name);
CREATE INDEX IF NOT EXISTS idx_brokers_type ON brokers(broker_type);
CREATE INDEX IF NOT EXISTS idx_brokers_active ON brokers(is_active);
-- Broker connections indexes
CREATE INDEX IF NOT EXISTS idx_broker_connections_broker_account ON broker_connections(broker_id, account_id);
CREATE INDEX IF NOT EXISTS idx_broker_connections_status ON broker_connections(status);
CREATE INDEX IF NOT EXISTS idx_broker_connections_heartbeat ON broker_connections(last_heartbeat);
-- Create triggers for automatic updates
CREATE TRIGGER trigger_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_accounts_updated_at
BEFORE UPDATE ON accounts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_brokers_updated_at
BEFORE UPDATE ON brokers
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_broker_connections_updated_at
BEFORE UPDATE ON broker_connections
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Create functions for user management
-- Function to create new user with encrypted password
CREATE OR REPLACE FUNCTION create_user(
p_username VARCHAR(64),
p_email VARCHAR(255),
p_password TEXT,
p_first_name VARCHAR(100) DEFAULT NULL,
p_last_name VARCHAR(100) DEFAULT NULL,
p_role VARCHAR(50) DEFAULT 'trader',
p_created_by UUID DEFAULT NULL
)
RETURNS UUID AS $$
DECLARE
v_user_id UUID;
v_salt TEXT;
v_password_hash TEXT;
BEGIN
-- Generate salt and hash password
v_salt := encode(gen_random_bytes(32), 'hex');
v_password_hash := crypt(p_password || v_salt, gen_salt('bf', 12));
-- Insert user
INSERT INTO users (
username, email, password_hash, salt, first_name, last_name,
role, created_by, password_changed_at
) VALUES (
p_username, p_email, v_password_hash, v_salt, p_first_name, p_last_name,
p_role, p_created_by, NOW()
) RETURNING id INTO v_user_id;
-- Create audit log entry
INSERT INTO audit_logs (
event_type, entity_type, entity_id, user_id, action,
new_values, timestamp, source
) VALUES (
'user_created', 'user', v_user_id, p_created_by, 'create',
jsonb_build_object('username', p_username, 'email', p_email, 'role', p_role),
NOW(), 'user_management'
);
RETURN v_user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to authenticate user
CREATE OR REPLACE FUNCTION authenticate_user(
p_username VARCHAR(64),
p_password TEXT,
p_ip_address INET DEFAULT NULL,
p_user_agent TEXT DEFAULT NULL
)
RETURNS TABLE(
user_id UUID,
session_token TEXT,
expires_at TIMESTAMP WITH TIME ZONE,
role VARCHAR(50),
status VARCHAR(20)
) AS $$
DECLARE
v_user_record RECORD;
v_session_token TEXT;
v_expires_at TIMESTAMP WITH TIME ZONE;
BEGIN
-- Get user record
SELECT u.id, u.username, u.password_hash, u.salt, u.status, u.role, u.failed_login_attempts, u.locked_until
INTO v_user_record
FROM users u
WHERE u.username = p_username OR u.email = p_username;
-- Check if user exists
IF v_user_record.id IS NULL THEN
RAISE EXCEPTION 'Invalid credentials';
END IF;
-- Check if account is locked
IF v_user_record.locked_until IS NOT NULL AND v_user_record.locked_until > NOW() THEN
RAISE EXCEPTION 'Account is locked until %', v_user_record.locked_until;
END IF;
-- Check if account is active
IF v_user_record.status != 'active' THEN
RAISE EXCEPTION 'Account is not active';
END IF;
-- Verify password
IF NOT (v_user_record.password_hash = crypt(p_password || v_user_record.salt, v_user_record.password_hash)) THEN
-- Increment failed login attempts
UPDATE users
SET failed_login_attempts = failed_login_attempts + 1,
locked_until = CASE
WHEN failed_login_attempts >= 4 THEN NOW() + INTERVAL '30 minutes'
ELSE NULL
END
WHERE id = v_user_record.id;
RAISE EXCEPTION 'Invalid credentials';
END IF;
-- Reset failed login attempts and update last login
UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL,
last_login = NOW()
WHERE id = v_user_record.id;
-- Generate session token
v_session_token := encode(gen_random_bytes(64), 'hex');
v_expires_at := NOW() + INTERVAL '8 hours';
-- Create session
INSERT INTO user_sessions (
user_id, session_token, ip_address, user_agent, expires_at, last_used_at
) VALUES (
v_user_record.id, v_session_token, p_ip_address, p_user_agent, v_expires_at, NOW()
);
-- Log successful login
INSERT INTO audit_logs (
event_type, entity_type, entity_id, user_id, action,
new_values, timestamp, source, ip_address, user_agent
) VALUES (
'user_login', 'user', v_user_record.id, v_user_record.id, 'login',
jsonb_build_object('ip_address', p_ip_address::TEXT),
NOW(), 'authentication', p_ip_address, p_user_agent
);
-- Return session info
RETURN QUERY SELECT
v_user_record.id,
v_session_token,
v_expires_at,
v_user_record.role,
v_user_record.status;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to validate session
CREATE OR REPLACE FUNCTION validate_session(p_session_token TEXT)
RETURNS TABLE(
user_id UUID,
username VARCHAR(64),
role VARCHAR(50),
expires_at TIMESTAMP WITH TIME ZONE,
is_valid BOOLEAN
) AS $$
BEGIN
-- Update last used time and return session info
UPDATE user_sessions
SET last_used_at = NOW()
WHERE session_token = p_session_token
AND is_active = true
AND expires_at > NOW();
RETURN QUERY
SELECT
u.id,
u.username,
u.role,
s.expires_at,
(s.id IS NOT NULL AND s.expires_at > NOW()) as is_valid
FROM user_sessions s
JOIN users u ON s.user_id = u.id
WHERE s.session_token = p_session_token
AND s.is_active = true
AND u.status = 'active';
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to create trading account
CREATE OR REPLACE FUNCTION create_trading_account(
p_user_id UUID,
p_account_name VARCHAR(100),
p_account_type VARCHAR(20) DEFAULT 'individual',
p_initial_balance DECIMAL(20, 8) DEFAULT 0,
p_leverage DECIMAL(10, 2) DEFAULT 1.00,
p_is_demo BOOLEAN DEFAULT false
)
RETURNS UUID AS $$
DECLARE
v_account_id UUID;
v_account_number VARCHAR(64);
BEGIN
-- Generate account number
v_account_number := 'AC' || to_char(NOW(), 'YYYYMMDD') || '-' ||
encode(gen_random_bytes(4), 'hex');
-- Insert account
INSERT INTO accounts (
user_id, account_number, account_name, account_type,
initial_balance, current_balance, available_balance,
leverage, is_demo
) VALUES (
p_user_id, v_account_number, p_account_name, p_account_type,
p_initial_balance, p_initial_balance, p_initial_balance,
p_leverage, p_is_demo
) RETURNING id INTO v_account_id;
-- Grant full permissions to account owner
INSERT INTO account_permissions (
user_id, account_id, permission_type, granted_by
) VALUES
(p_user_id, v_account_id, 'read', p_user_id),
(p_user_id, v_account_id, 'trade', p_user_id),
(p_user_id, v_account_id, 'admin', p_user_id);
-- Create audit log
INSERT INTO audit_logs (
event_type, entity_type, entity_id, user_id, action,
new_values, timestamp, source
) VALUES (
'account_created', 'account', v_account_id, p_user_id, 'create',
jsonb_build_object(
'account_number', v_account_number,
'account_type', p_account_type,
'initial_balance', p_initial_balance,
'is_demo', p_is_demo
),
NOW(), 'account_management'
);
RETURN v_account_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Add constraints for data integrity
ALTER TABLE users ADD CONSTRAINT check_password_expiry
CHECK (password_expires_at IS NULL OR password_expires_at > password_changed_at);
ALTER TABLE accounts ADD CONSTRAINT check_balances
CHECK (current_balance >= 0 AND available_balance >= 0);
ALTER TABLE accounts ADD CONSTRAINT check_leverage
CHECK (leverage > 0 AND leverage <= max_leverage);
-- Create views for common queries
-- Active users view
CREATE VIEW active_users AS
SELECT
id, username, email, first_name, last_name, role,
last_login, created_at, two_factor_enabled
FROM users
WHERE status = 'active';
-- Account summary view
CREATE VIEW account_summary AS
SELECT
a.id, a.account_number, a.account_name, a.account_type,
u.username, u.first_name, u.last_name,
a.current_balance, a.available_balance, a.equity,
a.leverage, a.status, a.is_demo,
COUNT(p.id) as position_count,
COUNT(o.id) as open_orders
FROM accounts a
JOIN users u ON a.user_id = u.id
LEFT JOIN positions p ON a.id::text = p.account_id AND p.quantity != 0
LEFT JOIN orders o ON a.id::text = o.account_id AND o.status IN ('pending', 'partial')
GROUP BY a.id, u.username, u.first_name, u.last_name;
-- Add comments for documentation
COMMENT ON TABLE users IS 'Comprehensive user management for HFT trading systems';
COMMENT ON TABLE accounts IS 'Trading accounts with real-time balance tracking';
COMMENT ON TABLE api_keys IS 'API keys for programmatic trading access';
COMMENT ON TABLE user_sessions IS 'Active user sessions for security tracking';
COMMENT ON TABLE brokers IS 'External broker connection configurations';
COMMENT ON TABLE compliance_profiles IS 'Regulatory compliance requirements';
COMMENT ON FUNCTION create_user IS 'Create new user with encrypted password and audit trail';
COMMENT ON FUNCTION authenticate_user IS 'Authenticate user and create session with security logging';
COMMENT ON FUNCTION validate_session IS 'Validate active session and update last used timestamp';
COMMENT ON FUNCTION create_trading_account IS 'Create new trading account with permissions';

View File

@@ -1,508 +0,0 @@
-- Migration 005: Advanced Risk Management and Regulatory Compliance
-- This migration creates comprehensive risk management for institutional HFT trading
-- Risk limits table - comprehensive limit management
CREATE TABLE IF NOT EXISTS risk_limits (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
limit_type VARCHAR(50) NOT NULL, -- 'position_size', 'daily_loss', 'exposure', 'concentration', 'var', 'leverage'
limit_scope VARCHAR(20) NOT NULL DEFAULT 'account', -- 'account', 'user', 'symbol', 'sector', 'strategy'
symbol VARCHAR(32), -- NULL for portfolio-level limits
strategy_name VARCHAR(100), -- NULL for general limits
sector VARCHAR(50), -- NULL for non-sector limits
limit_value DECIMAL(20, 8) NOT NULL,
warning_threshold DECIMAL(5, 4) DEFAULT 0.80, -- Warn at 80% of limit
breach_action VARCHAR(50) NOT NULL DEFAULT 'alert', -- 'alert', 'block', 'reduce', 'liquidate'
time_window VARCHAR(20), -- '1m', '5m', '1h', '1d', 'rolling' - NULL for static limits
is_active BOOLEAN NOT NULL DEFAULT true,
priority INTEGER NOT NULL DEFAULT 100, -- Higher number = higher priority
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
metadata JSONB
);
-- Risk limit breaches - audit trail
CREATE TABLE IF NOT EXISTS risk_limit_breaches (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
risk_limit_id UUID NOT NULL REFERENCES risk_limits(id),
account_id UUID,
user_id UUID,
symbol VARCHAR(32),
breach_value DECIMAL(20, 8) NOT NULL,
limit_value DECIMAL(20, 8) NOT NULL,
breach_percentage DECIMAL(5, 4) NOT NULL,
severity VARCHAR(20) NOT NULL CHECK (severity IN ('warning', 'breach', 'critical')),
action_taken VARCHAR(100),
resolution_status VARCHAR(20) NOT NULL DEFAULT 'open' CHECK (resolution_status IN ('open', 'acknowledged', 'resolved', 'false_positive')),
resolved_at TIMESTAMP WITH TIME ZONE,
resolved_by UUID REFERENCES users(id),
breach_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
detected_by VARCHAR(50) NOT NULL, -- 'system', 'manual', 'external'
correlation_id UUID, -- Group related breaches
metadata JSONB
);
-- Trading strategies table - strategy definitions
CREATE TABLE IF NOT EXISTS trading_strategies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
strategy_name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
strategy_type VARCHAR(50) NOT NULL, -- 'trend_following', 'mean_reversion', 'arbitrage', 'market_making'
algorithm_version VARCHAR(20),
parameters JSONB NOT NULL DEFAULT '{}'::jsonb,
risk_parameters JSONB NOT NULL DEFAULT '{}'::jsonb,
symbols TEXT[], -- Supported symbols
timeframes TEXT[], -- Supported timeframes
min_account_balance DECIMAL(20, 8) DEFAULT 0,
max_position_size DECIMAL(20, 8),
max_daily_trades INTEGER,
is_active BOOLEAN NOT NULL DEFAULT true,
is_paper_only BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
metadata JSONB
);
-- Strategy assignments - link strategies to accounts
CREATE TABLE IF NOT EXISTS strategy_assignments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
strategy_id UUID NOT NULL REFERENCES trading_strategies(id) ON DELETE CASCADE,
allocation DECIMAL(5, 4) NOT NULL DEFAULT 1.0, -- Percentage of account allocated (0.0-1.0)
custom_parameters JSONB DEFAULT '{}'::jsonb,
risk_multiplier DECIMAL(5, 4) DEFAULT 1.0, -- Risk scaling factor
is_active BOOLEAN NOT NULL DEFAULT true,
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
stopped_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
metadata JSONB,
UNIQUE(account_id, strategy_id)
);
-- VaR calculations table - Value at Risk tracking
CREATE TABLE IF NOT EXISTS var_calculations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
calculation_date DATE NOT NULL,
confidence_level DECIMAL(5, 4) NOT NULL, -- 0.95, 0.99, etc.
time_horizon INTEGER NOT NULL, -- Days
var_amount DECIMAL(20, 8) NOT NULL,
expected_shortfall DECIMAL(20, 8), -- Conditional VaR
methodology VARCHAR(50) NOT NULL, -- 'historical', 'parametric', 'monte_carlo'
portfolio_value DECIMAL(20, 8) NOT NULL,
var_percentage DECIMAL(10, 6) NOT NULL,
calculation_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
model_parameters JSONB,
positions_snapshot JSONB, -- Snapshot of positions used
market_data_window JSONB, -- Time window of market data used
metadata JSONB,
UNIQUE(account_id, calculation_date, confidence_level, time_horizon)
);
-- Stress tests table - scenario analysis
CREATE TABLE IF NOT EXISTS stress_tests (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
test_name VARCHAR(100) NOT NULL,
description TEXT,
scenario_type VARCHAR(50) NOT NULL, -- 'historical', 'hypothetical', 'regulatory'
scenario_parameters JSONB NOT NULL,
account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
test_date DATE NOT NULL,
portfolio_value_before DECIMAL(20, 8) NOT NULL,
portfolio_value_after DECIMAL(20, 8) NOT NULL,
loss_amount DECIMAL(20, 8) NOT NULL,
loss_percentage DECIMAL(10, 6) NOT NULL,
worst_position JSONB, -- Position with worst performance
test_duration_ms INTEGER,
passed_regulatory BOOLEAN,
regulatory_threshold DECIMAL(20, 8),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
metadata JSONB
);
-- Regulatory reports table - compliance reporting
CREATE TABLE IF NOT EXISTS regulatory_reports (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
report_type VARCHAR(50) NOT NULL, -- 'daily_risk', 'var_breach', 'large_trader', 'position_limit'
jurisdiction VARCHAR(10) NOT NULL,
regulator VARCHAR(50) NOT NULL, -- 'CFTC', 'SEC', 'FCA', 'ESMA'
reporting_period_start DATE NOT NULL,
reporting_period_end DATE NOT NULL,
account_id UUID REFERENCES accounts(id),
user_id UUID REFERENCES users(id),
report_data JSONB NOT NULL,
file_path TEXT, -- Path to generated report file
submission_id VARCHAR(100), -- Regulator's submission ID
status VARCHAR(20) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'generated', 'submitted', 'acknowledged', 'rejected')),
due_date DATE,
submitted_at TIMESTAMP WITH TIME ZONE,
acknowledged_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
metadata JSONB
);
-- Trade surveillance alerts - monitoring suspicious activity
CREATE TABLE IF NOT EXISTS surveillance_alerts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
alert_type VARCHAR(50) NOT NULL, -- 'unusual_volume', 'price_manipulation', 'layering', 'spoofing', 'wash_trading'
severity VARCHAR(20) NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')),
account_id UUID REFERENCES accounts(id),
user_id UUID REFERENCES users(id),
symbol VARCHAR(32),
strategy_name VARCHAR(100),
trigger_condition TEXT NOT NULL,
detected_pattern JSONB NOT NULL,
related_orders UUID[], -- Array of order IDs
related_trades UUID[], -- Array of fill IDs
score DECIMAL(5, 2), -- Alert confidence score 0-100
false_positive_probability DECIMAL(5, 4), -- 0.0-1.0
status VARCHAR(20) NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'investigating', 'closed', 'escalated')),
assigned_to UUID REFERENCES users(id),
resolution TEXT,
detected_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP WITH TIME ZONE,
escalated_at TIMESTAMP WITH TIME ZONE,
metadata JSONB
);
-- Market data quality checks
CREATE TABLE IF NOT EXISTS market_data_quality (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
symbol VARCHAR(32) NOT NULL,
data_source VARCHAR(50) NOT NULL,
quality_check_type VARCHAR(50) NOT NULL, -- 'stale_data', 'outlier_price', 'missing_data', 'sequence_gap'
check_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
severity VARCHAR(20) NOT NULL CHECK (severity IN ('info', 'warning', 'error', 'critical')),
description TEXT NOT NULL,
affected_data JSONB,
resolution_action VARCHAR(100),
is_resolved BOOLEAN NOT NULL DEFAULT false,
resolved_at TIMESTAMP WITH TIME ZONE,
impact_assessment TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
metadata JSONB
);
-- Create optimized indexes for HFT performance
-- Risk limits indexes
CREATE INDEX IF NOT EXISTS idx_risk_limits_account_type ON risk_limits(account_id, limit_type);
CREATE INDEX IF NOT EXISTS idx_risk_limits_symbol ON risk_limits(symbol) WHERE symbol IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_risk_limits_active ON risk_limits(is_active, priority DESC);
CREATE INDEX IF NOT EXISTS idx_risk_limits_strategy ON risk_limits(strategy_name) WHERE strategy_name IS NOT NULL;
-- Risk limit breaches indexes
CREATE INDEX IF NOT EXISTS idx_risk_breaches_limit_time ON risk_limit_breaches(risk_limit_id, breach_time DESC);
CREATE INDEX IF NOT EXISTS idx_risk_breaches_account ON risk_limit_breaches(account_id, breach_time DESC);
CREATE INDEX IF NOT EXISTS idx_risk_breaches_severity ON risk_limit_breaches(severity, resolution_status);
CREATE INDEX IF NOT EXISTS idx_risk_breaches_correlation ON risk_limit_breaches(correlation_id) WHERE correlation_id IS NOT NULL;
-- Trading strategies indexes
CREATE INDEX IF NOT EXISTS idx_strategies_name ON trading_strategies(strategy_name);
CREATE INDEX IF NOT EXISTS idx_strategies_type ON trading_strategies(strategy_type);
CREATE INDEX IF NOT EXISTS idx_strategies_active ON trading_strategies(is_active);
-- Strategy assignments indexes
CREATE INDEX IF NOT EXISTS idx_strategy_assignments_account ON strategy_assignments(account_id, is_active);
CREATE INDEX IF NOT EXISTS idx_strategy_assignments_strategy ON strategy_assignments(strategy_id, is_active);
-- VaR calculations indexes
CREATE INDEX IF NOT EXISTS idx_var_calculations_account_date ON var_calculations(account_id, calculation_date DESC);
CREATE INDEX IF NOT EXISTS idx_var_calculations_date ON var_calculations(calculation_date DESC);
-- Stress tests indexes
CREATE INDEX IF NOT EXISTS idx_stress_tests_account_date ON stress_tests(account_id, test_date DESC);
CREATE INDEX IF NOT EXISTS idx_stress_tests_type ON stress_tests(scenario_type);
-- Regulatory reports indexes
CREATE INDEX IF NOT EXISTS idx_regulatory_reports_type_period ON regulatory_reports(report_type, reporting_period_start DESC);
CREATE INDEX IF NOT EXISTS idx_regulatory_reports_jurisdiction ON regulatory_reports(jurisdiction, status);
CREATE INDEX IF NOT EXISTS idx_regulatory_reports_due_date ON regulatory_reports(due_date) WHERE status IN ('draft', 'generated');
-- Surveillance alerts indexes
CREATE INDEX IF NOT EXISTS idx_surveillance_alerts_account ON surveillance_alerts(account_id, detected_at DESC);
CREATE INDEX IF NOT EXISTS idx_surveillance_alerts_type ON surveillance_alerts(alert_type, severity);
CREATE INDEX IF NOT EXISTS idx_surveillance_alerts_status ON surveillance_alerts(status, assigned_to);
CREATE INDEX IF NOT EXISTS idx_surveillance_alerts_symbol ON surveillance_alerts(symbol, detected_at DESC) WHERE symbol IS NOT NULL;
-- Market data quality indexes
CREATE INDEX IF NOT EXISTS idx_market_data_quality_symbol ON market_data_quality(symbol, check_timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_market_data_quality_source ON market_data_quality(data_source, severity);
-- Create triggers for automatic updates
CREATE TRIGGER trigger_risk_limits_updated_at
BEFORE UPDATE ON risk_limits
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_strategies_updated_at
BEFORE UPDATE ON trading_strategies
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_strategy_assignments_updated_at
BEFORE UPDATE ON strategy_assignments
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_regulatory_reports_updated_at
BEFORE UPDATE ON regulatory_reports
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Create advanced risk management functions
-- Function to check risk limits before trade
CREATE OR REPLACE FUNCTION check_risk_limits_before_trade(
p_account_id UUID,
p_symbol VARCHAR(32),
p_side VARCHAR(10), -- 'buy' or 'sell'
p_quantity BIGINT,
p_price BIGINT
)
RETURNS TABLE(
can_trade BOOLEAN,
violated_limits JSONB,
warnings JSONB
) AS $$
DECLARE
v_current_position BIGINT := 0;
v_new_position BIGINT;
v_trade_value DECIMAL(20, 8);
v_violations JSONB := '[]'::jsonb;
v_warnings JSONB := '[]'::jsonb;
v_limit RECORD;
v_current_exposure DECIMAL(20, 8);
v_account_balance DECIMAL(20, 8);
BEGIN
-- Get current position
SELECT COALESCE(quantity, 0) INTO v_current_position
FROM positions
WHERE account_id = p_account_id::text AND symbol = p_symbol;
-- Calculate new position
v_new_position := v_current_position +
CASE WHEN p_side = 'buy' THEN p_quantity ELSE -p_quantity END;
-- Calculate trade value
v_trade_value := (p_quantity * p_price) / 100.0;
-- Get account balance
SELECT current_balance INTO v_account_balance
FROM accounts WHERE id = p_account_id;
-- Check all active risk limits
FOR v_limit IN
SELECT * FROM risk_limits
WHERE is_active = true
AND (account_id = p_account_id OR account_id IS NULL)
AND (symbol = p_symbol OR symbol IS NULL)
ORDER BY priority DESC
LOOP
CASE v_limit.limit_type
WHEN 'position_size' THEN
IF ABS(v_new_position) > v_limit.limit_value THEN
v_violations := v_violations || jsonb_build_object(
'limit_id', v_limit.id,
'limit_type', 'position_size',
'current_value', ABS(v_new_position),
'limit_value', v_limit.limit_value
);
ELSIF ABS(v_new_position) > (v_limit.limit_value * v_limit.warning_threshold) THEN
v_warnings := v_warnings || jsonb_build_object(
'limit_id', v_limit.id,
'limit_type', 'position_size',
'current_value', ABS(v_new_position),
'threshold', v_limit.limit_value * v_limit.warning_threshold
);
END IF;
WHEN 'exposure' THEN
-- Calculate current exposure (simplified)
SELECT COALESCE(SUM(ABS(quantity * last_price) / 100.0), 0) + v_trade_value
INTO v_current_exposure
FROM positions p
WHERE p.account_id = p_account_id::text;
IF v_current_exposure > v_limit.limit_value THEN
v_violations := v_violations || jsonb_build_object(
'limit_id', v_limit.id,
'limit_type', 'exposure',
'current_value', v_current_exposure,
'limit_value', v_limit.limit_value
);
END IF;
WHEN 'leverage' THEN
IF v_current_exposure / v_account_balance > v_limit.limit_value THEN
v_violations := v_violations || jsonb_build_object(
'limit_id', v_limit.id,
'limit_type', 'leverage',
'current_value', v_current_exposure / v_account_balance,
'limit_value', v_limit.limit_value
);
END IF;
END CASE;
END LOOP;
-- Return results
RETURN QUERY SELECT
(jsonb_array_length(v_violations) = 0),
v_violations,
v_warnings;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to calculate portfolio VaR
CREATE OR REPLACE FUNCTION calculate_portfolio_var(
p_account_id UUID,
p_confidence_level DECIMAL(5, 4) DEFAULT 0.95,
p_time_horizon INTEGER DEFAULT 1
)
RETURNS DECIMAL(20, 8) AS $$
DECLARE
v_portfolio_value DECIMAL(20, 8) := 0;
v_var_amount DECIMAL(20, 8) := 0;
v_volatility DECIMAL(10, 6) := 0.02; -- Default 2% daily volatility
v_z_score DECIMAL(10, 6);
BEGIN
-- Get portfolio value
SELECT COALESCE(SUM(ABS(quantity * last_price) / 100.0), 0)
INTO v_portfolio_value
FROM positions
WHERE account_id = p_account_id::text AND quantity != 0;
-- Calculate Z-score for confidence level
v_z_score := CASE
WHEN p_confidence_level >= 0.99 THEN 2.326
WHEN p_confidence_level >= 0.95 THEN 1.645
ELSE 1.282
END;
-- Simple VaR calculation (can be enhanced with historical data)
v_var_amount := v_portfolio_value * v_volatility * v_z_score * SQRT(p_time_horizon);
-- Store calculation
INSERT INTO var_calculations (
account_id, calculation_date, confidence_level, time_horizon,
var_amount, methodology, portfolio_value, var_percentage
) VALUES (
p_account_id, CURRENT_DATE, p_confidence_level, p_time_horizon,
v_var_amount, 'parametric', v_portfolio_value,
CASE WHEN v_portfolio_value > 0 THEN v_var_amount / v_portfolio_value ELSE 0 END
) ON CONFLICT (account_id, calculation_date, confidence_level, time_horizon)
DO UPDATE SET
var_amount = EXCLUDED.var_amount,
portfolio_value = EXCLUDED.portfolio_value,
var_percentage = EXCLUDED.var_percentage,
calculation_time = NOW();
RETURN v_var_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to detect layering pattern
CREATE OR REPLACE FUNCTION detect_layering_pattern(
p_account_id UUID,
p_symbol VARCHAR(32),
p_time_window INTERVAL DEFAULT '5 minutes'
)
RETURNS BOOLEAN AS $$
DECLARE
v_order_count INTEGER;
v_cancel_ratio DECIMAL(5, 4);
v_pattern_detected BOOLEAN := false;
BEGIN
-- Count orders and cancellations in time window
SELECT
COUNT(*),
COUNT(*) FILTER (WHERE status = 'cancelled')::DECIMAL / NULLIF(COUNT(*), 0)
INTO v_order_count, v_cancel_ratio
FROM orders
WHERE account_id = p_account_id::text
AND symbol = p_symbol
AND created_at >= NOW() - p_time_window;
-- Detect pattern: high number of orders with high cancellation ratio
IF v_order_count >= 20 AND v_cancel_ratio >= 0.80 THEN
v_pattern_detected := true;
-- Create surveillance alert
INSERT INTO surveillance_alerts (
alert_type, severity, account_id, symbol, trigger_condition,
detected_pattern, score
) VALUES (
'layering', 'high', p_account_id, p_symbol,
'High order count with excessive cancellation ratio',
jsonb_build_object(
'order_count', v_order_count,
'cancel_ratio', v_cancel_ratio,
'time_window', p_time_window::text
),
85.0
);
END IF;
RETURN v_pattern_detected;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create materialized views for performance
-- Risk exposure summary
CREATE MATERIALIZED VIEW risk_exposure_summary AS
SELECT
a.id as account_id,
a.account_number,
u.username,
COUNT(p.id) as position_count,
COALESCE(SUM(ABS(p.quantity * p.last_price) / 100.0), 0) as total_exposure,
COALESCE(SUM(p.unrealized_pnl) / 100.0, 0) as unrealized_pnl,
COALESCE(MAX(var.var_amount), 0) as latest_var,
COUNT(rb.id) as active_breaches
FROM accounts a
JOIN users u ON a.user_id = u.id
LEFT JOIN positions p ON a.id::text = p.account_id AND p.quantity != 0
LEFT JOIN var_calculations var ON a.id = var.account_id AND var.calculation_date = CURRENT_DATE
LEFT JOIN risk_limit_breaches rb ON a.id = rb.account_id AND rb.resolution_status = 'open'
GROUP BY a.id, a.account_number, u.username;
-- Create unique index on materialized view
CREATE UNIQUE INDEX idx_risk_exposure_summary_account_id
ON risk_exposure_summary(account_id);
-- Add constraints
ALTER TABLE risk_limits ADD CONSTRAINT check_warning_threshold
CHECK (warning_threshold > 0 AND warning_threshold <= 1.0);
ALTER TABLE risk_limits ADD CONSTRAINT check_priority
CHECK (priority > 0);
ALTER TABLE var_calculations ADD CONSTRAINT check_confidence_level
CHECK (confidence_level > 0 AND confidence_level < 1.0);
ALTER TABLE strategy_assignments ADD CONSTRAINT check_allocation
CHECK (allocation >= 0 AND allocation <= 1.0);
-- Add comments for documentation
COMMENT ON TABLE risk_limits IS 'Comprehensive risk limit definitions with dynamic thresholds';
COMMENT ON TABLE risk_limit_breaches IS 'Audit trail of all risk limit violations';
COMMENT ON TABLE trading_strategies IS 'Trading strategy definitions and parameters';
COMMENT ON TABLE var_calculations IS 'Value at Risk calculations with multiple methodologies';
COMMENT ON TABLE stress_tests IS 'Stress testing scenarios and results';
COMMENT ON TABLE surveillance_alerts IS 'Trade surveillance and market abuse detection';
COMMENT ON TABLE regulatory_reports IS 'Regulatory compliance reporting and submissions';
COMMENT ON FUNCTION check_risk_limits_before_trade IS 'Pre-trade risk validation with violation detection';
COMMENT ON FUNCTION calculate_portfolio_var IS 'Portfolio Value at Risk calculation and storage';
COMMENT ON FUNCTION detect_layering_pattern IS 'Market abuse pattern detection for layering/spoofing';

View File

@@ -1,56 +0,0 @@
-- Drop Performance-Optimized Indexes for HFT Trading System
-- =========================================================
-- Removes specialized indexes for rollback scenarios
-- Drop monitoring functions
DROP FUNCTION IF EXISTS get_index_sizes();
DROP FUNCTION IF EXISTS get_index_usage_stats();
-- Drop GIN indexes
DROP INDEX IF EXISTS idx_fills_metadata_gin;
DROP INDEX IF EXISTS idx_orders_metadata_gin;
-- Drop expression indexes
DROP INDEX IF EXISTS idx_orders_remaining_qty;
DROP INDEX IF EXISTS idx_orders_value;
-- Drop partial indexes for hot data
DROP INDEX IF EXISTS idx_fills_recent;
DROP INDEX IF EXISTS idx_orders_recent;
-- Drop performance metrics indexes
DROP INDEX IF EXISTS idx_performance_metrics_component_timestamp;
-- Drop audit logs indexes
DROP INDEX IF EXISTS idx_audit_logs_entity_timestamp;
DROP INDEX IF EXISTS idx_audit_logs_event_timestamp;
DROP INDEX IF EXISTS idx_audit_logs_timestamp_desc;
-- Drop risk metrics indexes
DROP INDEX IF EXISTS idx_risk_metrics_account_metric_timestamp;
DROP INDEX IF EXISTS idx_risk_metrics_severity_timestamp;
-- Drop bars indexes
DROP INDEX IF EXISTS idx_bars_timestamp;
DROP INDEX IF EXISTS idx_bars_symbol_timeframe_timestamp;
-- Drop market data indexes (not concurrent for partitioned tables)
DROP INDEX IF EXISTS idx_market_data_timestamp;
DROP INDEX IF EXISTS idx_market_data_symbol_timestamp;
-- Drop positions indexes
DROP INDEX IF EXISTS idx_positions_active;
DROP INDEX IF EXISTS idx_positions_account_symbol;
-- Drop fills indexes
DROP INDEX IF EXISTS idx_fills_execution_time;
DROP INDEX IF EXISTS idx_fills_symbol_execution;
DROP INDEX IF EXISTS idx_fills_order_execution;
-- Drop orders indexes
DROP INDEX IF EXISTS idx_orders_account_created;
DROP INDEX IF EXISTS idx_orders_symbol_created;
DROP INDEX IF EXISTS idx_orders_status_created;
DROP INDEX IF EXISTS idx_orders_client_order_id;
-- Performance-optimized indexes dropped successfully!

View File

@@ -1,202 +0,0 @@
-- Performance-Optimized Indexes for HFT Trading System
-- ====================================================
-- Creates essential indexes for existing tables only
-- === ORDERS TABLE INDEXES ===
-- Critical path: Order lookups by client_order_id (most frequent)
CREATE INDEX IF NOT EXISTS idx_orders_client_order_id
ON orders (client_order_id) WHERE client_order_id IS NOT NULL;
-- Critical path: Order status queries for active orders
CREATE INDEX IF NOT EXISTS idx_orders_status_created
ON orders (status, created_at DESC)
WHERE status IN ('pending', 'partial');
-- Critical path: Symbol-based order queries with time
CREATE INDEX IF NOT EXISTS idx_orders_symbol_created
ON orders (symbol, created_at DESC);
-- Critical path: Account order history
CREATE INDEX IF NOT EXISTS idx_orders_account_created
ON orders (account_id, created_at DESC) WHERE account_id IS NOT NULL;
-- === FILLS TABLE INDEXES ===
-- Critical path: Order fill lookups
CREATE INDEX IF NOT EXISTS idx_fills_order_execution
ON fills (order_id, execution_time DESC);
-- Critical path: Symbol fill analysis
CREATE INDEX IF NOT EXISTS idx_fills_symbol_execution
ON fills (symbol, execution_time DESC);
-- Critical path: Recent fills
CREATE INDEX IF NOT EXISTS idx_fills_execution_time
ON fills (execution_time DESC);
-- === POSITIONS TABLE INDEXES ===
-- Critical path: Position lookups by account and symbol
CREATE INDEX IF NOT EXISTS idx_positions_account_symbol
ON positions (account_id, symbol) WHERE account_id IS NOT NULL;
-- Critical path: Non-zero positions only
CREATE INDEX IF NOT EXISTS idx_positions_active
ON positions (account_id, symbol)
WHERE quantity != 0 AND account_id IS NOT NULL;
-- === MARKET_DATA TABLE INDEXES ===
-- Note: market_data is partitioned, so we skip CONCURRENTLY
-- Critical path: Recent market data by symbol
CREATE INDEX IF NOT EXISTS idx_market_data_symbol_timestamp
ON market_data (symbol, timestamp DESC);
-- Critical path: Market data time range queries
CREATE INDEX IF NOT EXISTS idx_market_data_timestamp
ON market_data (timestamp DESC);
-- === BARS TABLE INDEXES ===
-- Critical path: Bar data by symbol and timeframe
CREATE INDEX IF NOT EXISTS idx_bars_symbol_timeframe_timestamp
ON bars (symbol, timeframe, timestamp DESC);
-- Critical path: Recent bars
CREATE INDEX IF NOT EXISTS idx_bars_timestamp
ON bars (timestamp DESC);
-- === RISK_METRICS TABLE INDEXES ===
-- These are already created in migration 2, adding complementary ones
-- Critical path: Recent risk metrics by severity
CREATE INDEX IF NOT EXISTS idx_risk_metrics_severity_timestamp
ON risk_metrics (severity, timestamp DESC)
WHERE severity IN ('high', 'critical');
-- Critical path: Account risk monitoring
CREATE INDEX IF NOT EXISTS idx_risk_metrics_account_metric_timestamp
ON risk_metrics (account_id, metric_type, timestamp DESC)
WHERE account_id IS NOT NULL;
-- === AUDIT_LOGS TABLE INDEXES ===
-- Critical path: Recent audit queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp_desc
ON audit_logs (timestamp DESC);
-- Critical path: Event type queries
CREATE INDEX IF NOT EXISTS idx_audit_logs_event_timestamp
ON audit_logs (event_type, timestamp DESC);
-- Critical path: Entity audit trail
CREATE INDEX IF NOT EXISTS idx_audit_logs_entity_timestamp
ON audit_logs (entity_type, entity_id, timestamp DESC);
-- === PERFORMANCE_METRICS TABLE INDEXES ===
-- These are already created in migration 2, adding complementary ones
-- Critical path: Metric analysis by component
CREATE INDEX IF NOT EXISTS idx_performance_metrics_component_timestamp
ON performance_metrics (component, timestamp DESC);
-- === PARTIAL INDEXES FOR HOT DATA ===
-- Only index recent orders (recent data only)
CREATE INDEX IF NOT EXISTS idx_orders_recent
ON orders (symbol, created_at DESC, status);
-- Only index recent fills
CREATE INDEX IF NOT EXISTS idx_fills_recent
ON fills (symbol, execution_time DESC);
-- === EXPRESSION INDEXES ===
-- Index for order value calculations (quantity * price)
CREATE INDEX IF NOT EXISTS idx_orders_value
ON orders ((quantity * price))
WHERE status IN ('pending', 'partial') AND price IS NOT NULL;
-- Index for remaining quantity calculations
CREATE INDEX IF NOT EXISTS idx_orders_remaining_qty
ON orders ((quantity - filled_quantity))
WHERE status = 'partial';
-- === GIN INDEXES FOR JSONB DATA ===
-- Enable fast queries on JSONB metadata where it exists
CREATE INDEX IF NOT EXISTS idx_orders_metadata_gin
ON orders USING GIN (metadata) WHERE metadata IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_fills_metadata_gin
ON fills USING GIN (metadata) WHERE metadata IS NOT NULL;
-- === UPDATE STATISTICS ===
-- Update statistics for better query planning
ANALYZE orders;
ANALYZE fills;
ANALYZE positions;
ANALYZE market_data;
ANALYZE bars;
ANALYZE risk_metrics;
ANALYZE performance_metrics;
ANALYZE audit_logs;
-- === INDEX MONITORING FUNCTIONS ===
-- Create function to monitor index usage
CREATE OR REPLACE FUNCTION get_index_usage_stats()
RETURNS TABLE (
schemaname TEXT,
tablename TEXT,
indexname TEXT,
idx_tup_read BIGINT,
idx_tup_fetch BIGINT,
usage_ratio NUMERIC
)
LANGUAGE SQL AS $$
SELECT
s.schemaname,
s.relname as tablename,
s.indexrelname as indexname,
s.idx_tup_read,
s.idx_tup_fetch,
CASE WHEN s.idx_tup_read = 0
THEN 0
ELSE ROUND(s.idx_tup_fetch::NUMERIC / s.idx_tup_read * 100, 2)
END as usage_ratio
FROM pg_stat_user_indexes s
WHERE s.schemaname = 'public'
ORDER BY s.idx_tup_read DESC;
$$;
-- === INDEX SIZE MONITORING ===
-- Create function to monitor index sizes
CREATE OR REPLACE FUNCTION get_index_sizes()
RETURNS TABLE (
tablename TEXT,
indexname TEXT,
index_size TEXT,
index_size_bytes BIGINT
)
LANGUAGE SQL AS $$
SELECT
t.relname as tablename,
i.relname as indexname,
pg_size_pretty(pg_relation_size(i.oid)) as index_size,
pg_relation_size(i.oid) as index_size_bytes
FROM pg_class i
JOIN pg_index ix ON i.oid = ix.indexrelid
JOIN pg_class t ON ix.indrelid = t.oid
JOIN pg_namespace n ON t.relnamespace = n.oid
WHERE i.relkind = 'i'
AND n.nspname = 'public'
ORDER BY pg_relation_size(i.oid) DESC;
$$;
-- Performance-optimized indexes created successfully!
-- Essential indexes for HFT workloads:
-- - Orders: client_order_id, status, symbol, account lookups
-- - Fills: order_id, symbol, execution_time lookups
-- - Positions: account/symbol combinations, active positions
-- - Market Data: symbol/timestamp combinations
-- - Risk Metrics: severity and account-based monitoring
-- - Audit Logs: timestamp and entity-based queries
-- - Partial indexes for hot data (last 7 days)
-- - Expression indexes for calculated values
-- - GIN indexes for JSONB metadata
--
-- Monitoring functions:
-- - get_index_usage_stats(): Monitor index usage patterns
-- - get_index_sizes(): Monitor index storage requirements

View File

@@ -91,12 +91,13 @@ CREATE TABLE config_settings (
affects TEXT[], -- What this setting affects (services, components)
hot_reload BOOLEAN NOT NULL DEFAULT true, -- Can be changed without restart
restart_required BOOLEAN NOT NULL DEFAULT false, -- Requires service restart
is_active BOOLEAN NOT NULL DEFAULT true, -- Soft delete / deactivation
version INTEGER NOT NULL DEFAULT 1, -- Version for optimistic locking
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by VARCHAR(100) NOT NULL DEFAULT CURRENT_USER,
updated_by VARCHAR(100) NOT NULL DEFAULT CURRENT_USER,
-- Constraints
CONSTRAINT uk_config_settings_key_env UNIQUE (config_key, environment),
CONSTRAINT chk_value_type CHECK (value_type IN ('string', 'number', 'boolean', 'object', 'array', 'null')),
@@ -251,9 +252,6 @@ CREATE INDEX idx_config_locks_locked_by ON config_locks(locked_by);
-- === TRIGGERS FOR NOTIFICATIONS ===
-- Add column for tracking if a config is active
ALTER TABLE config_settings ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;
-- Trigger for config_settings changes
CREATE TRIGGER tr_config_settings_notify
AFTER INSERT OR UPDATE OR DELETE ON config_settings
@@ -457,9 +455,9 @@ ANALYZE config_locks;
-- Create performance monitoring view
CREATE OR REPLACE VIEW config_performance_stats AS
SELECT
SELECT
schemaname,
tablename,
relname as tablename,
n_tup_ins as inserts,
n_tup_upd as updates,
n_tup_del as deletes,
@@ -469,9 +467,9 @@ SELECT
last_autovacuum,
last_analyze,
last_autoanalyze
FROM pg_stat_user_tables
WHERE tablename LIKE 'config_%'
ORDER BY tablename;
FROM pg_stat_user_tables
WHERE relname LIKE 'config_%'
ORDER BY relname;
-- === COMMENTS FOR DOCUMENTATION ===

View File

@@ -130,11 +130,11 @@ FROM config_categories WHERE category_path = 'system.service_discovery';
-- PostgreSQL Configuration
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'max_connections', id, 'database.postgresql', '10'::jsonb, 'number', 'development', 'Maximum database connections per service', ARRAY['database', 'performance'], false, true
SELECT 'postgres_max_connections', id, 'database.postgresql', '10'::jsonb, 'number', 'development', 'Maximum database connections per service', ARRAY['database', 'performance'], false, true
FROM config_categories WHERE category_path = 'database.postgresql';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'max_connections', id, 'database.postgresql', '50'::jsonb, 'number', 'production', 'Production database connection pool size', ARRAY['database', 'performance'], false, true
SELECT 'postgres_max_connections', id, 'database.postgresql', '50'::jsonb, 'number', 'production', 'Production database connection pool size', ARRAY['database', 'performance'], false, true
FROM config_categories WHERE category_path = 'database.postgresql';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
@@ -155,7 +155,7 @@ SELECT 'default_ttl_seconds', id, 'database.redis', '3600'::jsonb, 'number', 'de
FROM config_categories WHERE category_path = 'database.redis';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'max_connections', id, 'database.redis', '20'::jsonb, 'number', 'development', 'Maximum Redis connections per service', ARRAY['cache', 'performance'], false, true
SELECT 'redis_max_connections', id, 'database.redis', '20'::jsonb, 'number', 'development', 'Maximum Redis connections per service', ARRAY['cache', 'performance'], false, true
FROM config_categories WHERE category_path = 'database.redis';
-- === SECURITY CONFIGURATION ===

View File

@@ -116,87 +116,87 @@ CREATE INDEX idx_provider_endpoints_priority ON provider_endpoints(priority);
-- === DATABENTO CONFIGURATION ===
-- Databento API Configuration
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, hot_reload, restart_required)
SELECT 'api_key', id, 'trading.providers.databento', '""'::jsonb, 'string', 'development', 'Databento API key for authentication', ARRAY['databento', 'api', 'authentication'], true, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_api_key', id, 'trading.providers.databento', '""'::jsonb, 'string', 'development', 'Databento API key for authentication', ARRAY['databento', 'api', 'authentication'], true, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, hot_reload, restart_required)
SELECT 'api_key', id, 'trading.providers.databento', '""'::jsonb, 'string', 'production', 'Production Databento API key', ARRAY['databento', 'api', 'authentication'], true, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_api_key', id, 'trading.providers.databento', '""'::jsonb, 'string', 'production', 'Production Databento API key', ARRAY['databento', 'api', 'authentication'], true, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'dataset', id, 'trading.providers.databento', '"XNAS.ITCH"'::jsonb, 'string', 'development', 'Primary Databento dataset for market data', ARRAY['databento', 'dataset', 'market_data'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_dataset', id, 'trading.providers.databento', '"XNAS.ITCH"'::jsonb, 'string', 'development', 'Primary Databento dataset for market data', ARRAY['databento', 'dataset', 'market_data'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'dataset', id, 'trading.providers.databento', '"XNAS.ITCH"'::jsonb, 'string', 'production', 'Production Databento dataset', ARRAY['databento', 'dataset', 'market_data'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_dataset', id, 'trading.providers.databento', '"XNAS.ITCH"'::jsonb, 'string', 'production', 'Production Databento dataset', ARRAY['databento', 'dataset', 'market_data'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'symbols', id, 'trading.providers.databento', '["AAPL", "MSFT", "GOOGL", "TSLA", "AMZN"]'::jsonb, 'array', 'development', 'List of symbols to subscribe to', ARRAY['databento', 'symbols', 'subscription'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_symbols', id, 'trading.providers.databento', '["AAPL", "MSFT", "GOOGL", "TSLA", "AMZN"]'::jsonb, 'array', 'development', 'List of symbols to subscribe to', ARRAY['databento', 'symbols', 'subscription'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'connection_timeout_ms', id, 'trading.providers.databento', '30000'::jsonb, 'number', 'development', 'Connection timeout for Databento API', ARRAY['databento', 'timeout', 'connection'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_connection_timeout_ms', id, 'trading.providers.databento', '30000'::jsonb, 'number', 'development', 'Connection timeout for Databento API', ARRAY['databento', 'timeout', 'connection'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'request_timeout_ms', id, 'trading.providers.databento', '10000'::jsonb, 'number', 'development', 'Request timeout for Databento API calls', ARRAY['databento', 'timeout', 'request'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_request_timeout_ms', id, 'trading.providers.databento', '10000'::jsonb, 'number', 'development', 'Request timeout for Databento API calls', ARRAY['databento', 'timeout', 'request'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'rate_limit_requests_per_second', id, 'trading.providers.databento', '100'::jsonb, 'number', 'development', 'Rate limit for Databento API requests', ARRAY['databento', 'rate_limit'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_rate_limit_rps', id, 'trading.providers.databento', '100'::jsonb, 'number', 'development', 'Rate limit for Databento API requests', ARRAY['databento', 'rate_limit'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'enable_live_data', id, 'trading.providers.databento', 'true'::jsonb, 'boolean', 'development', 'Enable live market data streaming', ARRAY['databento', 'live_data', 'streaming'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_enable_live_data', id, 'trading.providers.databento', 'true'::jsonb, 'boolean', 'development', 'Enable live market data streaming', ARRAY['databento', 'live_data', 'streaming'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'enable_historical_data', id, 'trading.providers.databento', 'true'::jsonb, 'boolean', 'development', 'Enable historical data retrieval', ARRAY['databento', 'historical_data'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'databento_enable_historical_data', id, 'trading.providers.databento', 'true'::jsonb, 'boolean', 'development', 'Enable historical data retrieval', ARRAY['databento', 'historical_data'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.databento';
-- === BENZINGA CONFIGURATION ===
-- Benzinga API Configuration
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, hot_reload, restart_required)
SELECT 'api_key', id, 'trading.providers.benzinga', '""'::jsonb, 'string', 'development', 'Benzinga API key for authentication', ARRAY['benzinga', 'api', 'authentication'], true, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_api_key', id, 'trading.providers.benzinga', '""'::jsonb, 'string', 'development', 'Benzinga API key for authentication', ARRAY['benzinga', 'api', 'authentication'], true, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, hot_reload, restart_required)
SELECT 'api_key', id, 'trading.providers.benzinga', '""'::jsonb, 'string', 'production', 'Production Benzinga API key', ARRAY['benzinga', 'api', 'authentication'], true, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_api_key', id, 'trading.providers.benzinga', '""'::jsonb, 'string', 'production', 'Production Benzinga API key', ARRAY['benzinga', 'api', 'authentication'], true, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'subscription_tier', id, 'trading.providers.benzinga', '"pro"'::jsonb, 'string', 'development', 'Benzinga subscription tier (basic, pro, enterprise)', ARRAY['benzinga', 'subscription', 'tier'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_subscription_tier', id, 'trading.providers.benzinga', '"pro"'::jsonb, 'string', 'development', 'Benzinga subscription tier (basic, pro, enterprise)', ARRAY['benzinga', 'subscription', 'tier'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'subscription_tier', id, 'trading.providers.benzinga', '"enterprise"'::jsonb, 'string', 'production', 'Production Benzinga subscription tier', ARRAY['benzinga', 'subscription', 'tier'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_subscription_tier', id, 'trading.providers.benzinga', '"enterprise"'::jsonb, 'string', 'production', 'Production Benzinga subscription tier', ARRAY['benzinga', 'subscription', 'tier'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'enable_news_feed', id, 'trading.providers.benzinga', 'true'::jsonb, 'boolean', 'development', 'Enable Benzinga news feed', ARRAY['benzinga', 'news', 'feed'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_enable_news_feed', id, 'trading.providers.benzinga', 'true'::jsonb, 'boolean', 'development', 'Enable Benzinga news feed', ARRAY['benzinga', 'news', 'feed'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'enable_analyst_ratings', id, 'trading.providers.benzinga', 'true'::jsonb, 'boolean', 'development', 'Enable analyst ratings data', ARRAY['benzinga', 'analyst_ratings'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_enable_analyst_ratings', id, 'trading.providers.benzinga', 'true'::jsonb, 'boolean', 'development', 'Enable analyst ratings data', ARRAY['benzinga', 'analyst_ratings'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'enable_earnings_data', id, 'trading.providers.benzinga', 'true'::jsonb, 'boolean', 'development', 'Enable earnings calendar and data', ARRAY['benzinga', 'earnings'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_enable_earnings_data', id, 'trading.providers.benzinga', 'true'::jsonb, 'boolean', 'development', 'Enable earnings calendar and data', ARRAY['benzinga', 'earnings'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'connection_timeout_ms', id, 'trading.providers.benzinga', '30000'::jsonb, 'number', 'development', 'Connection timeout for Benzinga API', ARRAY['benzinga', 'timeout', 'connection'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_connection_timeout_ms', id, 'trading.providers.benzinga', '30000'::jsonb, 'number', 'development', 'Connection timeout for Benzinga API', ARRAY['benzinga', 'timeout', 'connection'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'rate_limit_requests_per_minute', id, 'trading.providers.benzinga', '1000'::jsonb, 'number', 'development', 'Rate limit for Benzinga API requests per minute', ARRAY['benzinga', 'rate_limit'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_rate_limit_rpm', id, 'trading.providers.benzinga', '1000'::jsonb, 'number', 'development', 'Rate limit for Benzinga API requests per minute', ARRAY['benzinga', 'rate_limit'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, hot_reload, restart_required)
SELECT 'news_categories', id, 'trading.providers.benzinga', '["earnings", "analyst-ratings", "sec-filings", "mergers-acquisitions"]'::jsonb, 'array', 'development', 'News categories to subscribe to', ARRAY['benzinga', 'news', 'categories'], false, true, false
INSERT INTO config_settings (config_key, category_id, category_path, config_value, value_type, environment, description, tags, is_sensitive, is_system, hot_reload, restart_required)
SELECT 'benzinga_news_categories', id, 'trading.providers.benzinga', '["earnings", "analyst-ratings", "sec-filings", "mergers-acquisitions"]'::jsonb, 'array', 'development', 'News categories to subscribe to', ARRAY['benzinga', 'news', 'categories'], false, false, true, false
FROM config_categories WHERE category_path = 'trading.providers.benzinga';
-- === PROVIDER ENDPOINT CONFIGURATIONS ===
@@ -221,19 +221,19 @@ INSERT INTO provider_endpoints (provider_name, endpoint_type, base_url, websocke
-- Databento subscriptions
INSERT INTO provider_subscriptions (provider_name, subscription_type, dataset, symbols, environment, rate_limit_per_second, max_concurrent_connections, metadata) VALUES
('databento', 'equities_l1', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'development', 100, 2, '{"schema": "mbo", "stype_in": "raw_symbol"}'),
('databento', 'equities_l2', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL'], 'development', 50, 1, '{"schema": "mbp-1", "stype_in": "raw_symbol"}'),
('databento', 'equities_l1', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META'], 'production', 500, 5, '{"schema": "mbo", "stype_in": "raw_symbol"}'),
('databento', 'equities_l2', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'production', 200, 3, '{"schema": "mbp-1", "stype_in": "raw_symbol"}');
('databento', 'equities_l1', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'development', 100, 2, '{"schema": "mbo", "stype_in": "raw_symbol"}'::jsonb),
('databento', 'equities_l2', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL'], 'development', 50, 1, '{"schema": "mbp-1", "stype_in": "raw_symbol"}'::jsonb),
('databento', 'equities_l1', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META'], 'production', 500, 5, '{"schema": "mbo", "stype_in": "raw_symbol"}'::jsonb),
('databento', 'equities_l2', 'XNAS.ITCH', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'production', 200, 3, '{"schema": "mbp-1", "stype_in": "raw_symbol"}'::jsonb);
-- Benzinga subscriptions
INSERT INTO provider_subscriptions (provider_name, subscription_type, dataset, symbols, environment, rate_limit_per_second, max_concurrent_connections, metadata) VALUES
('benzinga', 'news_feed', 'general', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'development', 10, 1, '{"channels": ["news", "analyst-ratings"]}'),
('benzinga', 'earnings_calendar', 'earnings', NULL, 'development', 5, 1, '{"importance": "high"}'),
('benzinga', 'analyst_ratings', 'ratings', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'development', 5, 1, '{"rating_type": ["Upgrade", "Downgrade", "Initiates", "Reiterates"]}'),
('benzinga', 'news_feed', 'general', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META'], 'production', 50, 3, '{"channels": ["news", "analyst-ratings", "sec-filings"]}'),
('benzinga', 'earnings_calendar', 'earnings', NULL, 'production', 20, 1, '{"importance": "high"}'),
('benzinga', 'analyst_ratings', 'ratings', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META'], 'production', 20, 1, '{"rating_type": ["Upgrade", "Downgrade", "Initiates", "Reiterates"]}');
('benzinga', 'news_feed', 'general', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'development', 10, 1, '{"channels": ["news", "analyst-ratings"]}'::jsonb),
('benzinga', 'earnings_calendar', 'earnings', NULL, 'development', 5, 1, '{"importance": "high"}'::jsonb),
('benzinga', 'analyst_ratings', 'ratings', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN'], 'development', 5, 1, '{"rating_type": ["Upgrade", "Downgrade", "Initiates", "Reiterates"]}'::jsonb),
('benzinga', 'news_feed', 'general', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META'], 'production', 50, 3, '{"channels": ["news", "analyst-ratings", "sec-filings"]}'::jsonb),
('benzinga', 'earnings_calendar', 'earnings', NULL, 'production', 20, 1, '{"importance": "high"}'::jsonb),
('benzinga', 'analyst_ratings', 'ratings', ARRAY['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META'], 'production', 20, 1, '{"rating_type": ["Upgrade", "Downgrade", "Initiates", "Reiterates"]}'::jsonb);
-- === NOTIFICATION TRIGGERS FOR PROVIDER TABLES ===

View File

@@ -67,28 +67,9 @@ END $$;
-- === AUDIT LOG FOR REMOVAL ===
-- Log the removal in config_history for audit purposes
INSERT INTO config_history (
config_setting_id,
config_key,
category_path,
environment,
old_value,
new_value,
change_type,
changed_by,
change_reason
) VALUES (
NULL,
'polygon_cleanup',
'system.migration',
'all',
'{"provider": "polygon", "status": "active"}'::jsonb,
'{"provider": "polygon", "status": "removed"}'::jsonb,
'delete',
'migration_010',
'Migration to dual-provider setup - Polygon provider removed in favor of Databento + Benzinga'
);
-- Note: config_history table requires a valid config_setting_id (NOT NULL constraint)
-- Migration events are tracked in the migration_notes category instead
-- See the INSERT into config_categories below for migration tracking
-- === UPDATE DOCUMENTATION ===

View File

@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS market_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_type VARCHAR(64) NOT NULL,
symbol VARCHAR(32),
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
"timestamp" TIMESTAMP WITH TIME ZONE NOT NULL,
event_data JSONB NOT NULL,
source VARCHAR(64), -- Event source (exchange, internal, etc.)
severity VARCHAR(20) DEFAULT 'info' CHECK (severity IN ('debug', 'info', 'warning', 'error', 'critical')),
@@ -24,11 +24,11 @@ CREATE TABLE IF NOT EXISTS trading_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_type VARCHAR(64) NOT NULL,
symbol VARCHAR(32),
order_id UUID,
"order_id" UUID,
account_id VARCHAR(64),
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
"timestamp" TIMESTAMP WITH TIME ZONE NOT NULL,
event_data JSONB NOT NULL,
correlation_id UUID, -- For tracing related events
"correlation_id" UUID, -- For tracing related events
severity VARCHAR(20) DEFAULT 'info' CHECK (severity IN ('debug', 'info', 'warning', 'error', 'critical')),
processed BOOLEAN DEFAULT FALSE,
processed_at TIMESTAMP WITH TIME ZONE,
@@ -40,7 +40,7 @@ CREATE TABLE IF NOT EXISTS event_processing_stats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
processor_name VARCHAR(128) NOT NULL,
event_type VARCHAR(64) NOT NULL,
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
"timestamp" TIMESTAMP WITH TIME ZONE NOT NULL,
events_processed INTEGER NOT NULL DEFAULT 0,
events_failed INTEGER NOT NULL DEFAULT 0,
average_processing_time_ms DECIMAL(10, 3),
@@ -48,7 +48,7 @@ CREATE TABLE IF NOT EXISTS event_processing_stats (
min_processing_time_ms DECIMAL(10, 3),
total_processing_time_ms BIGINT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE(processor_name, event_type, timestamp)
UNIQUE(processor_name, event_type, "timestamp")
);
-- Configuration table - stores application configuration (if not already exists from config migration)
@@ -135,21 +135,35 @@ CREATE TABLE IF NOT EXISTS var_calculations (
-- Create indexes for optimal query performance
-- Market events indexes
CREATE INDEX IF NOT EXISTS idx_market_events_type_timestamp ON market_events(event_type, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_market_events_symbol_timestamp ON market_events(symbol, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_market_events_timestamp ON market_events(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_market_events_processed ON market_events(processed, timestamp DESC);
-- Market events indexes (only create if table was created by this migration, not migration 001)
DO $$
BEGIN
-- Check if market_events has the "timestamp" column (our schema) vs event_timestamp (migration 001 schema)
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'market_events' AND column_name = 'timestamp') THEN
CREATE INDEX IF NOT EXISTS idx_market_events_type_timestamp ON market_events(event_type, "timestamp" DESC);
CREATE INDEX IF NOT EXISTS idx_market_events_symbol_timestamp ON market_events(symbol, "timestamp" DESC);
CREATE INDEX IF NOT EXISTS idx_market_events_timestamp ON market_events("timestamp" DESC);
CREATE INDEX IF NOT EXISTS idx_market_events_processed ON market_events(processed, "timestamp" DESC);
END IF;
END $$;
-- Trading events indexes
CREATE INDEX IF NOT EXISTS idx_trading_events_type_timestamp ON trading_events(event_type, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_trading_events_symbol_timestamp ON trading_events(symbol, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_trading_events_order_id ON trading_events(order_id);
CREATE INDEX IF NOT EXISTS idx_trading_events_correlation_id ON trading_events(correlation_id);
CREATE INDEX IF NOT EXISTS idx_trading_events_timestamp ON trading_events(timestamp DESC);
-- Trading events indexes (only create if table was created by this migration, not migration 001)
DO $$
BEGIN
-- Check if trading_events has the "timestamp" column (our schema) vs event_timestamp (migration 001 schema)
IF EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'trading_events' AND column_name = 'timestamp') THEN
CREATE INDEX IF NOT EXISTS idx_trading_events_type_timestamp ON trading_events(event_type, "timestamp" DESC);
CREATE INDEX IF NOT EXISTS idx_trading_events_symbol_timestamp ON trading_events(symbol, "timestamp" DESC);
CREATE INDEX IF NOT EXISTS idx_trading_events_order_id ON trading_events("order_id");
CREATE INDEX IF NOT EXISTS idx_trading_events_correlation_id ON trading_events("correlation_id");
CREATE INDEX IF NOT EXISTS idx_trading_events_timestamp ON trading_events("timestamp" DESC);
END IF;
END $$;
-- Event processing stats indexes
CREATE INDEX IF NOT EXISTS idx_event_processing_stats_processor_type ON event_processing_stats(processor_name, event_type, timestamp DESC);
-- Event processing stats indexes (always create as this table is new)
CREATE INDEX IF NOT EXISTS idx_event_processing_stats_processor_type ON event_processing_stats(processor_name, event_type, "timestamp" DESC);
-- Configuration indexes
CREATE INDEX IF NOT EXISTS idx_configuration_category ON configuration(category);

View File

@@ -236,14 +236,14 @@ BEGIN
WHEN NEW.classification = 'FOREX' THEN 'America/New_York'
ELSE 'America/New_York'
END,
CASE
CASE
WHEN NEW.classification = 'CRYPTO' THEN '00:00:00'::TIME
WHEN NEW.classification = 'FOREX' THEN '17:00:00'::TIME
WHEN NEW.classification = 'FOREX' THEN '17:00:00'::TIME -- Sunday 5 PM EST (Forex market opens)
ELSE '09:30:00'::TIME
END,
CASE
CASE
WHEN NEW.classification = 'CRYPTO' THEN '23:59:59'::TIME
WHEN NEW.classification = 'FOREX' THEN '17:00:00'::TIME
WHEN NEW.classification = 'FOREX' THEN '17:00:01'::TIME -- Friday 5:00:01 PM EST (Forex market closes, 1 second after open for weekly cycle)
ELSE '16:00:00'::TIME
END,
CASE

View File

@@ -74,13 +74,12 @@ CREATE OR REPLACE RULE no_delete_audit_events AS
-- FOR VALUES FROM ('2025-10-01') TO ('2025-11-01');
-- Grant permissions to application roles
GRANT SELECT, INSERT ON transaction_audit_events TO authenticated_users;
GRANT USAGE, SELECT ON SEQUENCE transaction_audit_events_id_seq TO authenticated_users;
-- Note: Role 'authenticated_users' needs to be created separately
-- GRANT SELECT, INSERT ON transaction_audit_events TO authenticated_users;
-- GRANT USAGE, SELECT ON SEQUENCE transaction_audit_events_id_seq TO authenticated_users;
-- Table and column comments for documentation
COMMENT ON TABLE transaction_audit_events IS
'Immutable audit trail for all trading system events - SOX and MiFID II compliance. ' ||
'Provides microsecond-precision timestamps, tamper detection, and comprehensive event tracking.';
COMMENT ON TABLE transaction_audit_events IS 'Immutable audit trail for all trading system events - SOX and MiFID II compliance. Provides microsecond-precision timestamps, tamper detection, and comprehensive event tracking.';
COMMENT ON COLUMN transaction_audit_events.event_id IS 'Unique event identifier (UUID-based)';
COMMENT ON COLUMN transaction_audit_events.event_type IS 'Type of audit event (OrderCreated, OrderExecuted, etc.)';

View File

@@ -1,13 +1,19 @@
-- Fix partitioned table constraints for PostgreSQL compliance
-- Unique constraints on partitioned tables must include all partitioning columns
-- Drop the problematic unique index on hft_performance_stats materialized view
DROP INDEX IF EXISTS idx_hft_performance_stats_unique;
-- Only apply fix if hft_performance_stats table exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'hft_performance_stats') THEN
-- Drop the problematic unique index on hft_performance_stats materialized view
DROP INDEX IF EXISTS idx_hft_performance_stats_unique;
-- Recreate the unique index including the partitioning column (minute_bucket)
-- This ensures the constraint includes the partitioning column as required by PostgreSQL
CREATE UNIQUE INDEX idx_hft_performance_stats_unique
ON hft_performance_stats(minute_bucket, metric_type, component);
-- Recreate the unique index including the partitioning column (minute_bucket)
-- This ensures the constraint includes the partitioning column as required by PostgreSQL
CREATE UNIQUE INDEX idx_hft_performance_stats_unique
ON hft_performance_stats(minute_bucket, metric_type, component);
-- Add comment explaining the fix
COMMENT ON INDEX idx_hft_performance_stats_unique IS 'Fixed unique index including partitioning column for PostgreSQL compliance';
-- Add comment explaining the fix
COMMENT ON INDEX idx_hft_performance_stats_unique IS 'Fixed unique index including partitioning column for PostgreSQL compliance';
END IF;
END $$;

View File

@@ -1,468 +0,0 @@
-- Authentication and Security Schema for Foxhunt Trading System
-- Implements comprehensive security for financial trading platform
-- Compliant with SOX, FINRA, and financial industry standards
-- Users table for authentication
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
salt VARCHAR(255) NOT NULL,
first_name VARCHAR(255),
last_name VARCHAR(255),
phone VARCHAR(50),
department VARCHAR(100),
job_title VARCHAR(100),
manager_id UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_login TIMESTAMP WITH TIME ZONE,
failed_login_attempts INTEGER DEFAULT 0,
account_locked_until TIMESTAMP WITH TIME ZONE,
password_changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
must_change_password BOOLEAN DEFAULT FALSE,
two_factor_enabled BOOLEAN DEFAULT FALSE,
two_factor_secret VARCHAR(255),
active BOOLEAN DEFAULT TRUE,
deleted_at TIMESTAMP WITH TIME ZONE,
-- Audit fields
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- Roles table for RBAC
CREATE TABLE IF NOT EXISTS roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
permissions TEXT[], -- JSON array of permissions
parent_role_id UUID REFERENCES roles(id),
resource_constraints JSONB, -- Resource-based constraints
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
active BOOLEAN DEFAULT TRUE,
-- Hierarchy depth for performance
hierarchy_level INTEGER DEFAULT 0,
-- Audit fields
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
-- User role assignments
CREATE TABLE IF NOT EXISTS user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE,
granted_by UUID NOT NULL REFERENCES users(id),
resource_constraints JSONB, -- Additional constraints for this assignment
active BOOLEAN DEFAULT TRUE,
UNIQUE(user_id, role_id)
);
-- Sessions table for session management
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token_hash VARCHAR(255) UNIQUE NOT NULL, -- Hashed session token
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_activity TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
client_ip INET,
user_agent TEXT,
device_fingerprint VARCHAR(255),
active BOOLEAN DEFAULT TRUE,
-- Session metadata
login_method VARCHAR(50), -- password, api_key, certificate
session_type VARCHAR(50) DEFAULT 'web' -- web, api, mobile
);
-- API keys table
CREATE TABLE IF NOT EXISTS api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
key_hash VARCHAR(255) UNIQUE NOT NULL, -- Hashed API key
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permissions TEXT[], -- JSON array of permissions
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_used TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
usage_count BIGINT DEFAULT 0,
rate_limit_override INTEGER, -- Custom rate limit for this key
ip_whitelist INET[], -- Allowed IP addresses
active BOOLEAN DEFAULT TRUE,
revoked_at TIMESTAMP WITH TIME ZONE,
revoked_by UUID REFERENCES users(id),
revoke_reason TEXT,
-- Key metadata
key_type VARCHAR(50) DEFAULT 'standard', -- standard, trading, readonly
scopes TEXT[] -- API scopes this key can access
);
-- Audit log table for compliance
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
event_type VARCHAR(100) NOT NULL,
severity VARCHAR(20) NOT NULL CHECK (severity IN ('info', 'warning', 'error', 'critical')),
user_id UUID REFERENCES users(id),
session_id UUID REFERENCES sessions(id),
api_key_id UUID REFERENCES api_keys(id),
client_ip INET,
user_agent TEXT,
resource VARCHAR(255),
action VARCHAR(100) NOT NULL,
result VARCHAR(100) NOT NULL,
details JSONB,
correlation_id UUID,
request_id UUID,
service_name VARCHAR(100),
service_version VARCHAR(50),
-- Compliance fields
compliance_category VARCHAR(100), -- SOX, FINRA, MiFID, etc.
retention_until TIMESTAMP WITH TIME ZONE,
-- Tamper detection
checksum VARCHAR(255),
previous_log_hash VARCHAR(255)
);
-- Rate limiting buckets
CREATE TABLE IF NOT EXISTS rate_limit_buckets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_identifier VARCHAR(255) NOT NULL,
bucket_type VARCHAR(50) NOT NULL, -- auth, api, trading, market_data
requests JSONB NOT NULL DEFAULT '[]', -- Array of request timestamps
total_requests BIGINT DEFAULT 0,
last_request TIMESTAMP WITH TIME ZONE,
violations INTEGER DEFAULT 0,
blocked_until TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(client_identifier, bucket_type)
);
-- TLS certificates table
CREATE TABLE IF NOT EXISTS certificates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
certificate_type VARCHAR(50) NOT NULL, -- server, client, ca
subject VARCHAR(500) NOT NULL,
issuer VARCHAR(500) NOT NULL,
serial_number VARCHAR(100) NOT NULL,
fingerprint VARCHAR(255) UNIQUE NOT NULL,
not_before TIMESTAMP WITH TIME ZONE NOT NULL,
not_after TIMESTAMP WITH TIME ZONE NOT NULL,
key_algorithm VARCHAR(50) NOT NULL,
key_size INTEGER NOT NULL,
signature_algorithm VARCHAR(100) NOT NULL,
san_dns_names TEXT[],
san_ip_addresses INET[],
certificate_pem TEXT NOT NULL,
private_key_encrypted TEXT, -- Encrypted private key (if stored)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
active BOOLEAN DEFAULT TRUE,
-- Certificate chain relationships
parent_certificate_id UUID REFERENCES certificates(id),
root_ca_id UUID REFERENCES certificates(id)
);
-- Permission cache table for performance
CREATE TABLE IF NOT EXISTS permission_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permissions JSONB NOT NULL,
cached_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
cache_version INTEGER DEFAULT 1,
UNIQUE(user_id, cache_version)
);
-- Compliance violations table
CREATE TABLE IF NOT EXISTS compliance_violations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
violation_type VARCHAR(100) NOT NULL,
severity VARCHAR(20) NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')),
user_id UUID REFERENCES users(id),
description TEXT NOT NULL,
detected_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
resolved_at TIMESTAMP WITH TIME ZONE,
resolved_by UUID REFERENCES users(id),
resolution_notes TEXT,
compliance_framework VARCHAR(100), -- SOX, FINRA, MiFID II, etc.
rule_violated VARCHAR(255),
evidence JSONB,
status VARCHAR(50) DEFAULT 'open' CHECK (status IN ('open', 'investigating', 'resolved', 'false_positive')),
-- Regulatory reporting
reported_to_regulator BOOLEAN DEFAULT FALSE,
regulator_reference VARCHAR(255),
reporting_deadline TIMESTAMP WITH TIME ZONE
);
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_active ON users(active) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login);
CREATE INDEX IF NOT EXISTS idx_roles_name ON roles(name);
CREATE INDEX IF NOT EXISTS idx_roles_active ON roles(active) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_roles_parent ON roles(parent_role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles(role_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_active ON user_roles(active) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_user_roles_expires ON user_roles(expires_at);
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions(active) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity);
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash);
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(active) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_api_keys_expires ON api_keys(expires_at);
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_event_type ON audit_logs(event_type);
CREATE INDEX IF NOT EXISTS idx_audit_logs_severity ON audit_logs(severity);
CREATE INDEX IF NOT EXISTS idx_audit_logs_correlation_id ON audit_logs(correlation_id);
CREATE INDEX IF NOT EXISTS idx_rate_limit_buckets_client ON rate_limit_buckets(client_identifier, bucket_type);
CREATE INDEX IF NOT EXISTS idx_rate_limit_buckets_updated ON rate_limit_buckets(updated_at);
CREATE INDEX IF NOT EXISTS idx_certificates_fingerprint ON certificates(fingerprint);
CREATE INDEX IF NOT EXISTS idx_certificates_not_after ON certificates(not_after);
CREATE INDEX IF NOT EXISTS idx_certificates_active ON certificates(active) WHERE active = TRUE;
CREATE INDEX IF NOT EXISTS idx_permission_cache_user_id ON permission_cache(user_id);
CREATE INDEX IF NOT EXISTS idx_permission_cache_expires ON permission_cache(expires_at);
CREATE INDEX IF NOT EXISTS idx_compliance_violations_user_id ON compliance_violations(user_id);
CREATE INDEX IF NOT EXISTS idx_compliance_violations_status ON compliance_violations(status);
CREATE INDEX IF NOT EXISTS idx_compliance_violations_detected ON compliance_violations(detected_at);
CREATE INDEX IF NOT EXISTS idx_compliance_violations_framework ON compliance_violations(compliance_framework);
-- Triggers for updated_at timestamps
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_roles_updated_at BEFORE UPDATE ON roles
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_certificates_updated_at BEFORE UPDATE ON certificates
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_rate_limit_buckets_updated_at BEFORE UPDATE ON rate_limit_buckets
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Function to clean up expired sessions
CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM sessions
WHERE expires_at < NOW() - INTERVAL '7 days';
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- Function to clean up old audit logs (based on retention policy)
CREATE OR REPLACE FUNCTION cleanup_audit_logs(retention_days INTEGER DEFAULT 2555)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM audit_logs
WHERE timestamp < NOW() - INTERVAL '1 day' * retention_days;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- Function to check password complexity
CREATE OR REPLACE FUNCTION check_password_complexity(password_hash TEXT)
RETURNS BOOLEAN AS $$
BEGIN
-- In production, implement proper password complexity checking
-- This is a placeholder that assumes passwords are already validated
RETURN LENGTH(password_hash) >= 60; -- Assuming bcrypt hash length
END;
$$ LANGUAGE plpgsql;
-- Function to log security events
CREATE OR REPLACE FUNCTION log_security_event(
event_type VARCHAR(100),
user_id UUID,
session_id UUID,
client_ip INET,
details JSONB
)
RETURNS UUID AS $$
DECLARE
log_id UUID;
BEGIN
INSERT INTO audit_logs (
event_type,
severity,
user_id,
session_id,
client_ip,
action,
result,
details,
service_name
) VALUES (
event_type,
'info',
user_id,
session_id,
client_ip,
'security_event',
'logged',
details,
'tli'
) RETURNING id INTO log_id;
RETURN log_id;
END;
$$ LANGUAGE plpgsql;
-- Insert default roles
INSERT INTO roles (id, name, description, permissions, hierarchy_level) VALUES
('00000000-0000-0000-0000-000000000001', 'system_admin', 'System Administrator - Full Access',
ARRAY['system:admin', 'system:config', 'system:user_management', 'system:role_management'], 0),
('00000000-0000-0000-0000-000000000002', 'trader', 'Senior Trader - Full Trading Access',
ARRAY['trade:execute', 'trade:view', 'trade:cancel', 'trade:modify', 'order:place', 'order:cancel', 'order:modify', 'order:view', 'position:view', 'position:close', 'position:modify', 'risk:view', 'market_data:view', 'market_data:subscribe', 'portfolio:view', 'report:view', 'analytics:view', 'api:access', 'ml:signal_view'], 1),
('00000000-0000-0000-0000-000000000003', 'junior_trader', 'Junior Trader - Limited Trading Access',
ARRAY['order:place', 'order:view', 'position:view', 'trade:view', 'risk:view', 'market_data:view', 'portfolio:view', 'report:view', 'ml:signal_view'], 2),
('00000000-0000-0000-0000-000000000004', 'risk_manager', 'Risk Manager - Risk Oversight',
ARRAY['risk:view', 'risk:config', 'risk:override', 'risk:limits', 'risk:drawdown_monitor', 'position:view', 'position:limit', 'trade:view', 'order:view', 'portfolio:view', 'report:view', 'report:generate', 'analytics:view', 'compliance:view', 'audit:view'], 1),
('00000000-0000-0000-0000-000000000005', 'viewer', 'Viewer - Read-only Access',
ARRAY['trade:view', 'order:view', 'position:view', 'risk:view', 'market_data:view', 'portfolio:view', 'report:view', 'analytics:view', 'ml:signal_view'], 3),
('00000000-0000-0000-0000-000000000006', 'api_user', 'API User - Programmatic Access',
ARRAY['api:access', 'market_data:view', 'market_data:subscribe', 'order:place', 'order:view', 'order:cancel', 'position:view', 'trade:view', 'ml:signal_view'], 2)
ON CONFLICT (id) DO NOTHING;
-- Insert default admin user (password should be changed on first login)
-- Default password hash is for 'DefaultAdmin123!' - MUST be changed in production
INSERT INTO users (
id,
username,
email,
password_hash,
salt,
first_name,
last_name,
must_change_password
) VALUES (
'00000000-0000-0000-0000-000000000001',
'admin',
'admin@foxhunt.local',
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/lewZJYHCB7vMNZJdK', -- DefaultAdmin123!
'default_salt_change_in_production',
'System',
'Administrator',
TRUE
) ON CONFLICT (id) DO NOTHING;
-- Assign admin role to default admin user
INSERT INTO user_roles (user_id, role_id, granted_by) VALUES
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001')
ON CONFLICT (user_id, role_id) DO NOTHING;
-- Create view for active user permissions
CREATE OR REPLACE VIEW user_permissions AS
SELECT DISTINCT
u.id as user_id,
u.username,
unnest(r.permissions) as permission,
ur.expires_at as permission_expires_at
FROM users u
JOIN user_roles ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
WHERE u.active = TRUE
AND ur.active = TRUE
AND r.active = TRUE
AND (ur.expires_at IS NULL OR ur.expires_at > NOW());
-- Create view for session summary
CREATE OR REPLACE VIEW active_sessions AS
SELECT
s.id,
s.user_id,
u.username,
s.created_at,
s.last_activity,
s.expires_at,
s.client_ip,
s.session_type,
EXTRACT(EPOCH FROM (s.expires_at - NOW())) as seconds_until_expiry
FROM sessions s
JOIN users u ON s.user_id = u.id
WHERE s.active = TRUE
AND s.expires_at > NOW();
-- Grant permissions to application role (adjust as needed)
-- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO foxhunt_app;
-- GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO foxhunt_app;
-- Add comments for documentation
COMMENT ON TABLE users IS 'User accounts for authentication and authorization';
COMMENT ON TABLE roles IS 'Role definitions for RBAC system';
COMMENT ON TABLE user_roles IS 'User to role assignments with optional expiration';
COMMENT ON TABLE sessions IS 'Active user sessions for session management';
COMMENT ON TABLE api_keys IS 'API keys for programmatic access';
COMMENT ON TABLE audit_logs IS 'Comprehensive audit trail for compliance';
COMMENT ON TABLE rate_limit_buckets IS 'Rate limiting state tracking';
COMMENT ON TABLE certificates IS 'TLS certificate management';
COMMENT ON TABLE permission_cache IS 'Cached user permissions for performance';
COMMENT ON TABLE compliance_violations IS 'Compliance violations tracking and reporting';
COMMENT ON COLUMN users.password_hash IS 'Bcrypt hash of user password';
COMMENT ON COLUMN users.salt IS 'Salt used for password hashing';
COMMENT ON COLUMN users.failed_login_attempts IS 'Count of consecutive failed login attempts';
COMMENT ON COLUMN users.account_locked_until IS 'Account lockout expiration timestamp';
COMMENT ON COLUMN users.two_factor_secret IS 'TOTP secret for 2FA (encrypted)';
COMMENT ON COLUMN audit_logs.checksum IS 'Tamper detection checksum for audit integrity';
COMMENT ON COLUMN audit_logs.previous_log_hash IS 'Hash of previous log entry for chain verification';
COMMENT ON COLUMN audit_logs.retention_until IS 'Data retention deadline for compliance';
COMMENT ON COLUMN api_keys.key_hash IS 'SHA-256 hash of the API key for secure storage';
COMMENT ON COLUMN api_keys.scopes IS 'API scopes this key can access';
COMMENT ON COLUMN api_keys.ip_whitelist IS 'Allowed source IP addresses for this key';
COMMENT ON COLUMN certificates.certificate_pem IS 'PEM-encoded certificate';
COMMENT ON COLUMN certificates.private_key_encrypted IS 'Encrypted private key (if stored)';
COMMENT ON COLUMN certificates.san_dns_names IS 'Subject Alternative Names - DNS names';
COMMENT ON COLUMN certificates.san_ip_addresses IS 'Subject Alternative Names - IP addresses';

View File

@@ -1,741 +0,0 @@
-- 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)
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
BEGIN
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 $$;
-- =======================================================================================
-- 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
DO $$
DECLARE
partition_start DATE;
partition_end DATE;
partition_name TEXT;
i INTEGER;
BEGIN
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 $$;
-- =======================================================================================
-- 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;
BEGIN
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 $$;
-- =======================================================================================
-- 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;
BEGIN
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 $$;
-- =======================================================================================
-- 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;
BEGIN
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 $$;
-- =======================================================================================
-- INDEXES FOR HIGH-PERFORMANCE QUERIES
-- =======================================================================================
-- Trading Events Indexes
CREATE INDEX IF NOT EXISTS idx_trading_events_order_id ON trading_events(order_id, event_timestamp);
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;
CREATE INDEX IF NOT EXISTS idx_trading_events_compliance ON trading_events USING GIN(compliance_flags) WHERE compliance_flags IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_trading_events_risk ON trading_events USING GIN(risk_violations) WHERE risk_violations IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_trading_events_latency ON trading_events(latency_ns) WHERE latency_ns IS NOT NULL;
-- Risk Events Indexes
CREATE INDEX IF NOT EXISTS idx_risk_events_type_severity ON risk_events(risk_type, severity, event_timestamp);
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;
CREATE INDEX IF NOT EXISTS idx_risk_events_resolution ON risk_events(resolution_status, event_timestamp);
CREATE INDEX IF NOT EXISTS idx_risk_events_order ON risk_events(order_id) WHERE order_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_risk_events_breach_amount ON risk_events(breach_amount) WHERE breach_amount 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
CREATE INDEX IF NOT EXISTS idx_system_events_system ON system_events(system_name, service_name, event_timestamp);
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;
CREATE INDEX IF NOT EXISTS idx_system_events_error ON system_events(error_code, event_timestamp) WHERE error_code IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_system_events_latency ON system_events(latency_ns) WHERE latency_ns IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_system_events_incident ON system_events(incident_id) WHERE incident_id 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
CREATE MATERIALIZED VIEW IF NOT EXISTS trading_activity_summary AS
SELECT
date_trunc('hour', event_timestamp) as hour,
symbol,
COUNT(*) as total_events,
COUNT(*) FILTER (WHERE event_type = 'order_created') as orders_created,
COUNT(*) FILTER (WHERE event_type = 'order_filled') as orders_filled,
COUNT(*) FILTER (WHERE event_type = 'order_cancelled') as orders_cancelled,
SUM(quantity) FILTER (WHERE event_type = 'order_created') as total_quantity,
SUM(execution_quantity) FILTER (WHERE event_type = 'order_filled') as filled_quantity,
AVG(latency_ns) FILTER (WHERE latency_ns IS NOT NULL) as avg_latency_ns,
MAX(latency_ns) FILTER (WHERE latency_ns IS NOT NULL) as max_latency_ns
FROM trading_events
WHERE event_timestamp >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY date_trunc('hour', event_timestamp), symbol;
-- Risk events summary
CREATE MATERIALIZED VIEW IF NOT EXISTS risk_events_summary AS
SELECT
date_trunc('day', event_timestamp) as day,
risk_type,
severity,
COUNT(*) as event_count,
COUNT(*) FILTER (WHERE resolution_status = 'resolved') as resolved_count,
AVG(EXTRACT(EPOCH FROM (resolution_timestamp - event_timestamp))/60)
FILTER (WHERE resolution_timestamp IS NOT NULL) as avg_resolution_minutes
FROM risk_events
WHERE event_timestamp >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY date_trunc('day', event_timestamp), risk_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';
COMMENT ON COLUMN trading_events.latency_ns IS 'Processing latency in nanoseconds for HFT performance monitoring';
COMMENT ON COLUMN trading_events.correlation_id IS 'Links related events together for complete transaction tracking';
COMMENT ON COLUMN risk_events.breach_percentage IS 'Percentage by which limit was breached (>1.0 = breach)';
COMMENT ON COLUMN audit_trail.regulatory_impact IS 'Flags changes that have regulatory compliance implications';
COMMENT ON COLUMN ml_signals.confidence IS 'Model confidence in prediction (0.0-1.0)';
COMMENT ON COLUMN system_events.latency_ns IS 'System operation latency in nanoseconds';
-- 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();');