📄 Simulador de Bolsa en SQL Server

Apuntes para entender y programar la Práctica 5 (Triggers / Motores / Simulación)

SQL Server Procedimientos Triggers Simulación

📋 Índice

  1. Visión general del simulador y del examen
  2. Modelo de datos: tablas principales
  3. Tiempo simulado, funciones y cálculos
  4. Procedimientos core: comprar, vender y actualizar cartera
  5. Simulación de mercado, motores y stops automáticos
  6. Triggers e integridad del histórico
  7. Informes analíticos: ROI, beneficios y ranking
1
Visión general del simulador y del examen
"Qué narices estamos construyendo y qué te pueden pedir"

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.

1.1 Objetivo general del sistema

El sistema debe ser capaz de:

  • Gestionar inversores y sus datos básicos.
  • Gestionar un conjunto de empresas del IBEX35 simulado.
  • Registrar operaciones de compra y venta de acciones.
  • Mantener una cartera con la posición actual de cada inversor en cada empresa.
  • Calcular:
    • Precio Medio Ponderado (PMP) de cada posición.
    • Beneficio/pérdida realizado al vender.
    • Beneficio/pérdida latente según la cotización actual.
  • Simular el mercado:
    • Variación aleatoria de cotizaciones.
    • Activación automática de STOP_LOSS y TAKE_PROFIT.
  • Generar informes (pérdidas/ganancias, ROI, ranking de inversores, etc.).
💡 Piensa en el sistema como tres bloques: datos (tablas), lógica (funciones/procedimientos/triggers) y explotación (consultas e informes).

1.2 Qué te van a exigir en el examen

En el examen/script final, como mínimo, tienen pinta de esperar:

Bloque "objetos de BD"

  • Tablas con claves primarias / foráneas correctas.
  • Funciones de cálculo (comisión, importes, resultado, quizá PMP).
  • Procedimientos de compra/venta y de simulación.
  • Triggers de protección del histórico y de log.

Bloque "lógica y pruebas"

  • Script que crea y rellena la BD sin errores.
  • Motores ejecutándose en bucle (mercado / trading / monitor).
  • Consultas analíticas que usen correctamente GROUP BY, SUM, AVG, etc.
⚠️ Importante: La regla de oro del enunciado es que el script principal debe ejecutarse entero (F5) sin errores y sin quedarse bloqueado en un bucle infinito.
2
Modelo de datos: tablas principales
"Quién guarda qué: Operacion, Cartera, Log, SimulacionEstado..."

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.

2.1 Vista general de las tablas

🔴 Base de datos SimuladorBolsa
└── 🔵 Tablas "maestro"
└── 🟢 Inversor (datos del inversor)
└── 🟢 EmpresasIBEX35 (lista de empresas y cotización actual)
└── 🔵 Tablas de movimiento / estado
└── 🟢 Operacion (histórico de compras/ventas)
└── 🟢 Cartera (posición viva de cada inversor/empresa)
└── 🟢 CotizacionHistorica (opcional, histórico de precios)
└── 🔵 Tablas de control
└── 🟢 SimulacionEstado (fecha simulada y estado de la simulación)
└── 🟢 LogSistema (registro de eventos para trazar todo)
📌 Operacion es el histórico completo. Cartera solo refleja el estado actual (posiciones abiertas).

2.2 Tablas maestras: Inversor y EmpresasIBEX35

TablaCampos típicosComentarios
Inversor idInversor (PK)
nombre
email (único)
fechaAlta (simulada)
Identifica a cada persona que compra/vende.
EmpresasIBEX35 idEmpresa (PK)
nombre
ticker
ultimaCotizacion
fechaActualizacion (simulada)
Lista fija de empresas que se negocian en el simulador.

2.3 Operacion vs Cartera

Tabla Operacion

  • Un registro por cada compra o venta.
  • Campos típicos:
    • idOperacion (PK)
    • idInversor, idEmpresa
    • tipo = 'C' (compra) o 'V' (venta)
    • cantidad
    • precioUnitario (cotización usada)
    • comision
    • importeBruto, importeNeto
    • pmpAplicado (PMP en el momento de la venta)
    • resultadoRealizado
    • motivo = NORMAL / STOP_LOSS / TAKE_PROFIT
    • fechaOperacion (simulada)

