Apuntes para entender y programar la Práctica 5 (Triggers / Motores / Simulación)
La práctica/examen consiste en montar en SQL Server un pequeño simulador de operaciones en bolsa. No es solo crear tablas: hay que programar lógica de negocio dentro de la base de datos usando funciones, procedimientos almacenados, triggers y scripts que simulan el mercado en bucles.
El sistema debe ser capaz de:
En el examen/script final, como mínimo, tienen pinta de esperar:
GROUP BY, SUM, AVG, etc.Antes de programar lógica, tienes que tener claro qué tablas existen y qué papel tiene cada una. El simulador se apoya en varias tablas clave.
| Tabla | Campos típicos | Comentarios |
|---|---|---|
Inversor |
idInversor (PK)nombreemail (único)fechaAlta (simulada)
|
Identifica a cada persona que compra/vende. |
EmpresasIBEX35 |
idEmpresa (PK)nombretickerultimaCotizacionfechaActualizacion (simulada)
|
Lista fija de empresas que se negocian en el simulador. |
idOperacion (PK)idInversor, idEmpresatipo = 'C' (compra) o 'V' (venta)cantidadprecioUnitario (cotización usada)comisionimporteBruto, importeNetopmpAplicado (PMP en el momento de la venta)resultadoRealizadomotivo = NORMAL / STOP_LOSS / TAKE_PROFITfechaOperacion (simulada)idCartera (PK)idInversor, idEmpresaacciones (cantidad actual)pmp (Precio Medio Ponderado)fechaUltMovimiento (simulada)Operacion directamente en el examen para simular cosas. Siempre hay que usar los procedimientos de compra/venta que tú mismo programas.
idSimulacion, fechaSimulada, quizá algún estado.idLog, categoria, evento, detallefechaSimulada (fn_FechaSimulada())fechaReal (GETDATE())Operacion) no debe poder borrarse ni modificarse libremente. Esa protección va en los triggers.
Este simulador no usa solo el tiempo real del sistema, sino un tiempo simulado que se guarda
en la tabla SimulacionEstado y se obtiene normalmente con la función fn_FechaSimulada().
| Tipo de tiempo | De dónde sale | Para qué se usa |
|---|---|---|
| Tiempo simulado | Función fn_FechaSimulada() que lee SimulacionEstado |
|
| Tiempo real | GETDATE() |
|
GETDATE() en campos que deben usar fn_FechaSimulada(). La práctica insiste mucho en separar ambos tiempos.
Es muy recomendable crear funciones escalares para evitar repetir fórmulas en todos los procedimientos.
T-SQL-- Ejemplo de función de comisión CREATE FUNCTION fn_Comision( @importeBruto DECIMAL(18,2) ) RETURNS DECIMAL(18,2) AS BEGIN DECLARE @comision DECIMAL(18,2); -- 1% del importe bruto SET @comision = @importeBruto * 0.01; -- Mínimo 10€ IF @comision < 10 SET @comision = 10; RETURN @comision; END;
Con esta función, en los procedimientos solo tienes que llamar:
T-SQL-- Dentro de un procedimiento SET @importeBruto = @cantidad * @precioUnitario; SET @comision = dbo.fn_Comision(@importeBruto);
| Concepto | Fórmula | Notas |
|---|---|---|
| Importe bruto (compra/venta) | importeBruto = cantidad * precioUnitario |
Siempre antes de comisión. |
| Comisión | max(1% del importeBruto, 10€) |
Regla del enunciado. |
| Importe neto compra | importeNeto = importeBruto + comision |
Lo que paga realmente el inversor. |
| Importe neto venta | importeNeto = importeBruto - comision |
Lo que recibe realmente el inversor. |
| Resultado realizado (venta) | (ImporteBruto - Comision) - (CantidadVendida * PMP) |
Diferencia entre lo cobrado y el coste histórico. |
pmpAplicado en la tabla Operacion.
Cuando haces una compra adicional de la misma empresa para el mismo inversor, hay que recalcular el PMP:
FÓRMULA-- PMP = coste total acumulado / acciones totales costeAnterior = accionesAnteriores * pmpAnterior costeNuevo = (cantidadComprada * precioUnitario) + comision costeTotal = costeAnterior + costeNuevo accionesTotales = accionesAnteriores + cantidadComprada pmpNuevo = costeTotal / accionesTotales
T-SQL-- Esqueleto genérico de un procedimiento CREATE PROCEDURE sp_MiProcedimiento ( @param1 INT, @param2 DECIMAL(18,2) ) AS BEGIN -- 1) Declarar variables DECLARE @algo INT; -- 2) Validar parámetros de entrada IF @param1 <= 0 BEGIN RAISERROR('Parámetro no válido', 16, 1); RETURN; END; -- 3) Lógica principal (SELECT/UPDATE/INSERT...) END;
Este es el corazón del sistema cuando alguien compra acciones.
@idInversor, @idEmpresa o @ticker, @cantidad.ultimaCotizacion de EmpresasIBEX35.importeBruto, comision, importeNeto.T-SQL (ESQUEMA)-- Este código es orientativo, no definitivo CREATE PROCEDURE sp_Comprar ( @idInversor INT, @idEmpresa INT, @cantidad INT ) AS BEGIN DECLARE @precio DECIMAL(18,2), @importeBruto DECIMAL(18,2), @comision DECIMAL(18,2), @importeNeto DECIMAL(18,2), @fecha DATETIME; -- Validar cantidad IF @cantidad <= 0 BEGIN RAISERROR('La cantidad debe ser > 0', 16, 1); RETURN; END; -- Obtener cotización actual SELECT @precio = ultimaCotizacion FROM EmpresasIBEX35 WHERE idEmpresa = @idEmpresa; -- Calcular importes SET @importeBruto = @cantidad * @precio; SET @comision = dbo.fn_Comision(@importeBruto); SET @importeNeto = @importeBruto + @comision; SET @fecha = dbo.fn_FechaSimulada(); -- Insertar operación de compra INSERT INTO Operacion(idInversor, idEmpresa, tipo, cantidad, precioUnitario, comision, importeBruto, importeNeto, motivo, fechaOperacion) VALUES(@idInversor, @idEmpresa, 'C', @cantidad, @precio, @comision, @importeBruto, @importeNeto, 'NORMAL', @fecha); -- TODO: Actualizar Cartera (crear o sumar acciones y recalcular PMP) -- TODO: Insertar en LogSistema END;
resultadoRealizado.motivo.
El enunciado pide que cada empresa varíe su cotización entre -3% y +3% usando
RAND(CHECKSUM(NEWID())) para tener valores independientes por fila.
T-SQL-- Ejemplo de variación de cotización entre -3% y +3% UPDATE EmpresasIBEX35 SET ultimaCotizacion = ultimaCotizacion * (1 + (RAND(CHECKSUM(NEWID())) * 0.06 - 0.03)), fechaActualizacion = dbo.fn_FechaSimulada();
RAND() normal genera el mismo valor por sentencia. El truco con CHECKSUM(NEWID()) hace que cada fila reciba un valor diferente.
Para evaluar los STOPs, el enunciado pide expresamente usar una tabla temporal y un WHILE.
T-SQL-- Crear tabla temporal con las posiciones actuales en orden aleatorio SELECT ROW_NUMBER() OVER (ORDER BY NEWID()) AS idTmp, idInversor, idEmpresa, acciones, pmp INTO #Posiciones FROM Cartera;
T-SQL-- Recorrer #Posiciones con WHILE DECLARE @maxId INT, @i INT = 1; SELECT @maxId = MAX(idTmp) FROM #Posiciones; WHILE @i <= @maxId BEGIN -- Leer una fila SELECT @idInversor = idInversor, @idEmpresa = idEmpresa, @acciones = acciones, @pmp = pmp FROM #Posiciones WHERE idTmp = @i; -- Aquí se evalúan los STOPs para esta posición -- y se llama a sp_Vender si procede SET @i = @i + 1; END;
Para cada posición viva, se comprueba la cotización actual y se comparan estos umbrales:
cotizacionActual <= 0.90 * PMP (cae un 10%).cotizacionActual >= 1.20 * PMP (sube un 20%).T-SQL (ESQUEMA)-- Dentro del WHILE, para cada posición SELECT @cotizacion = ultimaCotizacion FROM EmpresasIBEX35 WHERE idEmpresa = @idEmpresa; IF @cotizacion <= @pmp * 0.90 BEGIN -- STOP_LOSS: vender todas las acciones EXEC sp_Vender @idInversor, @idEmpresa, @acciones, 'STOP_LOSS'; END ELSE IF @cotizacion >= @pmp * 1.20 BEGIN -- TAKE_PROFIT EXEC sp_Vender @idInversor, @idEmpresa, @acciones, 'TAKE_PROFIT'; END;
El enunciado indica que habrá 3 ventanas de SQL Server:
T-SQL (ESQUEMA)-- Ejemplo de bucle infinito "controlado" WHILE 1 = 1 BEGIN -- 1) Avanzar fecha simulada llamando a sp_SumarTiempoSimul EXEC sp_SumarTiempoSimul @dias = 1; -- 2) Simular mercado (variar cotizaciones) EXEC sp_SimularMercado; -- 3) Evaluar STOPs automáticos EXEC sp_EvaluarStops; -- 4) Pausa entre ciclos para no reventar la CPU WAITFOR DELAY '00:00:02'; -- 2 segundos END;
WHILE 1=1 que se ejecute solo al darle a F5. Los bucles infinitos se ejecutan en scripts aparte, no en el principal que crea la BD.
Un trigger es un tipo especial de procedimiento que se ejecuta automáticamente cuando ocurre un
INSERT, UPDATE o DELETE en una tabla.
Dentro del trigger, tienes dos tablas lógicas:
| Tabla | Cuándo se usa | Qué contiene |
|---|---|---|
inserted |
INSERT y UPDATE |
Las filas nuevas o modificadas tras la operación. |
deleted |
DELETE y UPDATE |
Las filas antiguas antes de la operación. |
UPDATE, hay filas tanto en deleted (antes) como en inserted (después).
Este trigger registra en LogSistema cada operación que se inserta en la tabla Operacion.
T-SQL (ESQUEMA)-- Trigger para auditar inserciones en Operacion CREATE TRIGGER tr_Operacion_AfterInsert ON Operacion AFTER INSERT AS BEGIN INSERT INTO LogSistema(categoria, evento, detalle, fechaSimulada, fechaReal) SELECT 'OPERACION', 'INSERT', 'Nueva operación: ' + tipo + ' - Inversor ' + CAST(idInversor AS NVARCHAR(10)), fechaOperacion, GETDATE() FROM inserted; END;
Para proteger el histórico, se suelen crear triggers que directamente bloquean modificaciones o borrados.
T-SQL-- Bloquear DELETE en Operacion CREATE TRIGGER tr_Operacion_NoDelete ON Operacion INSTEAD OF DELETE AS BEGIN -- Registrar intento en LogSistema INSERT INTO LogSistema(categoria, evento, detalle, fechaSimulada, fechaReal) SELECT 'OPERACION', 'INTENTO_DELETE', 'Intento de borrar operaciones', dbo.fn_FechaSimulada(), GETDATE() FROM deleted; RAISERROR('No se permite borrar operaciones del histórico', 16, 1); END;
T-SQL-- Bloquear UPDATE de campos críticos en Operacion CREATE TRIGGER tr_Operacion_NoUpdateCritico ON Operacion AFTER UPDATE AS BEGIN -- Si alguien intenta cambiar cantidad, precioUnitario, importes, tipo, motivo... IF UPDATE(cantidad) OR UPDATE(precioUnitario) OR UPDATE(importeBruto) OR UPDATE(importeNeto) OR UPDATE(resultadoRealizado) OR UPDATE(tipo) OR UPDATE(motivo) BEGIN RAISERROR('No se permite modificar campos críticos de Operacion', 16, 1); ROLLBACK TRANSACTION; RETURN; END; END;
Cada vez que se actualice SimulacionEstado (por ejemplo, con sp_SumarTiempoSimul),
se puede registrar el cambio de fecha simulada en LogSistema.
T-SQL-- Registrar cambio de fecha simulada CREATE TRIGGER tr_SimulacionEstado_LogFecha ON SimulacionEstado AFTER UPDATE AS BEGIN INSERT INTO LogSistema(categoria, evento, detalle, fechaSimulada, fechaReal) SELECT 'SIMULACION', 'CAMBIO_FECHA', 'Fecha simulada pasa de ' + CONVERT(NVARCHAR(19), d.fechaSimulada, 120) + ' a ' + CONVERT(NVARCHAR(19), i.fechaSimulada, 120), i.fechaSimulada, GETDATE() FROM inserted i JOIN deleted d ON i.idSimulacion = d.idSimulacion; END;
La última parte de la práctica son informes que explotan los datos generados. Aquí es donde entran en juego las
consultas con GROUP BY, SUM, COUNT, AVG, filtros por fechas y por tipos de operación.
Enunciado resumido:
@fechaInicio, @fechaFin sobre fechaOperacion.resultadoRealizado.T-SQL (ESQUEMA)-- Estructura básica del procedimiento de informe CREATE PROCEDURE sp_InformePyGPorInversor ( @fechaInicio DATE, @fechaFin DATE ) AS BEGIN SELECT i.idInversor, i.nombre, COUNT(*) AS numVentas, SUM(o.resultadoRealizado) AS resultadoTotal, AVG(o.resultadoRealizado) AS resultadoMedio, SUM(CASE WHEN o.motivo = 'STOP_LOSS' THEN 1 ELSE 0 END) AS numStopLoss, SUM(CASE WHEN o.motivo = 'TAKE_PROFIT' THEN 1 ELSE 0 END) AS numTakeProfit, CASE WHEN SUM(o.resultadoRealizado) > 0 THEN 'BENEFICIO' WHEN SUM(o.resultadoRealizado) < 0 THEN 'PERDIDA' ELSE 'NEUTRO' END AS clasificacion FROM Operacion o JOIN Inversor i ON o.idInversor = i.idInversor WHERE o.tipo = 'V' AND o.fechaOperacion BETWEEN @fechaInicio AND @fechaFin GROUP BY i.idInversor, i.nombre ORDER BY resultadoTotal DESC; END;
Definición de ROI del enunciado:
FÓRMULA-- ROI(%) = Beneficio total realizado / Total invertido * 100 ROI(%) = SUM(resultadoRealizado) / SUM(importeNeto de compras) * 100
Para sacar este informe necesitas:
SUM(importeNeto) de operaciones tipo 'C'.SUM(resultadoRealizado) de operaciones tipo 'V'.La rentabilidad en porcentaje se define como:
FÓRMULA-- Rentabilidad(%) = ResultadoRealizado / (Cantidad * PMP) * 100 Rentabilidad(%) = resultadoRealizado / (cantidad * pmpAplicado) * 100
pmpAplicado en la tabla Operacion.