261 lines
8.7 KiB
PL/PgSQL
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;
|