Tabla Cartera

  • Una fila por inversor + empresa (posición actual).
  • Campos típicos:
    • idCartera (PK)
    • idInversor, idEmpresa
    • acciones (cantidad actual)
    • pmp (Precio Medio Ponderado)
    • fechaUltMovimiento (simulada)
  • Si la posición se cierra (acciones = 0), normalmente se borra la fila.
💡 Recuerda: Nunca se debe tocar Operacion directamente en el examen para simular cosas. Siempre hay que usar los procedimientos de compra/venta que tú mismo programas.

2.4 Tablas de control: SimulacionEstado y LogSistema

  • SimulacionEstado:
    • Normalmente tiene una sola fila.
    • Campos típicos: idSimulacion, fechaSimulada, quizá algún estado.
    • Sirve para saber en qué "día simulado" vamos.
  • LogSistema:
    • Registro de todo lo importante:
      • Compras/ventas realizadas.
      • Activación de STOP_LOSS / TAKE_PROFIT.
      • Cambios de fecha simulada.
      • Intentos de borrar/modificar histórico.
    • Suele tener:
      • idLog, categoria, evento, detalle
      • fechaSimulada (fn_FechaSimulada())
      • fechaReal (GETDATE())
🚫 El histórico (tabla Operacion) no debe poder borrarse ni modificarse libremente. Esa protección va en los triggers.
3
Tiempo simulado, funciones y cálculos
"fn_FechaSimulada(), comisión, importes, PMP y resultado"

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().

3.1 Tiempo real vs tiempo simulado

Tipo de tiempoDe dónde salePara qué se usa
Tiempo simulado Función fn_FechaSimulada() que lee SimulacionEstado
  • fechaAlta de inversores
  • fechaActualizacion de empresas
  • fechaOperacion de compras/ventas
  • fechaUltMovimiento en Cartera
Tiempo real GETDATE()
  • Campo fechaReal en LogSistema
  • Saber cuándo se ejecutaron realmente las cosas en la BD
⚠️ No mezcles GETDATE() en campos que deben usar fn_FechaSimulada(). La práctica insiste mucho en separar ambos tiempos.

3.2 Funciones escalares para cálculos

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

3.3 Fórmulas clave

ConceptoFórmulaNotas
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.
💡 El PMP se usa solo en ventas para calcular el beneficio/pérdida realizado y se congela en pmpAplicado en la tabla Operacion.

3.4 Recalcular el PMP (Precio Medio Ponderado)

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
💡 Recuerda: El PMP tiene en cuenta la comisión también, porque forma parte del coste real de adquirir las acciones.
4
Procedimientos core: comprar, vender y actualizar cartera
"sp_Comprar, sp_Vender y cómo no romper la Cartera"

4.1 Estructura típica de un procedimiento

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;
📌 Los procedimientos de compra y venta seguirán este patrón, pero con muchas más validaciones y cálculos.

4.2 Procedimiento de compra (sp_Comprar...)

Este es el corazón del sistema cuando alguien compra acciones.

1. Parámetros de entrada: normalmente @idInversor, @idEmpresa o @ticker, @cantidad.
2. Validaciones básicas: que el inversor exista, que la empresa exista, que la cantidad sea > 0.
3. Obtener la cotización actual: leer ultimaCotizacion de EmpresasIBEX35.
4. Calcular importes: importeBruto, comision, importeNeto.
5. Insertar en Operacion: tipo 'C', motivo NORMAL, importes, fecha simulada.
6. Actualizar Cartera: crear posición si no existe o recalcular PMP si ya existe.
7. Escribir en LogSistema: detalle de la compra (quién, qué, cuántas, a qué precio).
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;

4.3 Procedimiento de venta (sp_Vender...)

