TradingAgents/tradingagents/portfolio/migrations/001_initial_schema.sql

160 lines
7.3 KiB
PL/PgSQL

-- =============================================================================
-- Portfolio Manager Agent — Initial Schema
-- Migration: 001_initial_schema.sql
-- Description: Creates all tables, indexes, and triggers for the portfolio
-- management data layer.
-- Safe to re-run: all statements use IF NOT EXISTS / CREATE OR REPLACE.
-- =============================================================================
-- ---------------------------------------------------------------------------
-- Table: portfolios
-- Purpose: One row per managed portfolio. Tracks cash balance, initial capital,
-- and a pointer to the filesystem report directory.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS portfolios (
portfolio_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
cash NUMERIC(18,4) NOT NULL CHECK (cash >= 0),
initial_cash NUMERIC(18,4) NOT NULL CHECK (initial_cash > 0),
currency CHAR(3) NOT NULL DEFAULT 'USD',
report_path TEXT, -- relative FS path to daily report dir
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE portfolios IS
'One row per managed portfolio. Tracks cash balance and links to filesystem reports.';
COMMENT ON COLUMN portfolios.report_path IS
'Relative path to the daily portfolio report directory, e.g. reports/daily/2026-03-20/portfolio';
COMMENT ON COLUMN portfolios.metadata IS
'Free-form JSONB for agent notes, tags, or strategy parameters.';
-- ---------------------------------------------------------------------------
-- Table: holdings
-- Purpose: Current open positions. One row per (portfolio, ticker). Deleted
-- when shares reach zero — zero-share rows are never stored.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS holdings (
holding_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
portfolio_id UUID NOT NULL REFERENCES portfolios(portfolio_id) ON DELETE CASCADE,
ticker TEXT NOT NULL,
shares NUMERIC(18,6) NOT NULL CHECK (shares > 0),
avg_cost NUMERIC(18,4) NOT NULL CHECK (avg_cost >= 0),
sector TEXT,
industry TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT holdings_portfolio_ticker_unique UNIQUE (portfolio_id, ticker)
);
COMMENT ON TABLE holdings IS
'Open positions. Upserted on BUY (avg-cost update), deleted when fully sold.';
COMMENT ON COLUMN holdings.avg_cost IS
'Weighted-average cost basis per share in portfolio currency.';
-- ---------------------------------------------------------------------------
-- Table: trades
-- Purpose: Immutable append-only log of every mock trade. Never modified.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS trades (
trade_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
portfolio_id UUID NOT NULL REFERENCES portfolios(portfolio_id) ON DELETE CASCADE,
ticker TEXT NOT NULL,
action TEXT NOT NULL CHECK (action IN ('BUY', 'SELL')),
shares NUMERIC(18,6) NOT NULL CHECK (shares > 0),
price NUMERIC(18,4) NOT NULL CHECK (price > 0),
total_value NUMERIC(18,4) NOT NULL CHECK (total_value > 0),
trade_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
rationale TEXT, -- PM agent rationale for this trade
signal_source TEXT, -- 'scanner' | 'holding_review' | 'pm_agent'
metadata JSONB NOT NULL DEFAULT '{}'
);
COMMENT ON TABLE trades IS
'Immutable trade log. Records every mock BUY/SELL with PM rationale.';
COMMENT ON COLUMN trades.rationale IS
'Natural-language reason provided by the Portfolio Manager Agent.';
COMMENT ON COLUMN trades.signal_source IS
'Which sub-system generated the trade signal: scanner, holding_review, or pm_agent.';
-- ---------------------------------------------------------------------------
-- Table: snapshots
-- Purpose: Immutable point-in-time portfolio state. Taken after each trade
-- execution session for performance tracking and time-series analysis.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS snapshots (
snapshot_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
portfolio_id UUID NOT NULL REFERENCES portfolios(portfolio_id) ON DELETE CASCADE,
snapshot_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
total_value NUMERIC(18,4) NOT NULL,
cash NUMERIC(18,4) NOT NULL,
equity_value NUMERIC(18,4) NOT NULL,
num_positions INTEGER NOT NULL CHECK (num_positions >= 0),
holdings_snapshot JSONB NOT NULL DEFAULT '[]', -- serialised List[Holding.to_dict()]
metadata JSONB NOT NULL DEFAULT '{}'
);
COMMENT ON TABLE snapshots IS
'Immutable portfolio snapshots for performance tracking (NAV series).';
COMMENT ON COLUMN snapshots.holdings_snapshot IS
'JSONB array of Holding.to_dict() objects at snapshot time.';
-- ---------------------------------------------------------------------------
-- Indexes
-- ---------------------------------------------------------------------------
-- portfolios: lookup by name (uniqueness enforced at application level)
CREATE INDEX IF NOT EXISTS idx_portfolios_name
ON portfolios (name);
-- holdings: list all holdings for a portfolio (most frequent query)
CREATE INDEX IF NOT EXISTS idx_holdings_portfolio_id
ON holdings (portfolio_id);
-- holdings: fast (portfolio, ticker) point lookup for upserts
CREATE INDEX IF NOT EXISTS idx_holdings_portfolio_ticker
ON holdings (portfolio_id, ticker);
-- trades: list recent trades for a portfolio, newest first
CREATE INDEX IF NOT EXISTS idx_trades_portfolio_id_date
ON trades (portfolio_id, trade_date DESC);
-- trades: filter by ticker within a portfolio
CREATE INDEX IF NOT EXISTS idx_trades_portfolio_ticker
ON trades (portfolio_id, ticker);
-- snapshots: get latest snapshot for a portfolio
CREATE INDEX IF NOT EXISTS idx_snapshots_portfolio_id_date
ON snapshots (portfolio_id, snapshot_date DESC);
-- ---------------------------------------------------------------------------
-- updated_at trigger
-- Purpose: Automatically sets updated_at = NOW() on every UPDATE for mutable
-- tables (portfolios, holdings). Trades and snapshots are immutable.
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to portfolios
CREATE OR REPLACE TRIGGER trg_portfolios_updated_at
BEFORE UPDATE ON portfolios
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Apply to holdings
CREATE OR REPLACE TRIGGER trg_holdings_updated_at
BEFORE UPDATE ON holdings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();