#!/usr/bin/env python3 """ generar_ejemplo.py — Fabrica la cartera de ejemplo que Caudal enseña a quien quiere mirar antes de empezar (cartera/ejemplo/). python3 herramientas/generar_ejemplo.py Todo es inventado y corriente a propósito: bitcoin comprado poco a poco, un fondo indexado, un ETF, unas acciones, una cuenta bancaria, un piso alquilado con su hipoteca, una finca de cereal arrendada y una Letra del Tesoro. Lo que Caudal calcula solo (el valor de los inmuebles y de la Letra) no se guarda: se calcula al abrir el ejemplo, ya con las fechas desplazadas. Las fechas se escriben respecto a una fecha base (base.json); al abrir el ejemplo, Caudal las desplaza para que siempre parezca reciente. """ from __future__ import annotations import datetime as dt import json import shutil import sys import tempfile from pathlib import Path PROGRAMA = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROGRAMA)) from cartera.operaciones import Cartera # noqa: E402 BASE = dt.date(2026, 10, 1) DESTINO = PROGRAMA / "cartera" / "ejemplo" def meses(desde: dt.date, hasta: dt.date): d = desde while d <= hasta: yield d d = dt.date(d.year + (d.month == 12), d.month % 12 + 1, d.day) def camino(inicio: float, fin: float, n: int, ondas: tuple) -> list[float]: """Una subida de «inicio» a «fin» en n pasos con altibajos fijos (sin azar: sale siempre igual).""" return [round((inicio + (fin - inicio) * i / (n - 1)) * (1 + ondas[i % len(ondas)]), 2) for i in range(n)] def main() -> int: with tempfile.TemporaryDirectory() as tmp: c = Cartera(Path(tmp) / "datos") alta = lambda **k: c.guardar_activo(k)["id"] # noqa: E731 btc = alta(nombre="Bitcoin", tipo="Cripto", codigo="bitcoin", unidad="BTC") fondo = alta(nombre="Fondo indexado mundial", tipo="Fondo", codigo="IE00B03HCZ61", unidad="participaciones") etf = alta(nombre="ETF S&P 500", tipo="ETF", codigo="IE00B5BMR087", unidad="participaciones") accion = alta(nombre="Acciones de una empresa", tipo="Accion", codigo="", unidad="acciones") cuenta = alta(nombre="Cuenta bancaria", tipo="Cuenta", unidad="€") hipoteca = c.guardar_activo({"nombre": "Hipoteca del piso", "tipo": "Deuda", "fecha_inicio": "2024-02-20", "capital": 120000, "tin": 2.9, "cuotas": 300, "primera_cuota": "2024-03-20"})["id"] piso = c.guardar_activo({"nombre": "Piso alquilado", "tipo": "Inmueble", "inmueble": { "clase": "vivienda", "uso": "alquilado", "comunidad": "valenciana", "auto": True, "precio_compra": 160000, "referencia": "Calle del Ejemplo 1, 2.º A", "hipoteca": hipoteca, "valor_catastral": 70000, "porcentaje_construccion": 60, "contrato": "habitual"}})["id"] finca = c.guardar_activo({"nombre": "Finca de cereal", "tipo": "Inmueble", "inmueble": { "clase": "terreno_rustico", "uso": "alquilado", "comunidad": "castilla_leon", "auto": True, "superficie": 4, "cultivo": "TAS", "origen": "compra", "referencia": "Polígono 3, parcela 41"}})["id"] letra = c.guardar_activo({"nombre": "Letra del Tesoro a 12 meses", "tipo": "RentaFija", "renta_fija": { "clase": "letra", "nominal": 10000, "vencimiento": "2027-03-05", "pagos": 0, "emisor": "Tesoro Público"}})["id"] mov = lambda activo, tipo, fecha, **k: c.guardar_movimiento({"activo": activo, "tipo": tipo, "fecha": fecha.isoformat(), **k}) # noqa: E731 fechas = list(meses(dt.date(2024, 1, 5), dt.date(2026, 9, 5))) ondas_btc = (0.0, 0.06, -0.08, 0.12, 0.03, -0.10, 0.05, -0.02) ondas_fondo = (0.0, 0.01, -0.015, 0.02, 0.005, -0.01) p_btc = camino(39000, 96000, len(fechas), ondas_btc) p_fondo = camino(40.0, 52.5, len(fechas), ondas_fondo) for f, pb, pf in zip(fechas, p_btc, p_fondo): mov(btc, "Compra", f, importe=200, cantidad=round(200 / pb, 8)) # 200 € al mes, pase lo que pase mov(fondo, "Compra", f.replace(day=10), importe=150, cantidad=round(150 / pf, 4)) if f.month in (3, 6, 9, 12): # un precio por trimestre, para el gráfico fin = f.replace(day=28) c.precio_manual(btc, fin, pb) c.precio_manual(fondo, fin, pf) mov(etf, "Compra", dt.date(2024, 4, 15), cantidad=6, precio=470) mov(etf, "Compra", dt.date(2025, 1, 20), cantidad=4, precio=545) mov(etf, "Compra", dt.date(2025, 9, 12), cantidad=3, precio=560) mov(accion, "Compra", dt.date(2024, 3, 12), cantidad=50, precio=20) mov(accion, "Dividendo", dt.date(2024, 12, 16), importe=20.25, retencion=4.75) mov(accion, "Compra", dt.date(2025, 2, 18), cantidad=30, precio=24) mov(accion, "Venta", dt.date(2025, 11, 6), cantidad=20, precio=30) mov(accion, "Dividendo", dt.date(2025, 12, 15), importe=24.30, retencion=5.70) mov(cuenta, "Dividendo", dt.date(2025, 12, 31), importe=31.60, retencion=7.40) mov(cuenta, "Dividendo", dt.date(2026, 6, 30), importe=28.35, retencion=6.65) mov(piso, "Compra", dt.date(2024, 2, 20), importe=172000, notas="Precio y gastos de la compra") for f in meses(dt.date(2024, 4, 1), dt.date(2026, 9, 1)): mov(piso, "Dividendo", f, importe=850 if f.year == 2024 else 875) for año in (2024, 2025): mov(piso, "Gasto", dt.date(año, 10, 15), importe=420, clase_gasto="otro", notas="IBI") for año in (2024, 2025, 2026): mov(piso, "Gasto", dt.date(año, 3, 1), importe=210, clase_gasto="otro", notas="Seguro del hogar") mov(piso, "Gasto", dt.date(2025, 5, 12), importe=640, clase_gasto="reparacion", notas="Caldera") mov(finca, "Compra", dt.date(2024, 9, 10), importe=22000) mov(finca, "Dividendo", dt.date(2025, 9, 30), importe=600, notas="Arrendamiento de la campaña") mov(letra, "Compra", dt.date(2026, 3, 10), importe=9760, cantidad=10000) for f, saldo in ((dt.date(2024, 1, 2), 9000), (dt.date(2024, 12, 31), 7400), (dt.date(2025, 12, 31), 6100), (BASE, 5800)): c.precio_manual(cuenta, f, saldo) for f, pe, pa in ((dt.date(2024, 6, 28), 495, 21.5), (dt.date(2024, 12, 30), 540, 23), (dt.date(2025, 6, 27), 552, 26.5), (dt.date(2025, 12, 30), 575, 29.5), (BASE, 590, 31.2)): c.precio_manual(etf, f, pe) c.precio_manual(accion, f, pa) c.precio_manual(btc, BASE, 96000) c.precio_manual(fondo, BASE, 52.5) c.cerrar() DESTINO.mkdir(parents=True, exist_ok=True) for f in sorted((Path(tmp) / "datos").glob("*.json*")): # Sin marcas de tiempo de cuando se generó: que salga igual cada vez if f.suffix == ".jsonl": lineas = [] for i, linea in enumerate(f.read_text(encoding="utf-8").splitlines()): r = json.loads(linea) # Lo que Caudal apunta solo se rehace al abrir el ejemplo (ver servidor.App.ejemplo) if f.stem == "precios" and r.get("fuente") in ("IPV", "compra", "MAPA", "devengo"): continue if f.stem == "movimientos" and str(r.get("id", "")).startswith("rf-"): continue r["modificado"] = "2026-10-01T00:00:00.000000Z" if f.stem == "movimientos": r["id"] = f"mej{i:04d}" if f.stem == "activos": r["uid"] = f"ejemplo{i:05d}" lineas.append(json.dumps(r, ensure_ascii=False)) (DESTINO / f.name).write_text("\n".join(lineas) + "\n", encoding="utf-8") else: shutil.copyfile(f, DESTINO / f.name) (DESTINO / "base.json").write_text(json.dumps({"base": BASE.isoformat()}) + "\n", encoding="utf-8") print(f"Cartera de ejemplo escrita en {DESTINO}") return 0 if __name__ == "__main__": sys.exit(main())