Metadata-Version: 2.4
Name: amortizacion-engine
Version: 0.1.0
Summary: Motor puro y stateless de tablas de amortizacion con 13 modalidades de credito.
Project-URL: Repository, https://github.com/jesusmrv81/amortizacion-engine
Project-URL: Documentation, https://github.com/jesusmrv81/amortizacion-engine#readme
Author-email: Jesus Ramirez <jesusmarcos81@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Jesus Ramirez
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: amortization,credit,finance,interest,loan,schedule
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.10
Provides-Extra: pydantic
Requires-Dist: pydantic>=2.0; extra == 'pydantic'
Description-Content-Type: text/markdown

# amortizacion-engine

Motor **puro y stateless** de tablas de amortización en Python: 13 modalidades
de crédito con precisión `decimal.Decimal` exacta, API mínima (`simulate()`),
cero dependencias duras y resultados serializables (`to_dict()` / `to_json()`).

[![PyPI](https://img.shields.io/pypi/v/amortizacion-engine.svg)](https://pypi.org/project/amortizacion-engine/)
[![Python](https://img.shields.io/pypi/pyversions/amortizacion-engine.svg)](https://pypi.org/project/amortizacion-engine/)
[![CI](https://img.shields.io/github/actions/workflow/status/jesusmrv81/amortizacion-engine/ci.yml?branch=master&label=CI)](https://github.com/jesusmrv81/amortizacion-engine/actions)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/jesusmrv81/amortizacion-engine)
[![License](https://img.shields.io/github/license/jesusmrv81/amortizacion-engine.svg)](LICENSE)

## Tabla de contenidos

- [Instalacion](#instalacion)
- [Quickstart](#quickstart)
- [Las 13 modalidades](#las-13-modalidades)
- [Comparativa rapida de sistemas](#comparativa-rapida-de-sistemas)
- [Ejemplos por modalidad](#ejemplos-por-modalidad)
- [Principios de diseno](#principios-de-diseno)
- [API publica](#api-publica)
- [Desarrollo](#desarrollo)
- [Contributing](#contributing)
- [FAQ](#faq)
- [Licencia](#licencia)

## Caracteristicas clave

- **Precision decimal exacta**: todo monto y tasa se calcula con
  `decimal.Decimal`, nunca `float`. El cuadre de capital está garantizado:
  `sum(principal_component) == principal` y saldo final en `0.00`.
- **13 modalidades de credito**: interes simple, compuesto, sistema frances,
  aleman, americano, con enganche, pagos extraordinarios, sin intereses, tasa
  variable, pagos personalizados, periodo de gracia, cargos/seguros e
  indexado a inflacion.
- **API minima**: una sola funcion de entrada, `simulate()`, con conversion de
  tipos amigable y semantica *request XOR kwargs*.
- **Cero dependencias duras**: solo libreria estandar (extra opcional
  `pydantic`).
- **Stateless y determinista**: funciones puras y `dataclasses(frozen=True)`;
  las fechas siempre llegan como parametro, nunca `datetime.now()` implicito.
- **Serializable**: `to_dict()` y `to_json()` en todos los resultados, sin
  dependencias externas (Decimal como `str` para roundtrip JSON exacto).
- **Extensible por Strategy**: cada modalidad es una estrategia de
  `AmortizationStrategy`; anadir la modalidad 14 no rompe nada existente.
- **Errores propios**: jerarquia `FinancialEngineError` con subclases
  accionables; validacion agresiva, nunca `assert`.

## Instalacion

Requiere **Python >= 3.10** y no tiene dependencias duras (solo libreria
estandar; el extra `pydantic` es opcional).

```bash
pip install amortizacion-engine
```

Con soporte opcional de Pydantic (validacion adicional de los modelos):

```bash
pip install "amortizacion-engine[pydantic]"
```

## Quickstart

Genera una tabla de amortizacion en menos de 10 lineas: un credito frances de
15 000 a 24 % anual en 12 meses.

```python
from financial_engine import simulate, AmortizationMode

result = simulate(
    mode=AmortizationMode.FRENCH_FIXED_PAYMENT,
    principal="15000.00",
    annual_rate="0.24",
    periods=12,
    start_date="2026-08-09",
)
print(result.summary.total_interest)
for row in result.schedule:
    print(row.period, row.payment, row.principal_component, row.interest_component, row.balance)
```

Salida (interes total del credito y las 12 filas del calendario):

```
2020.68

1 1418.39 1118.39 300.00 13881.61
2 1418.39 1140.76 277.63 12740.85
3 1418.39 1163.57 254.82 11577.28
4 1418.39 1186.84 231.55 10390.44
5 1418.39 1210.58 207.81 9179.86
6 1418.39 1234.79 183.60 7945.07
7 1418.39 1259.49 158.90 6685.58
8 1418.39 1284.68 133.71 5400.90
9 1418.39 1310.37 108.02 4090.53
10 1418.39 1336.58 81.81 2753.95
11 1418.39 1363.31 55.08 1390.64
12 1418.39 1390.64 27.75 0.00
```

Cada fila del calendario es una `PaymentRow` inmutable con
`payment`, `principal_component`, `interest_component`, `fees_component`,
`balance` y `extra_payment`; el resumen (`summary`) agrega los totales.

## Las 13 modalidades

Cada modalidad corresponde a un miembro del enum `AmortizationMode` y se
implementa como una estrategia del patron Strategy.

| Modalidad (`AmortizationMode`) | Sistema | Descripcion |
| --- | --- | --- |
| `SIMPLE_INTEREST` | Interes simple | Interes simple sobre saldo insoluto, sin capitalizacion. |
| `COMPOUND_INTEREST` | Interes compuesto | Interes compuesto con capitalizacion periodica configurable (mensual, quincenal o anual). |
| `FRENCH_FIXED_PAYMENT` | Sistema frances | Cuota fija, amortizacion creciente, interes decreciente. |
| `GERMAN_CONSTANT_AMORTIZATION` | Sistema aleman | Amortizacion de capital constante, cuota decreciente. |
| `AMERICAN_BULLET` | Sistema americano | Solo interes en cada periodo, capital al vencimiento. |
| `DOWN_PAYMENT_ADJUSTED` | Con enganche/anticipo | Igual que frances/aleman pero descontando un enganche inicial del principal. |
| `EXTRA_PAYMENTS` | Pagos extraordinarios | Abonos a capital con dos submodalidades: reducir plazo o reducir cuota. |
| `ZERO_INTEREST` | Sin intereses | Tasa 0 %, divide el principal entre el numero de periodos. |
| `VARIABLE_RATE` | Tasa variable | Acepta un calendario de tasas distintas por periodo o rango de periodos. |
| `CUSTOM_SCHEDULE` | Pagos personalizados | El usuario define manualmente el monto de cada pago. |
| `GRACE_PERIOD_DEFERRED` | Periodo de gracia | N periodos iniciales sin pago de capital (solo interes, o totalmente diferido con capitalizacion). |
| `FEES_AND_INSURANCE` | Con cargos/seguro | Igual que frances/aleman sumando cargos periodicos fijos o porcentuales a cada cuota. |
| `INDEXED_INFLATION` | Indexado a inflacion | Ajusta el saldo o la cuota periodo a periodo segun un indice externo (UDI, INPC, etc.). |

## Comparativa rapida de sistemas

Los cuatro sistemas clasicos frente a frente. La base comun es un credito de
15 000 a 24 % anual en 12 meses; **excepciones**: `AMERICAN_BULLET` usa
10 000 a 12 % (como en los ejemplos de modalidad) y `SIMPLE_INTEREST` usa
1 200 a 12 % (tambien como en sus ejemplos), para mantener la coherencia con
el resto del documento.

| Sistema | Cuota inicial | Interes total | Total pagado |
| --- | ---: | ---: | ---: |
| Frances (`FRENCH_FIXED_PAYMENT`) | 1418.39 | 2020.68 | 17020.68 |
| Aleman (`GERMAN_CONSTANT_AMORTIZATION`) | 1550.00 (1250 + 300) | 1950.00 | 16950.00 |
| Americano (`AMERICAN_BULLET`, 10 000 a 12 %) | 100.00 | 1200.00 | 11200.00 |
| Interes simple (`SIMPLE_INTEREST`, 1 200 a 12 %) | 112.00 | 78.00 | 1278.00 |

## Ejemplos por modalidad

Todos los ejemplos usan la API publica `simulate()` con los mismos datos de
partida (`start_date="2026-08-09"`) y los valores comentados son la salida
real verificada contra la libreria.

### 1. `SIMPLE_INTEREST` — interes simple

```python
r = simulate(
    mode=AmortizationMode.SIMPLE_INTEREST,
    principal="1200.00", annual_rate="0.12", periods=12,
    start_date="2026-08-09",
)
print(r.summary.total_interest)          # 78.00
print(r.summary.total_paid)              # 1278.00
print(r.schedule[0].interest_component)  # 12.00 (decrece hasta 1.00)
```

### 2. `COMPOUND_INTEREST` — interes compuesto (zero-coupon)

```python
r = simulate(
    mode=AmortizationMode.COMPOUND_INTEREST,
    principal="1000.00", annual_rate="0.12", periods=12,
    start_date="2026-08-09",
)
print(r.summary.total_interest)  # 126.84
print(r.schedule[-1].payment)    # 1126.84 (capital + interes al vencimiento)
```

### 3. `FRENCH_FIXED_PAYMENT` — sistema frances

```python
r = simulate(
    mode=AmortizationMode.FRENCH_FIXED_PAYMENT,
    principal="15000.00", annual_rate="0.24", periods=12,
    start_date="2026-08-09",
)
print(r.schedule[0].payment)     # 1418.39 (cuota fija)
print(r.summary.total_interest)  # 2020.68
print(r.summary.total_paid)      # 17020.68
```

### 4. `GERMAN_CONSTANT_AMORTIZATION` — sistema aleman

```python
r = simulate(
    mode=AmortizationMode.GERMAN_CONSTANT_AMORTIZATION,
    principal="15000.00", annual_rate="0.24", periods=12,
    start_date="2026-08-09",
)
print(r.schedule[0].interest_component)   # 300.00 (decrece linealmente)
print(r.schedule[-1].interest_component)  # 25.00
print(r.summary.total_interest)           # 1950.00
print(r.summary.total_paid)               # 16950.00
```

### 5. `AMERICAN_BULLET` — sistema americano

```python
r = simulate(
    mode=AmortizationMode.AMERICAN_BULLET,
    principal="10000.00", annual_rate="0.12", periods=12,
    start_date="2026-08-09",
)
print(r.schedule[0].payment)    # 100.00 (solo interes)
print(r.schedule[-1].payment)   # 10100.00 (capital + interes final)
print(r.summary.total_interest) # 1200.00
print(r.summary.total_paid)     # 11200.00
```

### 6. `DOWN_PAYMENT_ADJUSTED` — con enganche

El enganche se paga fuera de la tabla; el motor amortiza con sistema frances
el capital efectivo `principal - down_payment`.

```python
r = simulate(
    mode=AmortizationMode.DOWN_PAYMENT_ADJUSTED,
    principal="15000.00", annual_rate="0.24", periods=12,
    down_payment="3000.00", start_date="2026-08-09",
)
print(r.schedule[0].principal_component)   # 894.72
print(r.schedule[0].balance)               # 11105.28
print(r.summary.total_interest)            # 1616.64
print(sum(x.principal_component for x in r.schedule))  # 12000.00 (cuadre exacto)
```

### 7. `EXTRA_PAYMENTS` — pagos extraordinarios

Dos submodalidades via `extra_payment_behavior`. Abono de 2 000 en el
periodo 6 sobre el credito frances de 15 000:

```python
r = simulate(
    mode=AmortizationMode.EXTRA_PAYMENTS,
    principal="15000.00", annual_rate="0.24", periods=12,
    extra_payments=[{"period": 6, "amount": "2000.00"}],
    extra_payment_behavior="reduce_term",   # misma cuota, plazo mas corto
    start_date="2026-08-09",
)
print(r.schedule[5].extra_payment)   # 2000.00
print(r.schedule[5].balance)         # 5945.07
print(r.summary.periods_count)       # 11 (el plazo se acorto)
print(r.summary.total_interest)      # 1784.76
```

Y con `reduce_payment` (mismo plazo, cuota menor):

```python
r = simulate(
    mode=AmortizationMode.EXTRA_PAYMENTS,
    principal="15000.00", annual_rate="0.24", periods=12,
    extra_payments=[{"period": 6, "amount": "2000.00"}],
    extra_payment_behavior="reduce_payment",
    start_date="2026-08-09",
)
print(r.schedule[0].principal_component)  # 950.46
print(r.summary.total_interest)           # 2005.54
print(r.summary.total_paid)               # 17005.54
```

### 8. `ZERO_INTEREST` — sin intereses

La tasa anual debe ser exactamente `0.00`; cada cuota es `principal / n`.

```python
r = simulate(
    mode=AmortizationMode.ZERO_INTEREST,
    principal="6000.00", annual_rate="0.00", periods=12,
    start_date="2026-08-09",
)
print(r.schedule[0].payment)      # 500.00
print(r.summary.total_interest)   # 0.00
print(r.summary.total_paid)       # 6000.00
```

### 9. `VARIABLE_RATE` — tasa variable

Base de amortizacion constante; `rate_schedule` cambia la tasa del periodo 7
en adelante al 30 % anual (la tasa vigente se expone en
`row.extra_data["annual_rate"]`):

```python
r = simulate(
    mode=AmortizationMode.VARIABLE_RATE,
    principal="15000.00", annual_rate="0.24", periods=12,
    rate_schedule=[{"period_from": 7, "period_to": None, "annual_rate": "0.30"}],
    start_date="2026-08-09",
)
print(r.schedule[0].interest_component)    # 300.00 (24 % anual)
print(r.schedule[6].interest_component)    # 187.50 (30 % anual)
print(r.summary.total_interest)            # 2081.25
print(r.summary.total_paid)                # 17081.25
```

### 10. `CUSTOM_SCHEDULE` — pagos personalizados

El usuario define el monto de cada pago (`custom_payments`); el motor
distribuye cada pago entre interes y capital. Solo se emiten filas para los
periodos pagados.

```python
r = simulate(
    mode=AmortizationMode.CUSTOM_SCHEDULE,
    principal="10000.00", annual_rate="0.12", periods=12,
    custom_payments=[
        {"period": 1, "amount": "1100.00"},
        {"period": 6, "amount": "2100.00"},
        {"period": 12, "amount": "7000.00"},
    ],
    start_date="2026-08-09",
)
print(r.schedule[0].principal_component)  # 1000.00
print(r.schedule[-1].payment)             # 7059.90 (ultima fila cuadrada)
print(r.summary.total_interest)           # 259.90
print(r.summary.total_paid)               # 10259.90
```

### 11. `GRACE_PERIOD_DEFERRED` — periodo de gracia

Submodalidad `interest_only` (se paga solo interes durante la gracia):

```python
r = simulate(
    mode=AmortizationMode.GRACE_PERIOD_DEFERRED,
    principal="15000.00", annual_rate="0.24", periods=12,
    grace_periods=3, grace_period_behavior="interest_only",
    start_date="2026-08-09",
)
print(r.schedule[0].interest_component)  # 300.00
print(r.summary.total_interest)          # 2439.57
print(r.summary.total_paid)              # 17439.57
```

Submodalidad `deferred` (sin pagos, el interes se capitaliza en el saldo):

```python
r = simulate(
    mode=AmortizationMode.GRACE_PERIOD_DEFERRED,
    principal="15000.00", annual_rate="0.24", periods=12,
    grace_periods=3, grace_period_behavior="deferred",
    start_date="2026-08-09",
)
print(r.metadata["principal_effective"])     # 15918.12 (saldo capitalizado)
print(r.metadata["capitalized_interest"])    # 918.12
print(r.summary.total_interest)              # 2551.98
print(r.summary.total_paid)                  # 17551.98
```

### 12. `FEES_AND_INSURANCE` — con cargos/seguro

Base francesa mas un cargo porcentual sobre saldo (`seguro`, 2 % anual,
cobrado mensualmente) en `fees_component`:

```python
r = simulate(
    mode=AmortizationMode.FEES_AND_INSURANCE,
    principal="15000.00", annual_rate="0.24", periods=12,
    fees=[{"name": "seguro", "rate": "0.02"}],
    start_date="2026-08-09",
)
print(r.schedule[0].fees_component)   # 25.00 (decrece con el saldo)
print(r.schedule[-1].fees_component)  # 2.32
print(r.summary.total_fees)           # 168.40
print(r.summary.total_paid)           # 17189.08
```

### 13. `INDEXED_INFLATION` — indexado a inflacion

El saldo se ajusta por el factor compuesto del indice (un factor por
periodo) y el capital efectivo se amortiza en sistema frances:

```python
r = simulate(
    mode=AmortizationMode.INDEXED_INFLATION,
    principal="1000.00", annual_rate="0.12", periods=12,
    inflation_index=["1.004"] * 12,
    start_date="2026-08-09",
)
print(r.metadata["principal_effective"])  # 1049.07
print(r.summary.total_interest)           # 69.45
print(r.summary.total_paid)               # 1118.52
```

## Principios de diseno

- **Precision decimal exacta**: todo monto y tasa se calcula con
  `decimal.Decimal`, nunca `float`. El redondeo es explicito y configurable
  (`rounding="ROUND_HALF_UP"` por defecto) y el cuadre de capital esta
  garantizado: `sum(principal_component) == principal` y saldo final en
  `0.00`.
- **Sin estado (stateless)**: funciones puras y `dataclasses(frozen=True)`
  para entradas y salidas. Cada calculo recibe todo como parametro y
  devuelve un resultado inmutable; no hay persistencia ni efectos
  secundarios.
- **Determinista**: las fechas siempre llegan como parametro
  (`date` o `str` ISO), nunca `datetime.now()` implicito. Misma entrada,
  mismo resultado exacto.
- **Inmutabilidad matematica**: una vez publicada, una modalidad no se
  modifica. Si hay que cambiar una formula se crea una nueva modalidad
  (p.ej. `..._V2`), nunca se altera la existente.
- **Schema de salida fijo**: `AmortizationResult` y `PaymentRow` exponen
  siempre los mismos campos en las 13 modalidades; los montos no aplicables
  se emiten como `0.00` y los datos especificos de modalidad viajan en
  `extra_data` / `metadata`.
- **Extensible**: cada modalidad es una estrategia de la interfaz
  `AmortizationStrategy`; anadir la modalidad 14 no rompe nada existente.
- **Serializable**: `to_dict()` y `to_json()` en todos los resultados, sin
  dependencias externas (los `Decimal` se serializan como `str` para un
  roundtrip JSON exacto; las fechas como ISO 8601).
- **Errores propios**: jerarquia `FinancialEngineError` con subclases
  accionables; validacion agresiva, nunca `assert`.

## API publica

### `simulate(mode, request=None, **kwargs) -> AmortizationResult`

Unica funcion de entrada de la libreria. Resuelve la modalidad, construye la
`AmortizationRequest`, ejecuta la estrategia y devuelve el resultado
inmutable.

- **Semantica request XOR kwargs**: se pasa un `AmortizationRequest`
  preconstruido **o** campos sueltos en `**kwargs`, nunca ambos (pasar ambos
  lanza `InvalidParametersError`). `mode` es obligatorio en ambos casos.
- **Conversion de tipos amigable** en `**kwargs`: montos y tasas como
  `str`/`int`/`Decimal` (el `float` se rechaza explicitamente); fechas ISO
  como `str`; listas de `ExtraPayment`/`RateChange`/`Fee`/`CustomPayment`
  como dataclasses o dicts planos; `annual_rate` es obligatorio.
- Los campos opcionales con valor por defecto son `periodicity="monthly"` y
  `rounding="ROUND_HALF_UP"`.

### `AmortizationMode`

Enum con los 13 miembros listados en la tabla anterior. Tambien acepta el
nombre como `str` (el valor del enum es identico al nombre, p.ej.
`"FRENCH_FIXED_PAYMENT"`).

### `AmortizationRequest`

Dataclass `frozen=True` con los campos de entrada (los opcionales son
`None` cuando no aplican):

- `principal: Decimal` — monto del credito (obligatorio, no negativo).
- `annual_rate: Decimal` — tasa anual como fraccion, p.ej. `0.24` = 24 %
  (obligatorio en todas las modalidades; `ZERO_INTEREST` exige `0.00`).
- `periods: int` — numero de periodos (>= 1).
- `periodicity: Literal["monthly","biweekly","weekly","annual"]`.
- `start_date: date` — base del calendario (el periodo 1 cae en
  `start_date + un periodo`).
- `rounding: str = "ROUND_HALF_UP"` — modo de redondeo de `decimal`.
- `down_payment: Decimal | None` — enganche descontado del principal.
- `extra_payments: list[ExtraPayment] | None` — abonos a capital.
- `rate_schedule: list[RateChange] | None` — cambios de tasa por rango.
- `fees: list[Fee] | None` — cargos fijos o porcentuales.
- `grace_periods: int | None` — periodos iniciales con diferimiento.
- `inflation_index: list[Decimal] | None` — factores de ajuste por periodo.
- `extra_payment_behavior: Literal["reduce_term","reduce_payment"] =
  "reduce_term"`.
- `grace_period_behavior: Literal["deferred","interest_only"] =
  "deferred"`.
- `custom_payments: list[CustomPayment] | None` — pagos manuales por
  periodo.

Dataclasses auxiliares (todas `frozen=True`): `ExtraPayment(period,
amount)`, `RateChange(period_from, period_to, annual_rate)`, `Fee(name,
amount=None, rate=None, periodicity="monthly")` y `CustomPayment(period,
amount)`.

### Resultado: `AmortizationResult`

- `mode: AmortizationMode` — modalidad aplicada.
- `schedule: list[PaymentRow]` — calendario de pagos.
- `summary: AmortizationSummary` — totales agregados.
- `metadata: dict` — parametros originales del calculo (trazabilidad) y
  datos de contexto de la modalidad.
- `to_dict() -> dict` y `to_json() -> str`.

`PaymentRow` (schema fijo): `period`, `date`, `payment`,
`principal_component`, `interest_component`, `fees_component`, `balance`,
`extra_payment` y `extra_data` (dict opcional con datos especificos de la
modalidad, p.ej. `index_factor`, `capitalized_interest`).

`AmortizationSummary`: `total_paid`, `total_interest`, `total_fees`,
`effective_rate` (tasa periodica) y `periods_count`; con `to_dict()` /
`to_json()`.

### Excepciones

Jerarquia propia que hereda de `FinancialEngineError`:

- `InvalidParametersError` — parametros que violan el contrato de entrada
  (periodos < 1, calendarios inconsistentes, `request` + `kwargs` a la vez,
  `float` en un monto, `annual_rate=None`, etc.).
- `NegativePrincipalError` — principal negativo.
- `RateOutOfRangeError` — tasa anual fuera de `[0, 1]` (inclusive).
- `ScheduleMismatchError` — el calendario no cuadra con la solicitud.

Captura la raiz `except FinancialEngineError` o la subclase concreta segun
lo que necesites.

## Desarrollo

Clonar el repo, crear el venv e instalar en modo editable:

```bash
git clone https://github.com/jesusmrv81/amortizacion-engine.git
cd amortizacion-engine
python3.12 -m venv .venv
.venv/bin/pip install -e .
.venv/bin/pip install -r requirements-dev.txt
```

Ejecutar la suite completa con cobertura (objetivo >= 90 %):

```bash
.venv/bin/pytest --cov=financial_engine --cov-report=term-missing
```

Lint y formato:

```bash
.venv/bin/ruff check .
.venv/bin/black --check .
```

Los docstrings del API publico incluyen ejemplos verificables; se pueden
ejecutar como doctests con pytest:

```bash
.venv/bin/pytest --doctest-modules src/financial_engine/ -q
```

## Contributing

Las contribuciones son bienvenidas:

1. Haz un fork del repositorio y crea una rama para tu cambio.
2. Anade o actualiza los tests (`pytest`) y verifica que la cobertura se
   mantiene en el 100 %.
3. Ejecuta `ruff check .` y `black .` antes de abrir el PR.
4. **Respeto a la inmutabilidad matematica**: no modifiques la formula de
   una modalidad ya publicada. Si necesitas cambiar una formula, crea una
   modalidad nueva (p.ej. `FRENCH_FIXED_PAYMENT_V2`) y registrala en
   `AmortizationMode`.

## FAQ

**¿Por que `Decimal` y no `float`?**

El dinero no admite errores de punto flotante. `decimal.Decimal` representa
montos y tasas de forma exacta y permite un control explicito del redondeo
(`ROUND_HALF_UP` por defecto), lo que garantiza que dos calculos identicos
produzcan siempre el mismo resultado exacto y que el cuadre del capital
cierre al centavo.

**¿Como garantiza el cuadre del capital?**

El motor concilia la ultima fila de cada calendario
(`reconcile_last_payment`): ajusta el ultimo pago para que
`sum(principal_component)` sea exactamente igual al `principal` original y
el saldo final quede en `0.00`, sin arrastrar redondeos intermedios.

**¿Puedo usar fechas reales?**

Si. La fecha base del calendario se pasa como parametro
(`start_date`, como `datetime.date` o `str` ISO `"YYYY-MM-DD"`) y el motor
calcula las fechas de cada periodo a partir de ella. El motor es
determinista: nunca usa `datetime.now()` implicito.

**¿Es libre?**

Si, esta bajo licencia MIT. Puedes usarlo, modificarlo y distribuirlo en
proyectos comerciales o personales sin restricciones (ver [LICENSE](LICENSE)).

**¿Como anado una modalidad nueva?**

Implementa una clase que herede de `AmortizationStrategy` con su metodo
`compute()`, registrala en el mapa de estrategias y anade el miembro
correspondiente a `AmortizationMode`. El resto de la API (validacion,
schema de salida, serializacion) funciona sin cambios. Nunca modifiques una
modalidad ya publicada: crea una `_V2`.

## Licencia

MIT — ver [LICENSE](LICENSE).
