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