""" Pruebas del precio de mercado de la renta fija (cartera/bme.py y msgpack_min.py) con un BME de mentira: no salen a internet. python3 -m unittest pruebas.test_bme -v """ from __future__ import annotations import datetime as dt import json import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from cartera import bme, rentafija # noqa: E402 from cartera import msgpack_min as mp # noqa: E402 def fila(isin, ultimo=0.0, compra=0.0, venta=0.0, seg="SD", grupo="DE"): return ["20261015", seg, grupo, "BO", "Bonos y Obligaciones", f"BONO {isin}", isin, "T1", 1.0, 1000.0, 3.0, compra, venta, 2.9, 1000.0, 1.0, ultimo, 3.0, 5000.0, True] class Respuesta: def __init__(self, contenido=b"", js=None): self.content, self._js = contenido, js def json(self): return self._js def raise_for_status(self): pass class BMEdeMentira: """Contesta como el canal SignalR de BME: saludo y una respuesta por invocación.""" def __init__(self, mercados: dict): self.mercados, self.pendiente, self.saludo = mercados, None, True def post(self, url, data=b"", headers=None, timeout=None): if "negotiate" in url: return Respuesta(js={"connectionToken": "t"}) if isinstance(data, bytes) and data: for inv in mp.marcos(data): args = inv[4][0] filas = [f for f in self.mercados.get((args["Segment"], args["Group"]), []) if args["Search"] in f[6]] self.pendiente = [3, {}, inv[2], 3, {"Data": filas, "TotalResults": len(filas)}] return Respuesta() def get(self, url, headers=None, timeout=None): cuerpo = b"{}\x1e" if self.saludo else b"" self.saludo = False if self.pendiente: cuerpo += mp.marco(mp.empaquetar(self.pendiente)) self.pendiente = None return Respuesta(cuerpo) class TestMessagePack(unittest.TestCase): def test_ida_y_vuelta(self): dato = [1, {}, "1", "GetSessionPrices", [{"Segment": "SD", "Page": 0, "Precio": 98.5, "Vale": True, "Nada": None}], []] self.assertEqual(mp.desempaquetar(mp.empaquetar(dato))[0], dato) self.assertEqual(list(mp.marcos(mp.marco(mp.empaquetar("hola")) * 2)), ["hola", "hola"]) class TestConsulta(unittest.TestCase): def test_el_ultimo_precio_o_el_medio_y_en_el_primer_mercado_que_lo_tenga(self): sesion = BMEdeMentira({("SD", "DE"): [fila("ES0000012M77", ultimo=98.949), fila("ES0000012P90", compra=96.0, venta=98.0)], ("MV", ""): [fila("ES0000099999", ultimo=101.5, seg="MV", grupo="DS")]}) r = bme.consultar(["es0000012m77", "ES0000012P90", "ES0000099999", "ES0000000001", "no-es-un-isin"], sesion) self.assertEqual((r["ES0000012M77"]["precio"], r["ES0000012M77"]["origen"]), (98.949, "último precio negociado")) self.assertEqual((r["ES0000012P90"]["precio"], r["ES0000012P90"]["origen"]), (97.0, "punto medio entre compra y venta")) self.assertEqual((r["ES0000099999"]["mercado"], r["ES0000099999"]["fecha"]), ("SENAF", "2026-10-15")) self.assertNotIn("ES0000000001", r) def test_sin_compra_ni_venta_no_hay_precio(self): self.assertIsNone(bme.precio(dict(zip(bme.CAMPOS, fila("ES0000012M77"))))) class TestValorDeMercado(unittest.TestCase): def test_nominal_por_precio_mas_el_cupon_corrido(self): a = {"id": "RF1", "tipo": "RentaFija", "renta_fija": {"clase": "bono", "nominal": 10000, "interes": 2.5, "vencimiento": "2027-05-31", "pagos": 1}} movs = [{"id": "m1", "activo": "RF1", "tipo": "Compra", "fecha": "2024-06-10", "importe": 9900, "cantidad": 10000}] # 31-may → 15-oct: 137 días de cupón corrido al 2,5 % self.assertEqual(rentafija.valor_de_mercado(a, movs, dt.date(2026, 10, 15), 98.949), round(10000 * 0.98949 + 10000 * 0.025 * 137 / 365, 2)) movs.append({"id": "m2", "activo": "RF1", "tipo": "Reembolso", "fecha": "2026-01-10", "importe": 5000, "cantidad": 5000}) self.assertEqual(rentafija.valor_de_mercado(a, movs, dt.date(2026, 10, 15), 100.0), round(5000 + 5000 * 0.025 * 137 / 365, 2)) class TestAlActualizarPrecios(unittest.TestCase): def test_guarda_el_valor_de_mercado_y_sin_cotizacion_lo_dice(self): import tempfile from unittest import mock from cartera import servidor from cartera.datos import Datos, escribir from cartera.operaciones import Cartera t = tempfile.TemporaryDirectory() self.addCleanup(t.cleanup) carpeta = Path(t.name) / "datos" escribir(carpeta, Datos([], [], [], [])) c = Cartera(carpeta) self.addCleanup(c.cerrar) for isin in ("ES0000012M77", "ES0000000001"): a = c.guardar_activo({"tipo": "RentaFija", "nombre": isin, "codigo": isin, "renta_fija": {"clase": "bono", "nominal": 10000, "interes": 2.5, "vencimiento": "2027-05-31", "pagos": 1}}) c.guardar_movimiento({"activo": a["id"], "tipo": "Compra", "fecha": "2024-06-10", "importe": 9900, "cantidad": 10000}) app = servidor.App(c) hoy = dt.date.today().isoformat() with mock.patch.object(bme, "consultar", return_value={"ES0000012M77": {"fecha": hoy, "precio": 99.0, "origen": "último", "mercado": "AIAF deuda pública", "nombre": ""}}): informe = app._precios_renta_fija() self.assertEqual([i["ok"] for i in informe], [True, False]) r = [x for x in c.datos.vivos("precios") if x.get("fuente") == "BME"] self.assertEqual(len(r), 1) self.assertGreater(r[0]["valor_total"], 9900) if __name__ == "__main__": unittest.main()