Olá, pessoal!
Dando continuidade à série My Broker B3, hoje vamos falar sobre um componente essencial para o lado da B3 no nosso simulador: o B3 Market Sync API.
Antes de construirmos o motor que decide se uma ordem é executada ou rejeitada, precisamos garantir que ele tenha acesso a preços reais do mercado. É exatamente isso que o b3-market-sync-api faz: busca cotações reais de ativos brasileiros via brapi.dev e as armazena no Redis, pronto para consumo em tempo real.
🏗️ O que é o Market Sync?
Este microserviço tem uma responsabilidade única e bem definida: ser o produtor de dados de mercado do ecossistema. Ele opera de forma completamente assíncrona e independente — nenhum outro serviço precisa chamá-lo diretamente.
O fluxo é simples e elegante:
[brapi.dev] ◀── Feign Client ── [MarketSyncScheduler]
│
market:price:{TICKER}
│
[Redis]
│
[B3 Matching Engine] (próximo post)
🎯 Foco no MVP
Neste serviço o objetivo é claro: ter preços reais disponíveis no Redis com baixa latência. O Matching Engine que construiremos no próximo post não vai consultar banco de dados nem fazer chamadas HTTP para obter preços — ele vai direto no Redis. Isso garante que a decisão de match aconteça em microssegundos.
Nesta fase, priorizei:
- Sincronização agendada com guard de horário de pregão
- Cache Redis com TTL para evitar dados obsoletos
- Proteção de rate limit da API externa
- Correção de bugs críticos encontrados na revisão
- API REST para consulta dos preços em cache
- Documentação via Swagger
🛠️ Stack Tecnológica
| Tecnologia | Uso |
|---|---|
| Java 21 + Spring Boot 3.5.11 | Core do serviço |
| Spring Cloud OpenFeign | Cliente HTTP declarativo para brapi.dev |
| Spring Data Redis | Cache de alta performance |
| Spring Scheduling | Sincronização periódica |
| Jackson JSR310 | Suporte a tipos de data Java 8 |
| SpringDoc OpenAPI | Documentação via Swagger UI |
🏗️ Os Pilares da Implementação
1. Feign Client — Consumindo a Brapi
O Spring Cloud OpenFeign permite declarar um cliente HTTP como uma simples interface, sem código boilerplate:
@FeignClient(name = "brapiClient", url = "${app.brapi.url}")
public interface BrapiClient {
@GetMapping("/quote/{ticker}")
BrapiResponseDTO getQuote(
@PathVariable String ticker,
@RequestParam String token
);
}
A URL e o token são injetados via application.yaml, mantendo o código limpo e configurável por ambiente.
2. Scheduler com Guard de Pregão
O coração do serviço é um job agendado que roda a cada 30 minutos. Mas antes de qualquer chamada à API, ele verifica se o mercado está aberto:
@Scheduled(fixedRateString = "${app.sync.interval:1800000}")
public void sync() {
if (!isMarketOpen()) {
log.info("Sync aborted: market is closed (outside trading hours or weekend).");
return;
}
// ... busca e salva os preços
}
private boolean isMarketOpen() {
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/Sao_Paulo"));
DayOfWeek day = now.getDayOfWeek();
int hour = now.getHour();
if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
return false;
}
// B3 trading hours: 10:00 to 18:00 (Sao Paulo time)
return hour >= 10 && hour < 18;
}
Detalhe importante: usamos ZonedDateTime com America/Sao_Paulo explicitamente. Dentro de um container Docker, a JVM pode usar UTC por padrão — o que faria o guard de horário funcionar errado. Além disso, passamos -Duser.timezone=America/Sao_Paulo no ENTRYPOINT do Dockerfile para garantir que a JVM também respeite o timezone correto:
ENTRYPOINT ["java", "-Duser.timezone=America/Sao_Paulo", "-jar", "app.jar"]
3. Cache Redis com TTL Correto
O MarketDataService salva cada cotação no Redis com uma chave padronizada e TTL de 5 minutos:
private static final Duration CACHE_TTL = Duration.ofMinutes(5);
public void saveToCache(BrapiResultDTO quote) {
String key = CACHE_KEY_PREFIX + quote.getTicker();
// TTL of 5 minutes — if sync stops, stale prices expire quickly
// preventing the Matching Engine from using outdated data
redisTemplate.opsForValue().set(key, quote, CACHE_TTL);
log.info("Ticker {} updated in cache: R$ {}", quote.getTicker(), quote.getRegularMarketPrice());
}
O TTL é fundamental: se o serviço cair, os preços expiram em 5 minutos. O Matching Engine passa a não encontrar o preço no Redis e rejeita as ordens — comportamento correto e previsível, muito melhor do que usar preços de horas atrás.
4. Deserialização do Timestamp Epoch
Um bug sutil que encontrei: a brapi.dev retorna o campo regularMarketTime como epoch Unix (número inteiro), mas o Jackson tentava desserializar como LocalDateTime (string ISO). Isso causa falha silenciosa na desserialização.
A solução foi criar um deserializer customizado:
public class EpochToLocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {
private static final ZoneId SAO_PAULO = ZoneId.of("America/Sao_Paulo");
@Override
public LocalDateTime deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
long epoch = parser.getLongValue();
return LocalDateTime.ofInstant(Instant.ofEpochSecond(epoch), SAO_PAULO);
}
}
E anotá-lo no DTO:
@JsonDeserialize(using = EpochToLocalDateTimeDeserializer.class)
private LocalDateTime regularMarketTime;
5. Proteção de Rate Limit
Como utilizamos o plano gratuito da brapi.dev, implementamos um delay entre as chamadas para evitar throttling:
for (String ticker : brapiProperties.getTickers()) {
try {
BrapiResponseDTO response = brapiClient.getQuote(ticker.trim(), brapiProperties.getToken());
if (nonNull(response.getResults()) && !response.getResults().isEmpty()) {
marketDataService.saveToCache(response.getResults().getFirst());
}
// Small delay between calls to avoid hitting rate limit on free plan
Thread.sleep(200);
} catch (Exception e) {
log.error("Failed to sync ticker {}: {}", ticker, e.getMessage());
}
}
O try/catch por ticker é essencial — se um ativo falhar, o loop continua para os próximos.
6. API REST + Swagger
Embora o Matching Engine consuma os preços diretamente do Redis, adicionamos endpoints REST para facilitar debug e observabilidade:
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /api/v1/quotes |
Lista preços de todos os tickers configurados |
| GET | /api/v1/quotes/{ticker} |
Retorna o preço de um ticker específico |
Documentados via Swagger UI em http://localhost:8096/swagger-ui.html.
🔒 Segurança — Token Fora do Código
Um problema crítico que corrigi: o token da brapi.dev estava hardcoded como valor padrão no application.yaml:
# ❌ Antes — token exposto no repositório público
token: ${BRAPI_TOKEN:mFk3HMLijAtxsP5y1ZjhsY}
# ✅ Depois — sem fallback, variável obrigatória
token: ${BRAPI_TOKEN}
Nunca exponha credenciais com valores default em repositórios públicos. A aplicação agora falha na inicialização se a variável não for fornecida — comportamento correto.
✅ Validando a Execução
Com a aplicação rodando localmente:
- ✅ Tomcat subiu na porta
8096 - ✅ Aplicação inicializou em menos de 3 segundos
- ✅ Scheduler disparou e exibiu:
Sync aborted: market is closed (outside trading hours or weekend) - ✅ Swagger UI acessível em
http://localhost:8096/swagger-ui.html
🚀 O que vem a seguir?
Com o b3-market-sync-api pronto e alimentando o Redis com preços reais, temos a base que o próximo serviço precisa para funcionar. No próximo post vamos construir o B3 Matching Engine — o motor que vai consumir esses preços e decidir se as ordens da corretora serão executadas ou rejeitadas.
Ficou com alguma dúvida sobre a integração com Redis ou sobre o guard de horário de pregão? Deixe nos comentários!
🔎 Sobre a série
⬅️ Post Anterior: Do Stream para o Banco: Processando Market Data com Spring Boot, Redis e Flyway
➡️ Próximo Post: O Coração da B3: Construindo o Matching Engine com RabbitMQ e Redis (em breve)
📘 Índice da Série: Guia da Série
Links:
Top comments (0)