335 lines
10 KiB
PL/PgSQL
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;
|