catalogmx 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- catalogmx/__init__.py +133 -19
- catalogmx/calculators/__init__.py +113 -0
- catalogmx/calculators/costo_trabajador.py +213 -0
- catalogmx/calculators/impuestos.py +920 -0
- catalogmx/calculators/imss.py +370 -0
- catalogmx/calculators/isr.py +290 -0
- catalogmx/calculators/resico.py +154 -0
- catalogmx/catalogs/banxico/__init__.py +29 -3
- catalogmx/catalogs/banxico/cetes_sqlite.py +279 -0
- catalogmx/catalogs/banxico/inflacion_sqlite.py +302 -0
- catalogmx/catalogs/banxico/salarios_minimos_sqlite.py +295 -0
- catalogmx/catalogs/banxico/tiie_sqlite.py +279 -0
- catalogmx/catalogs/banxico/tipo_cambio_usd_sqlite.py +255 -0
- catalogmx/catalogs/banxico/udis_sqlite.py +332 -0
- catalogmx/catalogs/cnbv/__init__.py +9 -0
- catalogmx/catalogs/cnbv/sectores.py +173 -0
- catalogmx/catalogs/conapo/__init__.py +15 -0
- catalogmx/catalogs/conapo/sistema_urbano_nacional.py +50 -0
- catalogmx/catalogs/conapo/zonas_metropolitanas.py +230 -0
- catalogmx/catalogs/ift/__init__.py +1 -1
- catalogmx/catalogs/ift/codigos_lada.py +517 -313
- catalogmx/catalogs/inegi/__init__.py +17 -0
- catalogmx/catalogs/inegi/scian.py +127 -0
- catalogmx/catalogs/mexico/__init__.py +2 -0
- catalogmx/catalogs/mexico/giros_mercantiles.py +119 -0
- catalogmx/catalogs/sat/carta_porte/material_peligroso.py +5 -1
- catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +78 -0
- catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +2 -1
- catalogmx/catalogs/sepomex/__init__.py +2 -1
- catalogmx/catalogs/sepomex/codigos_postales.py +30 -2
- catalogmx/catalogs/sepomex/codigos_postales_completo.py +261 -0
- catalogmx/cli.py +12 -9
- catalogmx/data/__init__.py +10 -0
- catalogmx/data/mexico_dynamic.sqlite3 +0 -0
- catalogmx/data/updater.py +362 -0
- catalogmx/generators/__init__.py +20 -0
- catalogmx/generators/identity.py +582 -0
- catalogmx/helpers.py +177 -3
- catalogmx/utils/__init__.py +29 -0
- catalogmx/utils/clabe_utils.py +417 -0
- catalogmx/utils/text.py +7 -1
- catalogmx/validators/clabe.py +52 -2
- catalogmx/validators/nss.py +32 -27
- catalogmx/validators/rfc.py +185 -52
- catalogmx-0.4.0.dist-info/METADATA +905 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/RECORD +51 -25
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/WHEEL +1 -1
- catalogmx/catalogs/banxico/udis.py +0 -279
- catalogmx-0.3.0.dist-info/METADATA +0 -644
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/entry_points.txt +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RESICO (Régimen Simplificado de Confianza) Calculator for Mexico
|
|
3
|
+
Supports years: 2024, 2025, 2026
|
|
4
|
+
|
|
5
|
+
RESICO uses a DIRECT TAX RATE system (simpler than ISR):
|
|
6
|
+
- No deductions allowed
|
|
7
|
+
- No fixed fee (cuota fija)
|
|
8
|
+
- Tax = Total Income × Bracket Rate
|
|
9
|
+
|
|
10
|
+
Official source: Artículo 113-E LISR (Ley del Impuesto Sobre la Renta)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal, TypedDict
|
|
16
|
+
|
|
17
|
+
RESICOYear = Literal[2024, 2025, 2026]
|
|
18
|
+
RESICOPeriod = Literal["mensual", "anual"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RESICOBracket(TypedDict):
|
|
22
|
+
"""RESICO tax bracket structure (simplified - no cuota fija)"""
|
|
23
|
+
|
|
24
|
+
limiteInferior: float
|
|
25
|
+
limiteSuperior: float
|
|
26
|
+
tasa: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RESICOCalculationResult(TypedDict):
|
|
30
|
+
"""Complete RESICO calculation result"""
|
|
31
|
+
|
|
32
|
+
ingreso: float
|
|
33
|
+
periodo: str
|
|
34
|
+
year: int
|
|
35
|
+
limiteMaximo: float
|
|
36
|
+
dentroDeLimite: bool
|
|
37
|
+
bracket: dict
|
|
38
|
+
resicoCalculado: float
|
|
39
|
+
tasaEfectiva: float
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Load RESICO tables from centralized JSON
|
|
43
|
+
_RESICO_TABLES: dict | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_resico_tables() -> dict:
|
|
47
|
+
"""Load RESICO tables from shared JSON file"""
|
|
48
|
+
global _RESICO_TABLES
|
|
49
|
+
if _RESICO_TABLES is None:
|
|
50
|
+
json_path = (
|
|
51
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
52
|
+
/ "packages"
|
|
53
|
+
/ "shared-data"
|
|
54
|
+
/ "resico-tables.json"
|
|
55
|
+
)
|
|
56
|
+
with open(json_path, encoding="utf-8") as f:
|
|
57
|
+
_RESICO_TABLES = json.load(f)
|
|
58
|
+
return _RESICO_TABLES
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_resico_brackets(year: RESICOYear, periodo: RESICOPeriod) -> list[RESICOBracket]:
|
|
62
|
+
"""
|
|
63
|
+
Get RESICO tax brackets for a specific year and period
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
year: Tax year (2024, 2025, or 2026)
|
|
67
|
+
periodo: Period (mensual or anual)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of RESICO tax brackets
|
|
71
|
+
"""
|
|
72
|
+
tables = _load_resico_tables()
|
|
73
|
+
year_str = str(year)
|
|
74
|
+
|
|
75
|
+
brackets_data = tables["brackets"][year_str][periodo]
|
|
76
|
+
|
|
77
|
+
brackets = []
|
|
78
|
+
for b in brackets_data:
|
|
79
|
+
bracket = RESICOBracket(
|
|
80
|
+
limiteInferior=b["limiteInferior"], limiteSuperior=b["limiteSuperior"], tasa=b["tasa"]
|
|
81
|
+
)
|
|
82
|
+
brackets.append(bracket)
|
|
83
|
+
|
|
84
|
+
return brackets
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def calculate_resico(
|
|
88
|
+
ingreso: float, periodo: RESICOPeriod = "mensual", year: RESICOYear = 2026
|
|
89
|
+
) -> RESICOCalculationResult:
|
|
90
|
+
"""
|
|
91
|
+
Calculate RESICO (Régimen Simplificado de Confianza) tax
|
|
92
|
+
|
|
93
|
+
RESICO uses a DIRECT TAX RATE system (not marginal like regular ISR):
|
|
94
|
+
- No deductions allowed
|
|
95
|
+
- No fixed fee (cuota fija)
|
|
96
|
+
- Tax = Total Income × Bracket Rate
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
ingreso: Taxable income for the period
|
|
100
|
+
periodo: Period (mensual or anual, default: mensual)
|
|
101
|
+
year: Tax year (default: 2026)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Complete RESICO calculation result
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> # Monthly income $50,000 → bracket 1.5% → ISR = $50,000 × 1.5% = $750
|
|
108
|
+
>>> result = calculate_resico(50000, "mensual", 2026)
|
|
109
|
+
>>> print(f"RESICO: ${result['resicoCalculado']:.2f}")
|
|
110
|
+
RESICO: $750.00
|
|
111
|
+
|
|
112
|
+
>>> # Annual income $500,000 → bracket 1.1% → ISR = $500,000 × 1.1% = $5,500
|
|
113
|
+
>>> result = calculate_resico(500000, "anual", 2026)
|
|
114
|
+
>>> print(f"RESICO anual: ${result['resicoCalculado']:.2f}")
|
|
115
|
+
RESICO anual: $5500.00
|
|
116
|
+
"""
|
|
117
|
+
tables = _load_resico_tables()
|
|
118
|
+
|
|
119
|
+
# Get income limits
|
|
120
|
+
limits = tables["limits"]["personaFisica"]
|
|
121
|
+
limite_maximo = (
|
|
122
|
+
limits["ingresoMensualMaximo"] if periodo == "mensual" else limits["ingresoAnualMaximo"]
|
|
123
|
+
)
|
|
124
|
+
dentro_de_limite = ingreso <= limite_maximo
|
|
125
|
+
|
|
126
|
+
# Get brackets for year and period
|
|
127
|
+
brackets = get_resico_brackets(year, periodo)
|
|
128
|
+
|
|
129
|
+
# Find applicable bracket
|
|
130
|
+
bracket = None
|
|
131
|
+
for b in brackets:
|
|
132
|
+
if b["limiteInferior"] <= ingreso <= b["limiteSuperior"]:
|
|
133
|
+
bracket = b
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
if bracket is None:
|
|
137
|
+
bracket = brackets[-1] # Last bracket if not found
|
|
138
|
+
|
|
139
|
+
# RESICO calculation: Direct rate on total income (NO cuota fija, NO excedente)
|
|
140
|
+
resico_calculado = ingreso * (bracket["tasa"] / 100)
|
|
141
|
+
|
|
142
|
+
# Effective rate (should equal bracket rate for RESICO)
|
|
143
|
+
tasa_efectiva = (resico_calculado / ingreso * 100) if ingreso > 0 else 0
|
|
144
|
+
|
|
145
|
+
return RESICOCalculationResult(
|
|
146
|
+
ingreso=ingreso,
|
|
147
|
+
periodo=periodo,
|
|
148
|
+
year=year,
|
|
149
|
+
limiteMaximo=limite_maximo,
|
|
150
|
+
dentroDeLimite=dentro_de_limite,
|
|
151
|
+
bracket=dict(bracket),
|
|
152
|
+
resicoCalculado=resico_calculado,
|
|
153
|
+
tasaEfectiva=tasa_efectiva,
|
|
154
|
+
)
|
|
@@ -4,21 +4,47 @@ catalogmx.catalogs.banxico - Catálogos de Banxico
|
|
|
4
4
|
Catálogos incluidos:
|
|
5
5
|
- BankCatalog: Bancos autorizados por Banxico
|
|
6
6
|
- UDICatalog: UDIs (Unidades de Inversión)
|
|
7
|
+
- TipoCambioUSDCatalog: Tipo de cambio USD/MXN FIX
|
|
8
|
+
- TIIE28Catalog: TIIE 28 días
|
|
9
|
+
- CETES28Catalog: CETES 28 días
|
|
10
|
+
- InflacionAnualCatalog: Inflación anual (INPC)
|
|
11
|
+
- SalariosMinimosCatalog: Salarios mínimos por zona
|
|
7
12
|
- InstitucionesFinancieras: Tipos de instituciones del sistema financiero
|
|
8
13
|
- MonedasDivisas: Monedas y divisas internacionales
|
|
9
14
|
- CodigosPlazaCatalog: Códigos de plaza para CLABE
|
|
10
15
|
"""
|
|
11
16
|
|
|
17
|
+
# Static catalogs (JSON-based, don't change frequently)
|
|
12
18
|
from .banks import BankCatalog
|
|
19
|
+
from .cetes_sqlite import CETESCatalog as CETES28Catalog
|
|
13
20
|
from .codigos_plaza import CodigosPlazaCatalog
|
|
21
|
+
from .inflacion_sqlite import InflacionCatalog as InflacionAnualCatalog
|
|
14
22
|
from .instituciones_financieras import InstitucionesFinancieras
|
|
15
23
|
from .monedas_divisas import MonedasDivisas
|
|
16
|
-
from .
|
|
24
|
+
from .salarios_minimos_sqlite import SalariosMinimosCatalog
|
|
25
|
+
from .tiie_sqlite import TIIECatalog as TIIE28Catalog
|
|
26
|
+
from .tipo_cambio_usd_sqlite import TipoCambioUSDCatalog
|
|
27
|
+
|
|
28
|
+
# Dynamic catalogs (SQLite-based, auto-updating)
|
|
29
|
+
from .udis_sqlite import UDICatalog
|
|
30
|
+
|
|
31
|
+
# Aliases for convenience
|
|
32
|
+
Banks = BankCatalog
|
|
33
|
+
Bancos = BankCatalog
|
|
17
34
|
|
|
18
35
|
__all__ = [
|
|
36
|
+
# Main classes
|
|
19
37
|
"BankCatalog",
|
|
20
|
-
"
|
|
38
|
+
"CETES28Catalog",
|
|
39
|
+
"CodigosPlazaCatalog",
|
|
40
|
+
"InflacionAnualCatalog",
|
|
21
41
|
"InstitucionesFinancieras",
|
|
22
42
|
"MonedasDivisas",
|
|
23
|
-
"
|
|
43
|
+
"SalariosMinimosCatalog",
|
|
44
|
+
"TIIE28Catalog",
|
|
45
|
+
"TipoCambioUSDCatalog",
|
|
46
|
+
"UDICatalog",
|
|
47
|
+
# Aliases
|
|
48
|
+
"Banks",
|
|
49
|
+
"Bancos",
|
|
24
50
|
]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CETES (Certificados de la Tesorería) Catalog - SQLite Backend
|
|
3
|
+
|
|
4
|
+
This module provides access to CETES rates from Banco de México using a SQLite
|
|
5
|
+
database that can be automatically updated without requiring library releases.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from catalogmx.data.updater import get_database_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CETESCatalog:
|
|
15
|
+
"""
|
|
16
|
+
Catalog of CETES (Certificados de la Tesorería) rates
|
|
17
|
+
|
|
18
|
+
CETES are short-term Mexican treasury bills and serve as the
|
|
19
|
+
risk-free rate benchmark for the Mexican market.
|
|
20
|
+
|
|
21
|
+
This catalog uses an auto-updating SQLite database backend.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_db_path: Path | None = None
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _get_db_path(cls) -> Path:
|
|
28
|
+
"""Get path to database with auto-update"""
|
|
29
|
+
if cls._db_path is None:
|
|
30
|
+
cls._db_path = get_database_path(auto_update=True, max_age_hours=24)
|
|
31
|
+
return cls._db_path
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _row_to_dict(cls, row: sqlite3.Row) -> dict:
|
|
35
|
+
"""Convert SQLite row to dictionary"""
|
|
36
|
+
return {
|
|
37
|
+
"fecha": row["fecha"],
|
|
38
|
+
"plazo": row["plazo"],
|
|
39
|
+
"tasa": row["tasa"],
|
|
40
|
+
"año": row["anio"],
|
|
41
|
+
"mes": row["mes"],
|
|
42
|
+
"instrumento": "CETES", # Add instrumento field for compatibility
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def get_data(cls, plazo: int = 28) -> list[dict]:
|
|
47
|
+
"""
|
|
48
|
+
Get all CETES data for a specific term
|
|
49
|
+
|
|
50
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
51
|
+
:return: List of all CETES records
|
|
52
|
+
"""
|
|
53
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
54
|
+
db.row_factory = sqlite3.Row
|
|
55
|
+
cursor = db.execute(
|
|
56
|
+
"""
|
|
57
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
58
|
+
FROM cetes
|
|
59
|
+
WHERE plazo = ?
|
|
60
|
+
ORDER BY fecha
|
|
61
|
+
""",
|
|
62
|
+
(plazo,),
|
|
63
|
+
)
|
|
64
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
65
|
+
db.close()
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_por_fecha(cls, fecha: str, plazo: int = 28) -> dict | None:
|
|
70
|
+
"""
|
|
71
|
+
Get CETES rate for a specific date and term
|
|
72
|
+
|
|
73
|
+
:param fecha: Date string in YYYY-MM-DD format
|
|
74
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
75
|
+
:return: CETES record or None if not found
|
|
76
|
+
"""
|
|
77
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
78
|
+
db.row_factory = sqlite3.Row
|
|
79
|
+
|
|
80
|
+
cursor = db.execute(
|
|
81
|
+
"""
|
|
82
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
83
|
+
FROM cetes
|
|
84
|
+
WHERE fecha = ? AND plazo = ?
|
|
85
|
+
LIMIT 1
|
|
86
|
+
""",
|
|
87
|
+
(fecha, plazo),
|
|
88
|
+
)
|
|
89
|
+
row = cursor.fetchone()
|
|
90
|
+
db.close()
|
|
91
|
+
|
|
92
|
+
if row:
|
|
93
|
+
return cls._row_to_dict(row)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def get_actual(cls, plazo: int = 28) -> dict | None:
|
|
98
|
+
"""
|
|
99
|
+
Get most recent CETES rate
|
|
100
|
+
|
|
101
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
102
|
+
:return: Latest CETES record
|
|
103
|
+
"""
|
|
104
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
105
|
+
db.row_factory = sqlite3.Row
|
|
106
|
+
|
|
107
|
+
cursor = db.execute(
|
|
108
|
+
"""
|
|
109
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
110
|
+
FROM cetes
|
|
111
|
+
WHERE plazo = ?
|
|
112
|
+
ORDER BY fecha DESC
|
|
113
|
+
LIMIT 1
|
|
114
|
+
""",
|
|
115
|
+
(plazo,),
|
|
116
|
+
)
|
|
117
|
+
row = cursor.fetchone()
|
|
118
|
+
db.close()
|
|
119
|
+
|
|
120
|
+
if row:
|
|
121
|
+
return cls._row_to_dict(row)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def get_valor_actual(cls, plazo: int = 28) -> float | None:
|
|
126
|
+
"""
|
|
127
|
+
Get current CETES rate value
|
|
128
|
+
|
|
129
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
130
|
+
:return: Current CETES rate or None
|
|
131
|
+
"""
|
|
132
|
+
record = cls.get_actual(plazo)
|
|
133
|
+
return record.get("tasa") if record else None
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def get_por_anio(cls, anio: int, plazo: int = 28) -> list[dict]:
|
|
137
|
+
"""
|
|
138
|
+
Get all CETES rates for a specific year and term
|
|
139
|
+
|
|
140
|
+
:param anio: Year (e.g., 2024)
|
|
141
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
142
|
+
:return: List of CETES records for the year
|
|
143
|
+
"""
|
|
144
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
145
|
+
db.row_factory = sqlite3.Row
|
|
146
|
+
|
|
147
|
+
cursor = db.execute(
|
|
148
|
+
"""
|
|
149
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
150
|
+
FROM cetes
|
|
151
|
+
WHERE anio = ? AND plazo = ?
|
|
152
|
+
ORDER BY fecha
|
|
153
|
+
""",
|
|
154
|
+
(anio, plazo),
|
|
155
|
+
)
|
|
156
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
157
|
+
db.close()
|
|
158
|
+
return results
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def calcular_variacion(cls, fecha_inicio: str, fecha_fin: str, plazo: int = 28) -> float | None:
|
|
162
|
+
"""
|
|
163
|
+
Calculate variation between two dates
|
|
164
|
+
|
|
165
|
+
:param fecha_inicio: Start date (YYYY-MM-DD)
|
|
166
|
+
:param fecha_fin: End date (YYYY-MM-DD)
|
|
167
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
168
|
+
:return: Variation in percentage points or None
|
|
169
|
+
"""
|
|
170
|
+
record_inicio = cls.get_por_fecha(fecha_inicio, plazo)
|
|
171
|
+
record_fin = cls.get_por_fecha(fecha_fin, plazo)
|
|
172
|
+
|
|
173
|
+
if not record_inicio or not record_fin:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
tasa_inicio = record_inicio.get("tasa")
|
|
177
|
+
tasa_fin = record_fin.get("tasa")
|
|
178
|
+
|
|
179
|
+
if tasa_inicio is None or tasa_fin is None:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Return difference in percentage points (not percentage change)
|
|
183
|
+
return tasa_fin - tasa_inicio
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def get_promedio_anual(cls, anio: int, plazo: int = 28) -> float | None:
|
|
187
|
+
"""
|
|
188
|
+
Calculate annual average CETES rate
|
|
189
|
+
|
|
190
|
+
:param anio: Year (e.g., 2024)
|
|
191
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
192
|
+
:return: Annual average rate or None if no data
|
|
193
|
+
"""
|
|
194
|
+
records = cls.get_por_anio(anio, plazo)
|
|
195
|
+
if not records:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
tasas = [r.get("tasa") for r in records if r.get("tasa") is not None]
|
|
199
|
+
return sum(tasas) / len(tasas) if tasas else None
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def get_tasa_actual(cls, plazo: int = 28) -> float | None:
|
|
203
|
+
"""
|
|
204
|
+
Get current CETES rate value (alias for get_valor_actual)
|
|
205
|
+
|
|
206
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
207
|
+
:return: Current CETES rate or None
|
|
208
|
+
"""
|
|
209
|
+
return cls.get_valor_actual(plazo)
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def calcular_rendimiento(
|
|
213
|
+
cls, monto: float, fecha_inicio: str, fecha_fin: str, plazo: int = 28
|
|
214
|
+
) -> float | None:
|
|
215
|
+
"""
|
|
216
|
+
Calculate CETES return (principal + interest)
|
|
217
|
+
|
|
218
|
+
:param monto: Initial investment amount
|
|
219
|
+
:param fecha_inicio: Start date (YYYY-MM-DD)
|
|
220
|
+
:param fecha_fin: End date (YYYY-MM-DD)
|
|
221
|
+
:param plazo: Term in days (28, 91, 182, or 364)
|
|
222
|
+
:return: Total return (principal + interest) or None if data not available
|
|
223
|
+
"""
|
|
224
|
+
from datetime import datetime
|
|
225
|
+
|
|
226
|
+
record_inicio = cls.get_por_fecha(fecha_inicio, plazo)
|
|
227
|
+
record_fin = cls.get_por_fecha(fecha_fin, plazo)
|
|
228
|
+
|
|
229
|
+
if not record_inicio and not record_fin:
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
# Use available rate (start or end, or average if both)
|
|
233
|
+
tasa = None
|
|
234
|
+
if record_inicio and record_fin:
|
|
235
|
+
tasa = (record_inicio.get("tasa", 0) + record_fin.get("tasa", 0)) / 2
|
|
236
|
+
elif record_inicio:
|
|
237
|
+
tasa = record_inicio.get("tasa")
|
|
238
|
+
elif record_fin:
|
|
239
|
+
tasa = record_fin.get("tasa")
|
|
240
|
+
|
|
241
|
+
if tasa is None:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
# Calculate days between dates
|
|
245
|
+
date_inicio = datetime.strptime(fecha_inicio, "%Y-%m-%d")
|
|
246
|
+
date_fin = datetime.strptime(fecha_fin, "%Y-%m-%d")
|
|
247
|
+
dias = (date_fin - date_inicio).days
|
|
248
|
+
|
|
249
|
+
if dias <= 0:
|
|
250
|
+
return monto
|
|
251
|
+
|
|
252
|
+
# Calculate simple interest return
|
|
253
|
+
interes = monto * (tasa / 100) * (dias / 365)
|
|
254
|
+
return round(monto + interes, 2)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Convenience functions for CETES 28 (most commonly used)
|
|
258
|
+
def get_cetes_actual(plazo: int = 28) -> dict | None:
|
|
259
|
+
"""Get most recent CETES rate"""
|
|
260
|
+
return CETESCatalog.get_actual(plazo)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_cetes_por_fecha(fecha: str, plazo: int = 28) -> dict | None:
|
|
264
|
+
"""Get CETES rate for a specific date"""
|
|
265
|
+
return CETESCatalog.get_por_fecha(fecha, plazo)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_cetes_valor_actual(plazo: int = 28) -> float | None:
|
|
269
|
+
"""Get current CETES rate value"""
|
|
270
|
+
return CETESCatalog.get_valor_actual(plazo)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Export commonly used functions and classes
|
|
274
|
+
__all__ = [
|
|
275
|
+
"CETESCatalog",
|
|
276
|
+
"get_cetes_actual",
|
|
277
|
+
"get_cetes_por_fecha",
|
|
278
|
+
"get_cetes_valor_actual",
|
|
279
|
+
]
|