Skip to content

StephanieFran/bootstrapping-test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bootstrapping da Curva Zero-Cupom

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).


Estrutura do projeto

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)

Instalação

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.txt

Conteú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.


Execução

# 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 --help

O 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

Saída esperada (exemplo do enunciado)

{
  "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
    }
  ]
}

Testes

# 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 (em test_calendar.py) é marcado como skipped apenas se a biblioteca xlwt não estiver instalada — ela é usada para criar um .xls sintético naquele teste específico. Como xlwt consta no requirements.txt, a suite completa roda com 134 passed, 0 skipped.

Cobertura por módulo

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

(a) Como o problema foi modelado

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ítulo j na data i (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%.

Calendário e convenções de prazo

  • 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 em 2027-01-04 (segunda-feira), e count_business_days(2026-06-01, 2027-01-04) = 148, reproduzindo o DU=148 do enunciado. Essa lógica é implementada por next_business_day() em calendar_utils.py.

(b) Método de resolução e por quê

Estrutura da matriz C

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.

Substituição progressiva (forward substitution)

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.solve usa 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.

Recuperação das taxas spot

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.

Validação: re-precificação (R5)

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).


(c) Complexidade da solução

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.


Decisões de design notáveis

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.

About

Desafio Técnico — dado um conjunto de títulos(qualquer ativo de renda fixa pré-fixado) com seus preços de mercado e fluxos de caixa, construir a curva zero-cupom (fatores de desconto e taxas spot por vértice).

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages