trading-platform-database-v2/ddl/schemas/trading/tables/006_price_alerts.sql

261 lines
8.7 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: trading
-- TABLE: price_alerts
-- DESCRIPTION: Alertas de precio configuradas por usuarios
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Enum para tipo de alerta
DO $$ BEGIN
CREATE TYPE trading.alert_type AS ENUM (
'price_above', -- Precio sube por encima de
'price_below', -- Precio baja por debajo de
'price_cross', -- Precio cruza (cualquier direccion)
'percent_change', -- Cambio porcentual
'volume_spike', -- Spike de volumen
'volatility', -- Alerta de volatilidad
'indicator', -- Alerta de indicador tecnico
'pattern' -- Patron detectado
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para estado de alerta
DO $$ BEGIN
CREATE TYPE trading.alert_status AS ENUM (
'active', -- Activa y monitoreando
'triggered', -- Disparada
'expired', -- Expirada sin disparar
'paused', -- Pausada por usuario
'deleted' -- Eliminada (soft delete)
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla de Alertas de Precio
CREATE TABLE IF NOT EXISTS trading.price_alerts (
-- 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,
symbol_id UUID REFERENCES trading.symbols(id),
-- Simbolo
symbol VARCHAR(20) NOT NULL,
-- Tipo y estado
type trading.alert_type NOT NULL DEFAULT 'price_above',
status trading.alert_status NOT NULL DEFAULT 'active',
-- Condicion
target_price DECIMAL(15, 8), -- Precio objetivo
trigger_value DECIMAL(15, 8), -- Valor que dispara (precio, %, etc)
comparison VARCHAR(10) DEFAULT 'gte', -- 'gt', 'gte', 'lt', 'lte', 'eq', 'cross'
-- Para alertas de cambio porcentual
percent_change DECIMAL(10, 4),
time_window_minutes INTEGER, -- Ventana de tiempo para % change
base_price DECIMAL(15, 8), -- Precio base para calcular %
-- Para alertas de indicadores
indicator_name VARCHAR(50),
indicator_config JSONB,
indicator_threshold DECIMAL(15, 8),
-- Configuracion
is_recurring BOOLEAN NOT NULL DEFAULT FALSE, -- Se reactiva despues de disparar
cooldown_minutes INTEGER DEFAULT 60, -- Cooldown entre disparos
max_triggers INTEGER, -- Max veces que puede disparar (NULL = ilimitado)
trigger_count INTEGER NOT NULL DEFAULT 0,
-- Notificacion
notify_email BOOLEAN NOT NULL DEFAULT TRUE,
notify_push BOOLEAN NOT NULL DEFAULT TRUE,
notify_sms BOOLEAN NOT NULL DEFAULT FALSE,
notification_message TEXT, -- Mensaje personalizado
-- Sonido
play_sound BOOLEAN NOT NULL DEFAULT TRUE,
sound_name VARCHAR(50) DEFAULT 'default',
-- Vigencia
valid_from TIMESTAMPTZ DEFAULT NOW(),
valid_until TIMESTAMPTZ, -- NULL = sin expiracion
-- Ultimo trigger
last_triggered_at TIMESTAMPTZ,
last_triggered_price DECIMAL(15, 8),
-- Precio actual (cache)
current_price DECIMAL(15, 8),
price_updated_at TIMESTAMPTZ,
-- Notas
name VARCHAR(100), -- Nombre descriptivo opcional
notes TEXT,
-- Metadata
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE trading.price_alerts IS
'Alertas de precio configuradas por usuarios para monitorear instrumentos';
COMMENT ON COLUMN trading.price_alerts.is_recurring IS
'Si TRUE, la alerta se reactiva automaticamente despues de disparar';
-- Indices
CREATE INDEX IF NOT EXISTS idx_price_alerts_tenant
ON trading.price_alerts(tenant_id);
CREATE INDEX IF NOT EXISTS idx_price_alerts_user
ON trading.price_alerts(user_id);
CREATE INDEX IF NOT EXISTS idx_price_alerts_symbol
ON trading.price_alerts(symbol);
CREATE INDEX IF NOT EXISTS idx_price_alerts_status
ON trading.price_alerts(status);
CREATE INDEX IF NOT EXISTS idx_price_alerts_active
ON trading.price_alerts(symbol, status, target_price)
WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_price_alerts_user_active
ON trading.price_alerts(user_id, status)
WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_price_alerts_expiring
ON trading.price_alerts(valid_until)
WHERE valid_until IS NOT NULL AND status = 'active';
-- Trigger para updated_at
DROP TRIGGER IF EXISTS price_alert_updated_at ON trading.price_alerts;
CREATE TRIGGER price_alert_updated_at
BEFORE UPDATE ON trading.price_alerts
FOR EACH ROW
EXECUTE FUNCTION trading.update_trading_timestamp();
-- Funcion para verificar y disparar alertas
CREATE OR REPLACE FUNCTION trading.check_price_alerts(
p_symbol VARCHAR(20),
p_price DECIMAL(15, 8)
)
RETURNS TABLE (
alert_id UUID,
user_id UUID,
alert_type trading.alert_type,
notification_message TEXT
) AS $$
DECLARE
v_alert RECORD;
BEGIN
FOR v_alert IN
SELECT * FROM trading.price_alerts
WHERE symbol = p_symbol
AND status = 'active'
AND (valid_until IS NULL OR valid_until > NOW())
AND (last_triggered_at IS NULL OR last_triggered_at + (cooldown_minutes || ' minutes')::INTERVAL < NOW())
LOOP
-- Verificar condicion segun tipo
IF (v_alert.type = 'price_above' AND p_price >= v_alert.target_price) OR
(v_alert.type = 'price_below' AND p_price <= v_alert.target_price) OR
(v_alert.type = 'price_cross' AND
((v_alert.current_price < v_alert.target_price AND p_price >= v_alert.target_price) OR
(v_alert.current_price > v_alert.target_price AND p_price <= v_alert.target_price)))
THEN
-- Disparar alerta
UPDATE trading.price_alerts
SET status = CASE WHEN is_recurring THEN 'active' ELSE 'triggered' END,
last_triggered_at = NOW(),
last_triggered_price = p_price,
trigger_count = trigger_count + 1
WHERE id = v_alert.id;
-- Verificar max_triggers
IF v_alert.max_triggers IS NOT NULL AND v_alert.trigger_count + 1 >= v_alert.max_triggers THEN
UPDATE trading.price_alerts SET status = 'triggered' WHERE id = v_alert.id;
END IF;
RETURN QUERY SELECT
v_alert.id,
v_alert.user_id,
v_alert.type,
COALESCE(v_alert.notification_message,
v_alert.symbol || ' alcanzó ' || p_price::TEXT);
END IF;
END LOOP;
-- Actualizar precio actual en todas las alertas del simbolo
UPDATE trading.price_alerts
SET current_price = p_price,
price_updated_at = NOW()
WHERE symbol = p_symbol
AND status = 'active';
END;
$$ LANGUAGE plpgsql;
-- Funcion para expirar alertas
CREATE OR REPLACE FUNCTION trading.expire_old_alerts()
RETURNS INTEGER AS $$
DECLARE
v_count INTEGER;
BEGIN
UPDATE trading.price_alerts
SET status = 'expired'
WHERE status = 'active'
AND valid_until IS NOT NULL
AND valid_until < NOW();
GET DIAGNOSTICS v_count = ROW_COUNT;
RETURN v_count;
END;
$$ LANGUAGE plpgsql;
-- Vista de alertas activas del usuario
CREATE OR REPLACE VIEW trading.v_my_price_alerts AS
SELECT
id,
symbol,
type,
status,
target_price,
current_price,
name,
notify_email,
notify_push,
is_recurring,
trigger_count,
last_triggered_at,
valid_until,
created_at
FROM trading.price_alerts
WHERE status IN ('active', 'paused')
ORDER BY created_at DESC;
-- RLS Policy para multi-tenancy
ALTER TABLE trading.price_alerts ENABLE ROW LEVEL SECURITY;
CREATE POLICY price_alerts_tenant_isolation ON trading.price_alerts
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY price_alerts_user_isolation ON trading.price_alerts
FOR ALL
USING (user_id = current_setting('app.current_user_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.price_alerts TO trading_app;
GRANT SELECT ON trading.price_alerts TO trading_readonly;
GRANT SELECT ON trading.v_my_price_alerts TO trading_app;
GRANT EXECUTE ON FUNCTION trading.check_price_alerts TO trading_app;
GRANT EXECUTE ON FUNCTION trading.expire_old_alerts TO trading_app;