CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Table: stock_transactions -- Stores all buy/sell transactions for the portfolio CREATE TABLE IF NOT EXISTS stock_transactions ( id SERIAL PRIMARY KEY, transaction_id UUID DEFAULT uuid_generate_v4() UNIQUE NOT NULL, symbol VARCHAR(10) NOT NULL, transaction_type VARCHAR(10) NOT NULL CHECK (transaction_type IN ('BUY', 'SELL')), quantity DECIMAL(15, 4) NOT NULL CHECK (quantity > 0), price DECIMAL(15, 4) NOT NULL CHECK (price >= 0), transaction_date DATE NOT NULL, notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Table: portfolio_positions -- Stores current aggregated positions (calculated from transactions) CREATE TABLE IF NOT EXISTS portfolio_positions ( symbol VARCHAR(10) PRIMARY KEY, total_quantity DECIMAL(15, 4) NOT NULL CHECK (total_quantity >= 0), avg_cost_basis DECIMAL(15, 4) NOT NULL CHECK (avg_cost_basis >= 0), first_purchase_date DATE, last_transaction_date DATE, total_invested DECIMAL(15, 2) NOT NULL DEFAULT 0, realized_gains DECIMAL(15, 2) DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Table: portfolio_snapshots -- Stores historical portfolio value snapshots for performance tracking CREATE TABLE IF NOT EXISTS portfolio_snapshots ( id SERIAL PRIMARY KEY, snapshot_date DATE NOT NULL, total_value DECIMAL(15, 2) NOT NULL, total_cost_basis DECIMAL(15, 2) NOT NULL, total_gain_loss DECIMAL(15, 2) NOT NULL, total_gain_loss_pct DECIMAL(10, 4) NOT NULL, num_positions INTEGER NOT NULL, snapshot_data JSONB, -- Store detailed position data created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Table: stock_metadata -- Cache stock information to reduce API calls CREATE TABLE IF NOT EXISTS stock_metadata ( symbol VARCHAR(10) PRIMARY KEY, company_name VARCHAR(255), sector VARCHAR(100), industry VARCHAR(100), market_cap BIGINT, last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Indexes for performance optimization CREATE INDEX IF NOT EXISTS idx_transactions_symbol ON stock_transactions(symbol); CREATE INDEX IF NOT EXISTS idx_transactions_date ON stock_transactions(transaction_date DESC); CREATE INDEX IF NOT EXISTS idx_transactions_type ON stock_transactions(transaction_type); CREATE INDEX IF NOT EXISTS idx_snapshots_date ON portfolio_snapshots(snapshot_date DESC); CREATE INDEX IF NOT EXISTS idx_stock_metadata_sector ON stock_metadata(sector); -- Function to update the updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Triggers to automatically update updated_at CREATE TRIGGER update_stock_transactions_updated_at BEFORE UPDATE ON stock_transactions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_portfolio_positions_updated_at BEFORE UPDATE ON portfolio_positions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_stock_metadata_updated_at BEFORE UPDATE ON stock_metadata FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Function to recalculate portfolio positions -- This ensures positions are always in sync with transactions CREATE OR REPLACE FUNCTION recalculate_position(p_symbol VARCHAR) RETURNS VOID AS $$ DECLARE v_total_quantity DECIMAL(15, 4); v_avg_cost_basis DECIMAL(15, 4); v_total_invested DECIMAL(15, 2); v_realized_gains DECIMAL(15, 2); v_first_purchase DATE; v_last_transaction DATE; BEGIN -- Calculate position from transactions WITH position_calc AS ( SELECT SUM(CASE WHEN transaction_type = 'BUY' THEN quantity ELSE -quantity END) as net_quantity, SUM(CASE WHEN transaction_type = 'BUY' THEN quantity * price ELSE 0 END) as total_cost, SUM(CASE WHEN transaction_type = 'BUY' THEN quantity ELSE 0 END) as total_bought, MIN(CASE WHEN transaction_type = 'BUY' THEN transaction_date END) as first_buy, MAX(transaction_date) as last_trans, SUM(CASE WHEN transaction_type = 'SELL' THEN quantity * price ELSE 0 END) - SUM(CASE WHEN transaction_type = 'SELL' THEN quantity * (SELECT AVG(price) FROM stock_transactions WHERE symbol = p_symbol AND transaction_type = 'BUY') ELSE 0 END) as realized_gain FROM stock_transactions WHERE symbol = p_symbol ) SELECT net_quantity, CASE WHEN total_bought > 0 THEN total_cost / total_bought ELSE 0 END, total_cost, COALESCE(realized_gain, 0), first_buy, last_trans INTO v_total_quantity, v_avg_cost_basis, v_total_invested, v_realized_gains, v_first_purchase, v_last_transaction FROM position_calc; -- Update or delete position IF v_total_quantity > 0 THEN INSERT INTO portfolio_positions ( symbol, total_quantity, avg_cost_basis, total_invested, realized_gains, first_purchase_date, last_transaction_date ) VALUES ( p_symbol, v_total_quantity, v_avg_cost_basis, v_total_invested, v_realized_gains, v_first_purchase, v_last_transaction ) ON CONFLICT (symbol) DO UPDATE SET total_quantity = EXCLUDED.total_quantity, avg_cost_basis = EXCLUDED.avg_cost_basis, total_invested = EXCLUDED.total_invested, realized_gains = EXCLUDED.realized_gains, first_purchase_date = EXCLUDED.first_purchase_date, last_transaction_date = EXCLUDED.last_transaction_date; ELSE -- If position is fully closed, delete it DELETE FROM portfolio_positions WHERE symbol = p_symbol; END IF; END; $$ LANGUAGE plpgsql; -- Trigger to automatically recalculate positions when transactions change CREATE OR REPLACE FUNCTION trigger_recalculate_position() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'DELETE' THEN PERFORM recalculate_position(OLD.symbol); RETURN OLD; ELSE PERFORM recalculate_position(NEW.symbol); RETURN NEW; END IF; END; $$ LANGUAGE plpgsql; CREATE TRIGGER auto_recalculate_position AFTER INSERT OR UPDATE OR DELETE ON stock_transactions FOR EACH ROW EXECUTE FUNCTION trigger_recalculate_position(); -- View: current_portfolio_summary -- Provides a quick overview of the portfolio CREATE OR REPLACE VIEW current_portfolio_summary AS SELECT COUNT(*) as total_positions, SUM(total_quantity) as total_shares, SUM(total_invested) as total_invested, SUM(realized_gains) as total_realized_gains FROM portfolio_positions;