-- 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;