Taxes for the whole of Spain (common territory, Navarre, Álava, Bizkaia and Gipuzkoa); flats, garages and land valued on their own; Treasury bills, bonds and deposits priced on BME; broker accounts; a guided start; a page per investment with its chart and the tax of selling today; net worth at the top; undo when deleting; Woodpecker CI for the tests and the Linux build.
138 lines
7.9 KiB
Python
138 lines
7.9 KiB
Python
#!/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())
|