1. Preparação
O encontro abre medindo esta preparação com uma pergunta de Peer Instruction. Os dois autoestudos somam 110 minutos, e as duas animações e a autoverificação vêm depois deles.
Trilha mínima. Para acompanhar o encontro, o indispensável é o autoestudo de React e FastAPI (60 min), as duas animações e a autoverificação. O autoestudo de LLMs (50 min) é recomendado antes do encontro, porque prepara o Bloco 6, sobre o LLM no backend. Os tempos dos autoestudos são os da Adalove.
| Prioridade | Item | Tempo declarado |
|---|---|---|
| Indispensável | Autoestudo: React e FastAPI | 60 min |
| Indispensável | Animações 1 e 2 | até 60 segundos cada (ADR-005) |
| Indispensável | Autoverificação de cinco perguntas, sem nota | sem duração declarada |
| Recomendado | Autoestudo: LLMs com senso crítico | 50 min |
| Aprofundamento | Seções 2 a 8 deste material, o notebook e a leitura complementar da seção 9 | sem duração declarada |
Nenhum dos dois autoestudos desta semana é Atividade Ponderada. A entrega avaliada da Sprint 5 é o A5, de 2 pontos, trabalhada depois do encontro.
Autoestudos da semana, na Adalove
- React e FastAPI: a interface conversando com o código Python (60 min). Chegue com as duas pontas funcionando em separado: um componente React com input, estado e lista, e uma API mínima em FastAPI com
/docsaberto. - LLMs com senso crítico: codificação, limitações e bom uso (50 min). Onde os modelos erram e como escrever um prompt que a ferramenta consiga conferir.
Animação 1: o caminho de um pedido
O que observar: o pedido sai do navegador como JSON, a FastAPI valida antes de qualquer cálculo, os módulos das sprints resolvem 52 problemas de otimização e a resposta volta com os 50 pares da fronteira, que o navegador só desenha.
Animação 2: o tempo de resposta com e sem cache
O que observar: cinco pedidos chegam em sequência, três da consulta A e dois da consulta B. A barra mede o trabalho do servidor em problemas de otimização resolvidos, que é o que demora num pedido: sem cache, 260; com cache, 104. Repare em qual pedido o servidor com cache volta a calcular.
Autoverificação, sem nota
Responda por escrito antes de abrir cada resposta.
-
Quando o gestor clica em Otimizar, o que o navegador recebe da API, e quem desenha o gráfico?
Resposta comentada
Um JSON com as métricas por ativo, as duas carteiras ótimas e os pares da fronteira. O gráfico é desenhado no navegador, a partir desses números. Se o servidor mandasse uma imagem pronta, a tela perderia a tabela de pesos, o gráfico interativo e a exportação em CSV.
-
Na animação 1, em qual das três caixas um pedido com um ticker só para, e com qual código de status?
Resposta comentada
No servidor, na validação do Pydantic, com 422. O pedido nem chega aos módulos das sprints: a regra de 2 a 15 tickers está no modelo de entrada, e por isso nenhum problema de otimização é resolvido.
-
Quantos problemas de otimização um pedido novo resolve no exemplo, e de onde vem esse número?
Resposta comentada
52: um para a carteira de variância mínima, um para a de máximo Sharpe e um para cada um dos 50 pontos da fronteira. É a conta que explica por que repetir a mesma consulta sem cache custa caro.
-
Na animação 2, o pedido 3 usa os mesmos cinco ativos dos pedidos 1 e 2. Por que o servidor com cache resolve 52 problemas de novo?
Resposta comentada
Porque a janela de dados mudou: a consulta B começa em 2021. A chave do cache é a consulta inteira (tickers, início, fim e taxa livre de risco), e qualquer diferença nela é outra consulta, com outra fronteira.
-
O autoestudo de LLMs lista a alucinação entre as limitações. Que duas defesas o produto do A5 tem contra um número inventado no texto do modelo?
Resposta comentada
Mandar no prompt os números reais da otimização, com a regra de usar só esses números, e conferir por código cada número que o texto citar contra o mesmo JSON. A primeira reduz a chance de invenção; a segunda mede o que escapou.
2. Por que duas camadas
A Aula 05 deixou pronto o teste fora da amostra das duas carteiras ótimas e a medida de estabilidade dos pesos entre janelas. Hoje o trabalho das cinco sprints vira produto: o gestor do parceiro não abre notebook, ele digita tickers numa tela.
O otimizador roda no servidor, em Python, porque é lá que estão o coletor do A1, as métricas do A2 e o otimizador do A4, já testados. Reescrever tudo em JavaScript para rodar no navegador criaria uma segunda versão do mesmo cálculo, que diverge da primeira no primeiro arredondamento, e obrigaria a mandar o histórico de preços inteiro para cada gestor. O navegador fica com o que só ele faz bem: formulário, estado da tela e desenho.
No exemplo do acervo, a função pipeline.calcular encadeia as três camadas e devolve um dicionário, sem nada de HTTP. A mesma função serve à API, ao notebook e aos testes, e é isso que "importar sem copiar código" quer dizer na prática.
exemplos/aula06-api-carteira/
backend/
carteira/dados.py coletor (A1)
carteira/metricas.py risco e retorno (A2)
carteira/otimizador.py otimizador (A4)
carteira/pipeline.py as três camadas numa função
main.py rotas, Pydantic, cache, CORS
interpretacao.py prompt, chamada ao LLM, verificador
frontend/
src/api.js todo o HTTP e a tradução de erro
src/App.jsx formulário, estados, tabela
src/Fronteira.jsx gráfico em SVG
3. HTTP, JSON e o contrato da API
Cada pedido HTTP tem um método, um caminho e, no caso do POST, um corpo em JSON. A resposta traz um código de status e outro JSON. O contrato da API é a tabela abaixo: escrita no SPEC.md antes de dividir o trabalho, ela deixa quem faz o frontend e quem faz o backend trabalharem em paralelo, cada um testando contra o mesmo combinado.
| Rota | Entrada | Sucesso | Erros do contrato |
|---|---|---|---|
GET /ativos | nenhuma | 200, os 15 tickers da base | nenhum |
POST /carteiras/otimizar | 2 a 15 tickers sem repetição, início, fim e taxa livre de risco opcional | 200, início e fim da janela comum, número de retornos, métricas por ativo, as duas carteiras e a fronteira | 404 ticker fora da base; 422 entrada inválida ou janela comum com menos de 60 retornos |
POST /carteiras/interpretar | o mesmo pedido | 200, prompt, texto, modelo e checagem de presença de cada número | 404 e 422; 503 sem chave no servidor; 502 falha do provedor |
POST /carteiras/exportar | o mesmo pedido | 200, CSV com os pesos das duas carteiras | 404 e 422 |
Para o pedido de referência da aula (PETR4, VALE3, ITUB4, WEGE3 e IVVB11, de 01/01/2019 a 31/08/2026), a API devolve 1.907 retornos diários, de 03/01/2019 a 31/08/2026. O tratamento de ausência é o do módulo (ADR-009): o IVVB11 está sem cotação em 24/10/2025 e recebe o último preço, os retornos são recortados na janela comum dos tickers pedidos, e o primeiro pregão não tem retorno. A resposta declara o início e o fim dessa janela; com a GOLD11 no pedido, ela começa em 14/12/2020. A carteira de máximo Sharpe fica com 51,64% em IVVB11, 25,02% em WEGE3 e 23,33% em PETR4, com retorno anual de 26,90%, volatilidade de 19,13% e Sharpe de 0,91, usando o CDI do período, de 9,51% ao ano, como taxa livre de risco.
4. Lendo os erros da API
No montador de requisição do deck, a turma provoca os erros de propósito: um ticker só, datas invertidas, um ticker que não existe. Cada erro tem um código diferente porque cada um é detectado num lugar diferente e pede uma correção diferente de quem usa a tela.
O 422 do Pydantic
Com um ticker só, a API responde 422 e um corpo como este:
{"detail": [{"type": "too_short",
"loc": ["body", "tickers"],
"msg": "List should have at least 2 items after validation, not 1"}]}
A mensagem diz três coisas. loc aponta onde está o problema: no corpo do pedido, no campo tickers. type diz qual regra falhou, o min_length=2 declarado no modelo. msg diz o que chegou: uma lista com um item. Nenhuma linha do otimizador rodou. Esse erro é pedagógico porque mostra o contrato funcionando sozinho: a regra foi escrita uma vez, no modelo, e vale para toda rota que recebe PedidoCarteira, inclusive no /docs.
O 404 do ticker inexistente
Com ["PETR4", "XPTO3"], o pedido é bem formado: dois tickers, datas em ordem. O Pydantic aprova, e é o coletor que descobre que XPTO3 não está na base. A rota traduz a exceção TickerDesconhecido em 404 com o ticker na mensagem, e o frontend mostra ao gestor "Algum ativo pedido não está na base de dados. Confira o código do ticker. (XPTO3)". O 422 também é defensável, porque o ticker chega no corpo do pedido, e há APIs que tratam todo erro de domínio como entrada inválida. O exemplo escolheu 404 para separar o pedido mal formado do recurso que falta, e essa escolha vai escrita no contrato do SPEC.md, com o corpo de erro de cada caso. Devolver 500 diria que o servidor quebrou, quando quem precisa corrigir é o gestor.
O 503 sem chave
Na rota de interpretação, um servidor sem OPENROUTER_API_KEY responde 503. O número da carteira continua válido, e a tela diz isso: a falha é só da camada de texto.
Um 500 na sua API é sempre defeito do grupo: uma exceção que nenhuma rota tratou. O plano de testes do SPEC.md precisa ter ao menos um caso para cada status do contrato.
5. Cache da consulta
O @lru_cache sobre a função que calcula guarda o resultado de cada consulta. Na sequência da animação, cinco pedidos com duas consultas diferentes custam 260 problemas de otimização sem cache e 104 com cache, porque só a primeira vez de cada consulta calcula.
Dois cuidados que o ADR de cache do grupo precisa registrar. Primeiro, a chave: tickers, início, fim e taxa livre de risco, na mesma ordem e na mesma caixa; por isso o exemplo normaliza os tickers para maiúsculas antes de consultar o cache. Segundo, o alcance: o lru_cache vive na memória do processo, some quando o servidor reinicia e não é compartilhado entre dois processos. Para a review, com um servidor só, isso basta.
6. O componente React e o CORS
O componente guarda o estado da tela com useState: os campos do formulário, se está carregando, a mensagem de erro e o resultado. Cada set redesenha a tela. Toda chamada HTTP mora em src/api.js, que devolve dados ou lança um erro já traduzido para o gestor; o componente não sabe de status nem de URL.
async function aoOtimizar(evento) {
evento.preventDefault();
setCarregando(true);
setErro(null);
try {
setResultado(await otimizar(pedido()));
} catch (e) {
setResultado(null);
setErro(e.message); // já traduzida em api.js
} finally {
setCarregando(false);
}
}
O erro de CORS
Com o React em http://localhost:5173 e a API em http://127.0.0.1:8000, a primeira chamada sem configuração falha com esta mensagem no console do navegador:
Access to fetch at 'http://127.0.0.1:8000/carteiras/otimizar' from origin
'http://localhost:5173' has been blocked by CORS policy: Response to preflight
request doesn't pass access control check: No 'Access-Control-Allow-Origin'
header is present on the requested resource.
A mensagem confunde porque a API pode ter respondido normalmente: quem bloqueia é o navegador, que só entrega a resposta ao JavaScript de outra origem quando o servidor declara que aquela origem é permitida. Origem é a combinação de protocolo, endereço e porta, e as portas 5173 e 8000 já fazem duas origens. A correção é no backend, com o CORSMiddleware liberando a origem do frontend, e nunca com uma extensão de navegador que desliga a checagem, porque o gestor não vai ter essa extensão.
app.add_middleware(CORSMiddleware,
allow_origins=[os.environ.get("FRONTEND_ORIGEM", "http://localhost:5173")],
allow_methods=["GET", "POST"], allow_headers=["Content-Type"])
7. O LLM no backend
A chamada ao LLM acontece no servidor, com a chave na variável de ambiente OPENROUTER_API_KEY do processo do backend. A OpenRouter tem API compatível com a da OpenAI, e o modelo é escolhido pela variável LLM_MODELO: trocar de modelo muda o custo por token e o texto, e essa escolha merece um ADR com o custo medido por chamada.
Tudo o que o navegador baixa, qualquer pessoa lê, inclusive as variáveis VITE_* do Vite, que são copiadas para dentro do JavaScript. A chave nunca vai para o frontend. No repositório vai só o .env.example, com a chave vazia; o .env fica no .gitignore.
O prompt tem duas partes: as regras de escrita (usar só os números do JSON, em porcentagem com duas casas, declarar as limitações, não recomendar compra nem prometer retorno) e o próprio JSON da otimização, recortado e convertido para porcentagem. É esse JSON que torna o texto conferível depois.
8. Checagem de presença de cada número
O texto do slide de exercício foi escrito pelo acervo a partir dos números reais, no estilo de um LLM, com três erros plantados e declarados. Nenhum modelo o gerou. Ele tem 12 números, e o verificador da API procura cada um no JSON do prompt.
| Trecho | Erro plantado | Verificação de presença | Valor certo |
|---|---|---|---|
| 17,13% | volatilidade da carteira de máximo Sharpe alterada | acusa: o número não existe no JSON | 19,13% |
| 30,00% | promessa de retorno para o próximo ano | acusa: o número não existe no JSON | nenhum: o JSON não tem retorno futuro |
| 0,16 | correlação média do IVVB11 atribuída à VALE3 | aprova: o número existe, no IVVB11 | o IVVB11 é o que mais diversifica, com 0,16; a VALE3 tem 0,32 |
O verificador confere 10 de 12 números e acusa 2; dos 10 que passam, 9 estão certos. Ele faz uma verificação de presença: pega número inventado e número alterado, e deixa passar número certo no ativo errado. Por isso ele devolve também a origem de cada número: para o 0,16, a origem é ativos[IVVB11].correlacao_media, e quem lê a tabela vê na hora que a frase fala da VALE3. Uma regra mais estrita, que exige o mesmo ticker na frase e na origem, pega esse erro e passa a depender de como o modelo escreve a frase. A leitura humana continua no fluxo, agora com o código apontando onde olhar.
9. Depois do encontro
Desafio do notebook
Acrescentar a rota POST /carteiras/metricas, que recebe pesos informados pelo gestor e devolve retorno, volatilidade e Sharpe da carteira, com 422 para pesos que não somam 1, e testar os dois casos com TestClient.
Avanço do A5
- Contrato da API no SPEC.md, com o plano de testes, antes de dividir o trabalho.
/backende/frontendseparados, com.env.examplesem chave real.- README com
fastapi devno backend enpm installenpm run devno frontend. - Cache da consulta e exportação em CSV.
- ADR da divisão entre frontend e backend, com a decisão de cada parte do produto.
O A5 vale 2 pontos, e a review da Sprint 5, em 18/12/2026, fecha o módulo. O exemplo completo, com testes, está em exemplos/aula06-api-carteira.
Leitura complementar
Páginas de primeiros passos e de CORS da documentação do FastAPI, listadas em referências da Aula 06.