-- ============================================================================ -- SCHEMA: trading -- TABLE: watchlists, watchlist_items -- DESCRIPTION: Listas de seguimiento de simbolos -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026 -- ============================================================================ -- Tabla de Watchlists CREATE TABLE IF NOT EXISTS trading.watchlists ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, -- Informacion name VARCHAR(100) NOT NULL, description TEXT, color VARCHAR(7), -- Color hex para identificacion icon VARCHAR(50), -- Configuracion is_default BOOLEAN NOT NULL DEFAULT FALSE, -- Watchlist por defecto is_public BOOLEAN NOT NULL DEFAULT FALSE, -- Compartida publicamente is_pinned BOOLEAN NOT NULL DEFAULT FALSE, -- Fijada en UI -- Ordenamiento display_order INTEGER NOT NULL DEFAULT 0, sort_by VARCHAR(50) DEFAULT 'symbol', -- 'symbol', 'change', 'volume', 'custom' sort_direction VARCHAR(4) DEFAULT 'asc', -- Columnas visibles visible_columns JSONB DEFAULT '["symbol", "price", "change", "change_percent"]'::JSONB, -- Estadisticas item_count INTEGER NOT NULL DEFAULT 0, -- Compartir share_code VARCHAR(20), -- Codigo para compartir shared_at TIMESTAMPTZ, view_count INTEGER NOT NULL DEFAULT 0, copy_count INTEGER NOT NULL DEFAULT 0, -- Metadata metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT watchlists_unique_name UNIQUE (user_id, name) ); COMMENT ON TABLE trading.watchlists IS 'Listas de seguimiento de simbolos creadas por usuarios'; -- Tabla de Items de Watchlist CREATE TABLE IF NOT EXISTS trading.watchlist_items ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), watchlist_id UUID NOT NULL REFERENCES trading.watchlists(id) ON DELETE CASCADE, symbol_id UUID REFERENCES trading.symbols(id), ticker_id UUID REFERENCES market_data.tickers(id), -- Simbolo symbol VARCHAR(20) NOT NULL, -- Orden personalizado display_order INTEGER NOT NULL DEFAULT 0, -- Notas del usuario notes TEXT, tags VARCHAR(50)[], -- Alertas rapidas alert_price_above DECIMAL(15, 8), alert_price_below DECIMAL(15, 8), alert_enabled BOOLEAN NOT NULL DEFAULT FALSE, -- Precios de referencia del usuario entry_price DECIMAL(15, 8), -- Precio de entrada planeado target_price DECIMAL(15, 8), -- Precio objetivo stop_price DECIMAL(15, 8), -- Stop loss planeado -- Precio actual (cache) current_price DECIMAL(15, 8), price_change DECIMAL(15, 8), price_change_percent DECIMAL(10, 4), price_updated_at TIMESTAMPTZ, -- Metadata metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT watchlist_items_unique UNIQUE (watchlist_id, symbol) ); COMMENT ON TABLE trading.watchlist_items IS 'Simbolos individuales dentro de una watchlist'; -- Indices para watchlists CREATE INDEX IF NOT EXISTS idx_watchlists_tenant ON trading.watchlists(tenant_id); CREATE INDEX IF NOT EXISTS idx_watchlists_user ON trading.watchlists(user_id); CREATE INDEX IF NOT EXISTS idx_watchlists_default ON trading.watchlists(user_id, is_default) WHERE is_default = TRUE; CREATE INDEX IF NOT EXISTS idx_watchlists_public ON trading.watchlists(is_public, view_count DESC) WHERE is_public = TRUE; CREATE INDEX IF NOT EXISTS idx_watchlists_share_code ON trading.watchlists(share_code) WHERE share_code IS NOT NULL; -- Indices para watchlist_items CREATE INDEX IF NOT EXISTS idx_watchlist_items_watchlist ON trading.watchlist_items(watchlist_id, display_order); CREATE INDEX IF NOT EXISTS idx_watchlist_items_symbol ON trading.watchlist_items(symbol); CREATE INDEX IF NOT EXISTS idx_watchlist_items_alerts ON trading.watchlist_items(alert_enabled, symbol) WHERE alert_enabled = TRUE; -- Trigger para updated_at en watchlists DROP TRIGGER IF EXISTS watchlist_updated_at ON trading.watchlists; CREATE TRIGGER watchlist_updated_at BEFORE UPDATE ON trading.watchlists FOR EACH ROW EXECUTE FUNCTION trading.update_trading_timestamp(); -- Trigger para updated_at en watchlist_items DROP TRIGGER IF EXISTS watchlist_item_updated_at ON trading.watchlist_items; CREATE TRIGGER watchlist_item_updated_at BEFORE UPDATE ON trading.watchlist_items FOR EACH ROW EXECUTE FUNCTION trading.update_trading_timestamp(); -- Trigger para actualizar item_count en watchlist CREATE OR REPLACE FUNCTION trading.update_watchlist_count() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN UPDATE trading.watchlists SET item_count = item_count + 1 WHERE id = NEW.watchlist_id; ELSIF TG_OP = 'DELETE' THEN UPDATE trading.watchlists SET item_count = item_count - 1 WHERE id = OLD.watchlist_id; END IF; RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS watchlist_item_count ON trading.watchlist_items; CREATE TRIGGER watchlist_item_count AFTER INSERT OR DELETE ON trading.watchlist_items FOR EACH ROW EXECUTE FUNCTION trading.update_watchlist_count(); -- Trigger para asegurar solo una watchlist default por usuario CREATE OR REPLACE FUNCTION trading.ensure_single_default_watchlist() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_default = TRUE THEN UPDATE trading.watchlists SET is_default = FALSE WHERE user_id = NEW.user_id AND id != NEW.id AND is_default = TRUE; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS watchlist_single_default ON trading.watchlists; CREATE TRIGGER watchlist_single_default BEFORE INSERT OR UPDATE OF is_default ON trading.watchlists FOR EACH ROW WHEN (NEW.is_default = TRUE) EXECUTE FUNCTION trading.ensure_single_default_watchlist(); -- Funcion para copiar watchlist CREATE OR REPLACE FUNCTION trading.copy_watchlist( p_watchlist_id UUID, p_new_user_id UUID, p_new_name VARCHAR(100) DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_source RECORD; v_new_id UUID; v_tenant_id UUID; BEGIN -- Obtener watchlist origen SELECT * INTO v_source FROM trading.watchlists WHERE id = p_watchlist_id; IF v_source IS NULL THEN RAISE EXCEPTION 'Watchlist not found'; END IF; -- Obtener tenant del nuevo usuario SELECT tenant_id INTO v_tenant_id FROM users.users WHERE id = p_new_user_id; -- Crear nueva watchlist INSERT INTO trading.watchlists ( tenant_id, user_id, name, description, color, icon, sort_by, sort_direction, visible_columns ) VALUES ( v_tenant_id, p_new_user_id, COALESCE(p_new_name, v_source.name || ' (Copy)'), v_source.description, v_source.color, v_source.icon, v_source.sort_by, v_source.sort_direction, v_source.visible_columns ) RETURNING id INTO v_new_id; -- Copiar items INSERT INTO trading.watchlist_items ( watchlist_id, symbol_id, ticker_id, symbol, display_order, notes, tags, entry_price, target_price, stop_price ) SELECT v_new_id, symbol_id, ticker_id, symbol, display_order, notes, tags, entry_price, target_price, stop_price FROM trading.watchlist_items WHERE watchlist_id = p_watchlist_id; -- Incrementar contador de copias UPDATE trading.watchlists SET copy_count = copy_count + 1 WHERE id = p_watchlist_id; RETURN v_new_id; END; $$ LANGUAGE plpgsql; -- Vista de watchlists con items CREATE OR REPLACE VIEW trading.v_watchlists_with_items AS SELECT w.id, w.user_id, w.name, w.description, w.color, w.is_default, w.is_pinned, w.item_count, w.display_order, ( SELECT jsonb_agg(jsonb_build_object( 'symbol', wi.symbol, 'current_price', wi.current_price, 'price_change_percent', wi.price_change_percent, 'notes', wi.notes ) ORDER BY wi.display_order) FROM trading.watchlist_items wi WHERE wi.watchlist_id = w.id ) AS items FROM trading.watchlists w ORDER BY w.is_pinned DESC, w.display_order, w.name; -- Vista de items con precios actualizados CREATE OR REPLACE VIEW trading.v_watchlist_items_live AS SELECT wi.id, wi.watchlist_id, wi.symbol, t.name AS symbol_name, t.type AS symbol_type, t.current_bid, t.current_ask, wi.current_price, wi.price_change_percent, wi.entry_price, wi.target_price, wi.stop_price, wi.notes, wi.display_order FROM trading.watchlist_items wi LEFT JOIN market_data.tickers t ON wi.symbol = t.symbol ORDER BY wi.watchlist_id, wi.display_order; -- RLS Policy para multi-tenancy en watchlists ALTER TABLE trading.watchlists ENABLE ROW LEVEL SECURITY; CREATE POLICY watchlists_tenant_isolation ON trading.watchlists FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); CREATE POLICY watchlists_user_isolation ON trading.watchlists FOR ALL USING (user_id = current_setting('app.current_user_id', true)::UUID); -- RLS para watchlist_items (hereda de watchlist via JOIN) ALTER TABLE trading.watchlist_items ENABLE ROW LEVEL SECURITY; CREATE POLICY watchlist_items_via_watchlist ON trading.watchlist_items FOR ALL USING ( watchlist_id IN ( SELECT id FROM trading.watchlists WHERE user_id = current_setting('app.current_user_id', true)::UUID ) ); -- Grants GRANT SELECT, INSERT, UPDATE, DELETE ON trading.watchlists TO trading_app; GRANT SELECT, INSERT, UPDATE, DELETE ON trading.watchlist_items TO trading_app; GRANT SELECT ON trading.watchlists TO trading_readonly; GRANT SELECT ON trading.watchlist_items TO trading_readonly; GRANT SELECT ON trading.v_watchlists_with_items TO trading_app; GRANT SELECT ON trading.v_watchlist_items_live TO trading_app; GRANT EXECUTE ON FUNCTION trading.copy_watchlist TO trading_app;