trading-platform-database-v2/ddl/schemas/trading/tables/007_watchlists.sql

335 lines
10 KiB
PL/PgSQL

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