1. Validar: cantidad > 0, que exista posición en Cartera y que no venda más de lo que tiene.
2. Leer PMP actual de la Cartera para ese inversor/empresa.
3. Leer cotización actual de la empresa.
4. Calcular importes y resultadoRealizado.
5. Insertar operación 'V' con el PMP aplicado y resultado.
6. Actualizar Cartera: restar acciones; si queda 0, borrar la fila.
⚠️ En las ventas automáticas por STOP_LOSS / TAKE_PROFIT, este procedimiento se suele reutilizar, cambiando el motivo.
5
Simulación de mercado, motores y stops automáticos
"RAND(CHECKSUM(NEWID())), bucles WHILE y #tablas temporales"

5.1 Variación aleatoria de cotizaciones

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();
💡 Recuerda: RAND() normal genera el mismo valor por sentencia. El truco con CHECKSUM(NEWID()) hace que cada fila reciba un valor diferente.

5.2 Tabla temporal #Posiciones y bucle WHILE

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;

5.3 STOP_LOSS y TAKE_PROFIT

Para cada posición viva, se comprueba la cotización actual y se comparan estos umbrales:

  • STOP_LOSS: si cotizacionActual <= 0.90 * PMP (cae un 10%).
  • TAKE_PROFIT: si 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;

5.4 Motores en bucle infinito y monitor

El enunciado indica que habrá 3 ventanas de SQL Server:

  • Ventana 1: Motor de Mercado y Stops Automáticos (bucle infinito).
  • Ventana 2: Motor de Trading Automático (bucle infinito que lanza compras).
  • Ventana 3: Script de monitorización (consulta periódica del estado).
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;
⚠️ Importante: En el script principal para entregar, no debes dejar un 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.
6
Triggers e integridad del histórico
"AFTER INSERT/UPDATE/DELETE sobre Operacion y SimulacionEstado"

6.1 Recordatorio: qué es un trigger

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:

TablaCuándo se usaQué 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.
💡 Recuerda: En un UPDATE, hay filas tanto en deleted (antes) como en inserted (después).

6.2 Trigger AFTER INSERT en Operacion (auditoría)

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;

6.3 Triggers para impedir UPDATE/DELETE en Operacion

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;

6.4 Trigger sobre SimulacionEstado para loguear cambios de fecha

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;
7
Informes analíticos: ROI, beneficios y ranking
"GROUP BY bien usados, filtros por fechas y tipos de operación"

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.

7.1 Informe de pérdidas y ganancias por inversor entre dos fechas

Enunciado resumido:

  • Solo operaciones de tipo 'V' (ventas).
  • Filtrar por @fechaInicio, @fechaFin sobre fechaOperacion.
  • Agrupar por inversor.
  • Calcular:
    • Número total de ventas.
    • Suma de resultadoRealizado.
    • Resultado medio por operación.
    • Número de STOP_LOSS y TAKE_PROFIT.
    • Clasificación final: BENEFICIO / PERDIDA / NEUTRO.
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;

7.2 ROI por inversor (Ranking global)

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:

  • Calcular total invertido: SUM(importeNeto) de operaciones tipo 'C'.
  • Calcular beneficio total realizado: SUM(resultadoRealizado) de operaciones tipo 'V'.
  • Unir ambas cosas por inversor (se puede hacer con subconsultas o CTEs).
⚠️ Ojo con la división: si un inversor no ha hecho compras (total invertido = 0), debes evitar dividir entre 0. Puedes poner ROI = 0 o NULL.

7.3 Operaciones más rentables y menos rentables (en %)

La rentabilidad en porcentaje se define como:

FÓRMULA-- Rentabilidad(%) = ResultadoRealizado / (Cantidad * PMP) * 100
Rentabilidad(%) = resultadoRealizado / (cantidad * pmpAplicado) * 100
  • Solo operaciones de tipo 'V'.
  • El PMP aplicado puede estar en pmpAplicado en la tabla Operacion.
  • Se pide:
    • Top 5 operaciones más rentables (orden descendente).
    • Top 5 operaciones con mayor pérdida (orden ascendente).
Guía de repaso — Simulador de Bolsa SQL Server · DAM · Versión de estudio