Solução para o desafio técnico de construção da curva de juros zero-cupom (taxas spot) a partir de títulos públicos brasileiros (LTN e NTN-F), seguindo as convenções do mercado de renda fixa brasileiro (ANBIMA / B3, base 252 dias úteis).
bootstrapping/
├── main.py # ponto de entrada executável
├── feriados_nacionais_AMBIMA.xls # calendário de feriados (ANBIMA)
├── data/
│ └── anbima_20260601.txt # arquivo TXT de exemplo (data-base 2026-06-01)
├── src/
│ ├── __init__.py # API pública do pacote
│ ├── calendar_utils.py # contagem de dias úteis, ajuste para próximo dia útil, base 252
│ ├── parser.py # leitura do TXT da ANBIMA, geração de fluxos
│ └── bootstrap.py # montagem de C, resolução de Cd=P, curva spot
└── tests/
├── conftest.py # fixtures compartilhadas (holidays, caminhos)
├── test_calendar.py # 40 testes de calendar_utils
├── test_parser.py # 42 testes de parser
└── test_bootstrap.py # 52 testes de bootstrap (inclui R5)
Requisitos: Python 3.13+ e pip.
# 1. Clone ou descompacte o projeto e entre na pasta raiz
cd bootstrapping
# 2. Crie e ative o ambiente virtual
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
# 3. Instale as dependências
pip install -r requirements.txtConteúdo do requirements.txt:
xlrd # leitura do .xls de feriados da ANBIMA (formato legado Excel 97-2003)
xlwt # criação de .xls sintético em um teste de parser (opcional — teste é skipped se ausente)
python-dateutil # relativedelta para geração de datas de cupom semestral da NTN-F
pytest # execução dos testes automatizados
Não há dependência de numpy ou de bibliotecas de bootstrapping prontas.
# Usando os arquivos padrão (data/ e feriados na raiz do projeto)
python main.py
# Especificando arquivos alternativos
python main.py --txt data/anbima_20260601.txt --feriados feriados_nacionais_AMBIMA.xls
# Saída minificada (sem indentação)
python main.py --indent 0
# Ajuda completa
python main.py --helpO progresso é impresso em stderr; o JSON da curva vai para stdout.
Isso permite redirecionar a saída sem capturar ruído de log:
python main.py > curva.json{
"data_base": "2026-06-01",
"erro_reprecificacao": 0.0,
"curva": [
{
"data": "2026-07-01",
"du": 21,
"prazo_anos": 0.0833,
"fator_desconto": 0.988866,
"taxa_spot": 0.143798
},
{
"data": "2026-10-01",
"du": 86,
"prazo_anos": 0.3413,
"fator_desconto": 0.956259,
"taxa_spot": 0.140034
},
{
"data": "2027-01-01",
"du": 148,
"prazo_anos": 0.5873,
"fator_desconto": 0.925696,
"taxa_spot": 0.140498
}
]
}# Todos os testes (134 passando)
pytest tests/
# Com saída detalhada por teste
pytest tests/ -v
# Apenas um módulo
pytest tests/test_bootstrap.py -v
# Apenas o R5 (re-precificação obrigatória)
pytest tests/test_bootstrap.py -v -k "r5"
# Traceback compacto em caso de falha
pytest tests/ --tb=short* O teste
test_arquivo_sem_aba_feriados(emtest_calendar.py) é marcado comoskippedapenas se a bibliotecaxlwtnão estiver instalada — ela é usada para criar um.xlssintético naquele teste específico. Comoxlwtconsta norequirements.txt, a suite completa roda com 134 passed, 0 skipped.
| Módulo | Testes | O que é verificado |
|---|---|---|
calendar_utils |
40 | Parsing do .xls, classificação de dias úteis, contagem de DU, ajuste para próx. dia útil, base 252 |
parser |
42 | Conversão de datas e floats BR, geração de fluxos LTN/NTN-F, inputs inválidos |
bootstrap |
52 | Extração de vértices, matriz C, substituição progressiva, taxa spot, R5 |
O preço de mercado (PU) de qualquer título de renda fixa é a soma dos seus fluxos futuros descontados:
PU = Σᵢ Fluxo(i) × d(i)
onde d(i) é o fator de desconto incógnito para a data de pagamento i.
Com m títulos e n datas de pagamento distintas, isso forma um sistema
linear:
C · d = P
onde:
C ∈ ℝ^{m×n}— matriz de fluxos:C[j][i]é o valor futuro pago pelo títulojna datai(zero se não há pagamento nessa data)d ∈ ℝⁿ— vetor de fatores de desconto (incógnitas)P ∈ ℝᵐ— vetor de preços de mercado (PUs da ANBIMA)
Os títulos utilizados e seus fluxos (data-base 2026-06-01):
| Título | Vencimento | Fluxos | PU de mercado |
|---|---|---|---|
| LTN | 2026-07-01 | 2026-07-01 → R$ 1.000,00 | 988,866252 |
| LTN | 2026-10-01 | 2026-10-01 → R$ 1.000,00 | 956,259296 |
| NTN-F | 2027-01-01 | 2026-07-01 → R$ 48,81 ; 2027-01-01 → R$ 1.048,81 | 1019,143414 |
O cupom semestral da NTN-F (R$ 48,8088) não está no TXT da ANBIMA — é
derivado da taxa de cupom contratual de 10% a.a.:
Cupom = 1.000 × ((1,10)^(1/2) − 1) = 48,8088
A escolha de usar equivalência composta (não proporcional) é a convenção correta do mercado brasileiro: 5% ao semestre proporcional corresponderia a 10,25% ao ano, não a 10%.
- Base: 252 dias úteis por ano (convenção DCO/252 do mercado brasileiro)
- Contagem: intervalo
(D0, T]— a data-base não é contada, o vencimento sim - Feriados: calendário oficial da ANBIMA (arquivo
.xls, cobrindo 2001–2099) - Ajuste para o próximo dia útil: quando o vencimento contratual cai em
feriado ou fim de semana, o pagamento efetivo (liquidação financeira) ocorre
no próximo dia útil subsequente. O prazo de precificação (DU) é calculado
até essa data de liquidação efetiva. Por exemplo,
2027-01-01(Confraternização Universal, sexta-feira) tem liquidação em2027-01-04(segunda-feira), ecount_business_days(2026-06-01, 2027-01-04) = 148, reproduzindo oDU=148do enunciado. Essa lógica é implementada pornext_business_day()emcalendar_utils.py.
Ao ordenar os títulos por vencimento crescente, a matriz C torna-se triangular inferior:
┌ ┐ ┌ ┐ ┌ ┐
│ 1000,00 0 0 │ │ d1 │ │ 988,866 │
│ 0 1000,00 0 │ × │ d2 │ = │ 956,259 │
│ 48,81 0 1048,81│ │ d3 │ │ 1019,143 │
└ ┘ └ ┘ └ ┘
A triangularidade decorre de uma propriedade do conjunto de títulos escolhidos: cada título introduz exatamente uma nova data de pagamento. A LTN (zero-cupom) paga apenas no vencimento — zerando toda a linha à direita da diagonal. A NTN-F paga cupons em datas anteriores ao seu vencimento — que já foram cobertas pelos vértices das LTNs.
O sistema triangular inferior é resolvido linha a linha, da primeira para a última, sem necessidade de inversão de matriz:
d[j] = ( P[j] − Σ_{i<j} C[j][i] · d[i] ) / C[j][j]
Para o exemplo:
d1 = 988,866252 / 1000,00 = 0,988866
d2 = 956,259296 / 1000,00 = 0,956259
d3 = (1019,143414 − 48,8088 × 0,988866) / 1048,81 = 0,925696
Por que não usar numpy.linalg.solve?
numpy.linalg.solveusa decomposição LU — O(n³) — e não explora a triangularidade. A substituição progressiva é O(n²) e resolve o sistema na metade das operações.- Evita a dependência de numpy para o que é, essencialmente, um loop de 4 linhas.
Com os fatores de desconto em mãos, a taxa spot de cada vértice é obtida pela inversão da fórmula de capitalização composta:
d = 1 / (1+s)^p ⟹ s = d^(−1/p) − 1
onde p = DU / 252 é o prazo em anos.
Como verificação final, cada título é re-precificado usando a curva gerada:
PU_calc = Σᵢ Fluxo(i) × d(i)
O erro |PU_calc − PU_mercado| deve ser < 1e-4 para todos os títulos.
No exemplo do desafio, o erro é 0,0 exato — a resolução por substituição
progressiva é algébrica, sem aproximações numéricas.
Sanidade adicional para LTNs: como a LTN é zero-cupom, sua taxa spot
deve coincidir com a taxa indicativa publicada pela ANBIMA. Isso é verificado
explicitamente nos testes (test_vertice1_spot_igual_indicativa e
test_vertice2_spot_igual_indicativa).
| Etapa | Complexidade | Observação |
|---|---|---|
| Carregamento de feriados | O(F) | F = nº de feriados no arquivo (~1.260) |
| Verificação de dia útil | O(1) | frozenset garante busca em tempo constante |
| Contagem de DU por vértice | O(D) | D = dias corridos até o vencimento |
| Parsing do TXT | O(m) | m = nº de títulos no arquivo |
| Montagem da matriz C | O(m × n) | m títulos, n vértices |
| Substituição progressiva | O(n²) | ótimo para sistemas triangulares |
| Conversão para taxas spot | O(n) | uma potenciação por vértice |
| Re-precificação (R5) | O(m × n) | produto interno por título |
| Total | O(n² + m×n + D) | dominado pela substituição e contagem de DU |
Para o caso do desafio (n=3, m=3, D≤200), todas as etapas são
instantâneas. A solução escala linearmente com o número de títulos e
quadraticamente com o número de vértices — adequado para curvas de mercado
com dezenas a centenas de vértices.
frozenset para feriados: imutável e com busca O(1), contra O(n) de uma lista.
Faz diferença quando is_business_day é chamada para cada dia de um intervalo longo.
dataclass para Titulo e Vertice: campos tipados, repr automático para
debugging, sem a verbosidade de classes manuais.
stderr para progresso, stdout para JSON: permite redirecionar a saída
para arquivo sem capturar mensagens de log (python main.py > curva.json).
Funções privadas testadas diretamente: _parse_date, _forward_substitution
e similares têm testes unitários próprios — um bug nelas seria difícil de
diagnosticar apenas pelos testes de integração.