# Caudal — notas técnicas Caudal es una aplicación local que se usa desde el navegador (Python, sin depender de internet para funcionar) para llevar una cartera: fondos, planes, cripto, metales, acciones, préstamos, dividendos, inversiones cerradas y un asistente que entiende frases. Este fichero explica cómo está hecha y cómo se mantiene una instalación que se lleva desde su carpeta, por ejemplo en un USB. Para instalarla en un servidor de casa, ver `empaquetado/LEEME.md`. ``` bash abrir_app.sh # abre la aplicación (prepara Python si hace falta) bash actualizar_app.sh # trae la última versión de la rama «main» de tu servidor git ``` Qué hay dentro: `programa/cartera/__init__.py` lista los módulos y qué hace cada uno. Las pruebas (`programa/pruebas/`) se pasan enteras sin internet: ``` cd programa && python3 -m unittest discover -s pruebas ``` ## Qué hay en el repositorio y qué no Solo el programa, las pruebas y una cartera **ficticia** (`pruebas/cartera_ficticia/`, con importes inventados). Nunca datos personales: `datos/`, `backups/`, `bitcoin.json` y las notas privadas están en `.gitignore`. ## Cómo se actualiza una instalación La carpeta de la instalación es un clon de este repositorio. `actualizar_app.sh`: - trae la rama `main` del repositorio del que se clonó y enseña las novedades; - **prueba la versión nueva en una copia aparte** (pruebas automáticas y lectura de tus datos), pregunta y solo entonces la aplica; - guarda la versión anterior: `bash actualizar_app.sh --volver`. Solo hace falta red y git al actualizar; la aplicación funciona sin conexión. Quien mantenga su propia copia puede proteger `main` en su servidor y pasar los cambios por otra rama, para que nada llegue a la instalación sin revisarlo. ## Primera vez ``` git clone caudal && cd caudal && bash abrir_app.sh ``` Los datos (`datos/`), las copias (`backups/`) y los paquetes de Python guardados (`wheels/`) viven dentro de `programa/`, fuera del repositorio: actualizar nunca los toca. ## Entorno de Python `abrir_app.sh` lo prepara solo con `entorno_python.sh`: un entorno en `/tmp/.cartera-venv` (se borra al reiniciar el equipo y se recrea en unos segundos). Usa los paquetes de `wheels/` si están y, si no, los descarga de internet. Para poder instalar sin internet en adelante: ``` bash programa/entorno_python.sh --guardar-wheels ``` Si falta el módulo de entornos: `sudo apt install python3-venv` (o `python3.12-venv`, según la versión). ## Si se estropea o se pierde el equipo o el USB Lo que se pierde: las copias automáticas de `backups/` y los cambios que no hubieras sincronizado. El programa está en su repositorio y los datos, si usas «Copia cifrada en tu servidor git», cifrados con tu clave GPG en el repositorio de datos que elegiste. 1. Clona el programa en el equipo o el USB nuevo (ver «Primera vez»). 2. `bash abrir_app.sh`: preparará Python (con internet) y, al no encontrar datos, ofrecerá **recuperarlos de la copia cifrada**. Vuelven también los monederos de Bitcoin y los ajustes de extractos. Nunca sobrescribe datos que ya existan. La app queda sincronizada. 3. Opcional: `bash programa/entorno_python.sh --guardar-wheels` para poder instalar sin internet. **Simulacro** (recomendable de vez en cuando; no toca tu instalación): ``` bash programa/simulacro_usb_nuevo.sh ``` Descarga el programa en una carpeta temporal, crea Python desde cero, pasa las pruebas, recupera los datos con tu clave y los compara con los que tienes. ## Préstamos recibidos (deudas) Tipo de activo «Préstamo recibido (deuda)», distinto del «Préstamo concedido» (dinero que prestas tú). Cuota constante, sistema francés, tipo fijo. Se da de alta desde una fecha con lo que dice el banco: capital pendiente, TIN, cuotas que quedan y fecha de la próxima cuota. Las cuotas se calculan solas (no se apuntan); solo se registran: - **Amortizaciones anticipadas**, reduciendo plazo o cuota. El formulario simula antes cuánto ahorras con cada opción. También con el asistente: «amorticé 5.000 € del préstamo reduciendo plazo». - **Pendiente según el banco** de vez en cuando: corrige las diferencias de redondeo y la app sigue desde esa cifra. La deuda no entra en la rentabilidad: resta en el **patrimonio neto**, y así sale también al exportar a Excel. Motor en `cartera/deuda.py`. ## Fiscal: lo que hay que declarar La pestaña **Fiscal** enseña, ejercicio a ejercicio, las cifras como las pide la declaración, que no son las de la cartera: la cartera usa el coste medio para decir cómo va la inversión, y Hacienda manda que lo vendido sea lo primero que se compró (**FIFO**). - **Ganancias y pérdidas** de cada venta o reembolso, con las compras de las que sale (pulsa la fila). Todo el bitcoin es una sola bolsa, esté donde esté; los traslados entre tus lugares no son ventas; un traspaso entre fondos no tributa y conserva coste y fechas. - **Dividendos, intereses y rentas** cobrados, con su retención. - **Lo minado o recibido sin pagar**, a su valor de mercado de ese día. - **Lo que tenías a 31 de diciembre**, con su coste y su valor. - **Lo que falta por resolver** antes de fiarse: importes estimados, ventas sin compras suficientes apuntadas… - **La regla de la recompra** («de los dos meses»): una pérdida en acciones, ETF o fondos no se resta si compraste el mismo valor cerca de la venta. La pestaña dice cuánto queda aplazado, por qué compra, y en qué año se libera al vender lo recomprado. No se aplica a las criptomonedas. Los criterios aplicados y su fuente se listan al pie de la pestaña, y tres se pueden elegir ahí mismo: si la comisión de red es una pequeña venta o no tributa, si lo minado entra por su valor de ese día o a coste cero (cuando no se declaró al recibirlo), y el plazo de la recompra en fondos (dos meses o un año). No entran los planes de pensiones ni las cuentas de trading. - **Lo que saldría a pagar por el ahorro**, paso a paso, con las reglas de **Navarra**: los dos saldos (rendimientos, y ganancias y pérdidas), la exención cuando se vende poco en el año, las pérdidas de años anteriores (cuatro años), la compensación entre saldos (hasta el 25 %), la escala y lo ya retenido. Es una estimación de una parte de la declaración: no ve lo que no está en la cartera, ni la parte general, ni las deducciones. Las reglas de cada territorio no están en el código: están en `cartera/fiscal/reglas/.json`, por tramos de ejercicios, cada una con su artículo y una marca de si se ha comprobado contra la norma de ese año. Hoy solo está Navarra (comprobada de 2023 en adelante; los años anteriores usan la escala previa y lo avisan). Para añadir un territorio basta otro fichero igual. Es una ayuda para preparar la declaración, no asesoría fiscal. Motor en `cartera/fiscal/`, aparte del de la cartera: lee las mismas operaciones y no cambia ninguna cifra. ## Bitcoin desde tu nodo **Más → Bitcoin desde mi nodo…** compara los bitcoins apuntados con los que ve tu servidor Electrum (electrs o Fulcrum), por Tor si su dirección es `.onion`, y te propone qué apuntar de cada transacción nueva: compra, cobro, minería, venta, pago, comisión de red, traslado entre lugares tuyos… o enlazarla con una operación que ya tenías apuntada. Solo se guardan claves **públicas** (xpub, ypub, zpub). La comprobación diaria solo avisa de lo que pase a partir de darlas de alta; para el pasado, ver «Todo el historial». Las dos usan el mismo motor y la misma ventana: lo que decides en una queda decidido en la otra, y todo lo guardado desde el nodo se puede deshacer en «Importaciones hechas». Las claves viven en `programa/bitcoin.json` (permisos `600`). No entran en el repositorio del programa, pero sí **viajan cifradas en la copia de tu servidor git**, para que una instalación nueva no tenga que volver a teclearlas. ### Lo que pagaste frente al mercado (prima) De cada compra o venta de bitcoin la app calcula la **prima**: cuánto se aleja tu precio del cierre de ese día. Sale bajo el precio en Operaciones, y en **Bitcoin desde mi nodo → Importes y prima** hay una lista para repasarlas: - pon el importe que pagaste de verdad (si fueron varios pagos, escríbelos sumados: `350 + 420,50`) y la compra deja de ser «estimada»; - adjunta ahí mismo los justificantes, uno por pago si quieres; - si un precio queda muy lejos del mercado (más de un 15 % peor, o más de un 5 % mejor) se avisa en «Para revisar», porque suele ser una errata. Si la cifra es buena, «La cifra es buena» y no se avisa más. No cambia ningún cálculo: el coste de una compra es lo que pagaste, con su prima. Motor en `cartera/prima.py`. ### Un monedero compartido con otra persona Si de un monedero solo una parte es tuya, ponlo al editarlo: «Mi parte del monedero (%)» (50 = la mitad). Desde entonces todo lo de ese monedero cuenta por tu parte: de 1 BTC que llega se propone apuntar 0,5, y el cuadre se hace contra la mitad de lo que ve el nodo. En un reparto (una salida a un monedero tuyo y otra al otro dueño) lo tuyo es un traslado y lo del otro no aparece: no es un regalo ni una venta, porque nunca fue tuyo. Si ya tenías operaciones guardadas de ese monedero, «Revisar todo el historial» avisa de las que han dejado de encajar y las deshace de una vez para volver a decidirlas. ### Lugares: dónde tienes cada bitcoin Tu bitcoin es **un solo activo** («Bitcoin»), esté donde esté: para Hacienda y para la rentabilidad es una sola bolsa. Dónde está —un exchange, el monedero del móvil, el de escritorio, una multifirma, un nodo Lightning— es un **lugar** de ese activo, y se ve en su ficha («Dónde lo tienes», con el saldo de cada uno). - Cada **monedero** del nodo es un lugar (se crea solo con su nombre; en el formulario del monedero se puede elegir otro o compartirlo). - Cada **exchange** de un extracto importado es un lugar (se crea solo). - Los **nodos Lightning** se crean a mano en la ficha: «+ Lugar», tipo «Nodo Lightning». - Mover entre lugares tuyos es un **Traslado**: una sola línea «Exchange → Monedero», que no es compra ni venta y no cambia el coste. Su comisión (lo que se pierde por el camino) va unida al traslado y sí baja la cantidad. Se apunta a mano en «+ Operación», tipo «Traslado» (el traspaso a otro activo, igual: tipo «Traspaso»). - En **Operaciones**, al filtrar por un activo con lugares aparece el filtro «Todos los lugares» (con «Sin lugar» y el saldo de ese lugar). «+ Operación» abre ya en el activo y el lugar que estés filtrando. - Un activo aparte que en realidad es otro sitio del mismo bitcoin («BTC en Phoenix») se une con «Unir otro activo como lugar…» en la ficha: sus operaciones pasan a «Bitcoin» con ese lugar y sus traspasos se convierten en traslados. - L-BTC (Liquid) sigue siendo **otro activo**: pasar de BTC a L-BTC con un swap puede ser una permuta, así que es un traspaso, no un traslado. ### Todo el historial **Revisar todo el historial** (en la misma ventana) recorre todas las transacciones de todos los monederos desde la primera y te propone qué apuntar de cada una. No se guarda nada hasta que marcas las filas y pulsas «Guardar». - **Se hace a ratos.** Lo leído del nodo se guarda en este equipo (`programa/bitcoin_cache.json`, permisos `600`, no viaja a la copia cifrada): la revisión se reabre al instante y «Volver a leer el nodo» solo trae lo nuevo. Lo que dejas elegido sin guardar se recuerda. Arriba se ve cuánto llevas, y se puede filtrar por año. - **Da de alta todos los monederos antes de empezar.** Si añades uno después y una decisión guardada deja de encajar (aquella «venta» era en realidad un envío a tu propio monedero), sale un aviso con «Deshacer esa decisión». - **Todos los monederos cuentan como uno.** Pasar bitcoin de un monedero tuyo a otro no es compra ni venta: es un **traslado** entre sus lugares (o, si están en activos distintos, un traspaso que conserva el coste medio). - **Lo que ya tienes apuntado no se toca.** Si una operación tuya se parece (mismo activo y lugar, cantidad casi igual, fecha cercana) se propone *enlazarla*: se le anota el txid y ninguna cifra cambia. - **Las retiradas y depósitos de los exchanges se emparejan solos.** Importa antes el extracto del exchange: cada retirada queda como traslado «Exchange → ?» y cada depósito como «? → Exchange». Cuando el historial ve llegar (o salir) esa cantidad en un monedero, propone enlazarlo (✓): al guardar se completa el extremo que faltaba. - **Una salida no se supone venta**: eliges venta, pago o regalo, o «va a otro lugar mío» (un nodo Lightning, un exchange), que es un traslado. Crea antes el lugar en la ficha de «Bitcoin». Si la salida va a un contrato P2WSH, la app avisa de que parece la apertura de un canal. - Arriba se ve, **por monedero**, lo que hay en el nodo, lo apuntado ahí y lo que habría **tras guardar lo marcado**: cuando pone «cuadra», está bien. «Sin lugar» es lo apuntado sin decir dónde (lo de antes de los lugares) más los traslados de exchanges aún sin su otro extremo. - Los euros propuestos son una **estimación** con el cierre del día (serie empaquetada `cartera/serie_btc_eur.csv`, Bitstamp desde 2011, contrastada con otra fuente; se regenera con `python3 herramientas/serie_btc_eur.py`). En una compra P2P pon lo que pagaste de verdad: ese es el coste fiscal. - **Deshacer**: «Importaciones hechas» lista cada guardado y lo deshace entero (borra lo creado, quita los enlaces y vuelve a dejarlo pendiente). **«Empezar de cero…»** quita de una vez todo lo del historial y todo lo que metieron los extractos de exchanges, sin tocar los monederos ni lo apuntado antes. ### De dónde viene cada moneda **Mis monedas** lista las monedas (UTXO) que tienes sin gastar y, de cada una, de dónde viene: se sigue hacia atrás, transacción a transacción, hasta donde entró (compra, minado, algo que llegó de fuera). Cuando una transacción juntó varias monedas, la nueva viene de cada una **a prorrata** de lo que valía; la parte de terceros de un coinjoin se ve aparte. Con cada origen: lo que pagaste por bitcoin (si lo tienes apuntado) y el cierre de ese día. Es informativo: no cambia cifras ni el cálculo fiscal. Usa lo último leído del nodo. ## Actividades: la rentabilidad de la minería Cada operación puede llevar el nombre de una **actividad** (por ejemplo «Minería»). En el resumen aparece una tarjeta con sus cifras propias: lo puesto (aparatos y hosting), lo recuperado, lo producido, la TIR y **a cuánto te sale cada bitcoin minado**, que es lo que dice si compensa. Marcar operaciones no cambia ninguna cifra de la cartera: es una lectura aparte de lo que ya tienes. ## Cuando te devuelven parte de un gasto Los gastos ligados a un activo (el hosting de los mineros, los aparatos, la custodia) se apuntan como **Gasto** y suben su coste. Si te devuelven parte de ese dinero, se apunta como **Devolución de un gasto**, no como dividendo: un dividendo es una renta que produce el activo, y esto es un gasto que se anula. La diferencia no está en cuánto, sino en dónde: la TIR y la ganancia total salen igual de las dos maneras —el euro entra el mismo día—, pero la devolución **baja el coste** y, con él, el precio medio, y sube el ROI. Y deja de mezclarse con los dividendos y los intereses de verdad. Si el importe aún es provisional, márcala con «Cifras aproximadas»: cuando sepas el definitivo, la editas y se confirma. ## A qué precio se valora cada cosa - **Acciones y ETF con ISIN** (LS Exchange): al precio de **compra** (bid), que es lo que cobrarías si vendieras ahora y lo que enseñan las aplicaciones de los brókers. Si no se puede leer, se usa el precio medio entre compra y venta, que es algo mayor. - **Fondos y planes**: el valor liquidativo publicado (no hay compra y venta). - **Bitcoin y metales**: el precio de mercado de la fuente (CoinGecko, etc.), que no descuenta el margen que cobre cada bróker o exchange al vender. ## Si un precio deja de actualizarse Los precios salen de webs ajenas y a veces cambian sin avisar: en agosto de 2026 se cayeron a la vez Morningstar, la CNMV y la web de una gestora. La aplicación ya avisa cuando un precio lleva más de una semana sin moverse, pero eso llega tarde y no dice de quién es la culpa. **Más → Comprobar las fuentes de precios…** pregunta a cada web con un ejemplo público —un fondo, un ETF, bitcoin y el oro— y dice cuál contesta hoy y cuál no. No consulta ningún activo tuyo. Lo mismo desde la terminal, sin abrir la aplicación: python3 -m cartera.precios La CNMV sale siempre como «—»: dejó de publicar el valor liquidativo en 2026 y se conserva solo por si vuelve. Que aparezca ahí no es una avería nueva. Cada fuente vive en `cartera/precios.py`, una función por sitio, y se prueba en `pruebas/test_precios.py` contra respuestas guardadas en `pruebas/respuestas/`. Para arreglar una fuente rota: cambiar su función, guardar la respuesta nueva en esa carpeta y ajustar su prueba. ## Llevarse los números fuera **Más → Exportar a Excel (.xlsx)** guarda una foto de la cartera: una hoja con lo mismo que ves en el resumen (incluidos los préstamos recibidos y el patrimonio neto), otra con todos los movimientos y otra con los precios. Son valores, sin fórmulas: los abre cualquier programa de hojas de cálculo. Lo que se edite ahí no vuelve a la aplicación; los datos de verdad son los ficheros de texto de `datos/`, que se leen con cualquier editor. También desde la terminal: python3 -m cartera.exportar_xlsx datos/ cartera.xlsx ## Cuando cambia el formato de los datos `datos/version.json` dice en qué formato están los datos (si no está, es el 1). Cuando una versión nueva de la app necesite guardarlos de otra forma, los adapta sola al abrirlos; antes deja los ficheros tal como estaban en `datos/copia_vN/` (N = el formato de antes). La copia cifrada que llega del servidor se adapta igual antes de juntarla con la tuya. Una app **más antigua** que los datos no los abre ni los sincroniza: avisa de que hay que actualizarla, sin tocar nada. Por eso, si usas `actualizar_app.sh --volver` después de un cambio de formato, la versión vieja se negará a abrir; para volver del todo hay que recuperar también los datos de `datos/copia_vN/` (o de `backups/`), perdiendo lo apuntado desde entonces. Cómo se escribe una migración: ver el principio de `cartera/datos.py`. ## Varias operaciones a la vez y justificantes En **Operaciones**, cada fila tiene su casilla y la de la cabecera marca todas las que cumplen los filtros (aunque no quepan en pantalla). Con alguna marcada aparece una barra para asignarlas a una actividad, confirmarlas, cambiarlas de activo o borrarlas. Cada acción se aplica a todas o a ninguna: si una falla (por ejemplo, una venta que en el activo nuevo no tendría saldo), no cambia nada. Al editar una operación, en **Justificantes** se adjuntan el extracto del banco, la factura… (PDF, imagen u hoja de cálculo, hasta 5 MB y 10 por operación). Si la operación la trajo tu nodo de bitcoin, ahí aparece su txid. Los ficheros van a `datos/adjuntos/`, nombrados por su huella SHA-256; quitar uno de una operación no borra el fichero. No entran en las copias de `backups/` (serían 30 veces lo mismo): su copia es la del servidor git, en paquetes cifrados aparte que se suben una sola vez.