Um cliente em Tóquio compra uma assinatura de US$ 19,99. Seu código multiplica pela taxa USD/JPY, obtém 2942,82785, grava no banco de dados e envia ao seu processador de pagamentos. O processador rejeita — ou pior, aceita e cobra 100 vezes mais. O iene não tem casas decimais, e seu código nunca perguntou.
Arredondamento de moedas é um daqueles problemas que parecem triviais até chegarem à produção. Não é uma questão de formatação; é uma questão de correção. Cada moeda tem seu próprio número de casas decimais, a matemática de ponto flutuante corrompe dinheiro silenciosamente, e no momento em que você converte entre moedas introduz uma decisão de arredondamento que precisa ser tomada de forma deliberada. Este guia cobre as regras que realmente importam: quantas casas decimais cada moeda tem, por que armazenar valores como inteiros, qual modo de arredondamento escolher e como arredondar uma conversão cambial para que seu razão continue fechando no fim do mês.
Unidades menores: quantas casas decimais tem cada moeda?
A unidade menor de uma moeda é sua menor subdivisão transacionável. Para o dólar americano é o centavo, então USD tem duas casas decimais e US$ 19,99 são 1999 centavos. Essa é a representação que os processadores de pagamento esperam, e é a que seu banco de dados deve usar.
A ISO 4217 — o mesmo padrão que fornece códigos de três letras como USD e JPY — também atribui a cada moeda um expoente de unidade menor. A maioria dos desenvolvedores supõe que esse expoente é sempre 2. Não é, e essa suposição é o bug mais caro desta categoria.
Moedas sem casas decimais
Essas moedas não têm subunidade em circulação, então o valor que você envia é o número inteiro de unidades:
- JPY — iene japonês
- KRW — won sul-coreano
- VND — dong vietnamita
- CLP — peso chileno
- ISK — coroa islandesa
- XAF / XOF / XPF — francos CFA e CFP
- UGX — xelim ugandense
- PYG — guarani paraguaio
- RWF, GNF, KMF, DJF, VUV — e várias outras moedas de pequena denominação
Se você tratar JPY como moeda de duas casas decimais e multiplicar por 100 antes de enviar ao processador, acabou de cobrar do seu cliente 100× o valor pretendido.
Moedas com três casas decimais
Sete moedas se subdividem em milésimos em vez de centésimos:
- KWD — dinar kuwaitiano (1000 fils)
- BHD — dinar bareinita (1000 fils)
- OMR — rial omanense (1000 baisa)
- JOD — dinar jordaniano (1000 fils)
- TND — dinar tunisiano (1000 milésimos)
- IQD — dinar iraquiano (1000 fils)
- LYD — dinar líbio (1000 dirhams)
Aqui a falha vai na direção oposta: trate KWD como duas casas decimais e você cobra um décimo do que pretendia. Uma fatura de KWD 12,500 vira KWD 1,250.
Há até entradas de quatro casas decimais na ISO 4217 — a unidad de fomento chilena (CLF) e a unidad previsional uruguaia (UYW). São unidades contábeis indexadas, não dinheiro em espécie, mas se seu sistema aceita códigos ISO arbitrários, precisa sobreviver a elas.
Quando o padrão e seu processador discordam
Essa é a armadilha que pega equipes que fizeram todo o resto certo. Provedores de pagamento às vezes se desviam da ISO 4217 por razões operacionais. A Adyen, por exemplo, documenta que CLP, CVE, IDR e ISK usam em sua API um número de casas decimais diferente do que o padrão especifica — a ISK tem zero casas sob a ISO 4217, mas precisa ser enviada com duas casas para a Adyen.
A regra: sua tabela de arredondamento é uma propriedade do sistema com o qual você conversa, não uma constante universal. Mantenha uma tabela por integração, popule-a a partir da ISO 4217 e sobrescreva por provedor onde a documentação deles mandar. Nunca deixe 100 fixo no código.
Nunca armazene dinheiro como float
Antes de qualquer discussão sobre arredondamento, a base. O ponto flutuante binário não consegue representar exatamente a maioria das frações decimais:
0.1 + 0.2 // 0.30000000000000004
1.005 * 100 // 100.49999999999999
19.99 * 147.2150 // 2942.8278499999997Esses dígitos finais não são cosméticos. Passe-os por Math.round() no momento errado e você obtém um valor errado por uma unidade menor, o que basta para reprovar a conciliação.
Duas regras cobrem quase todos os casos:
- Armazene valores como inteiros em unidades menores. Uma coluna
amount_minor BIGINTmais uma colunacurrency CHAR(3).{ amount_minor: 1999, currency: "USD" }é inequívoco e corresponde ao que Stripe, Adyen e a maioria dos processadores já esperam. - Faça a aritmética com inteiros ou com um tipo decimal.
decimal.Decimaldo Python,BigDecimaldo Java,NUMERICdo PostgreSQL, ou uma biblioteca monetária JavaScript que encapsule aritmética inteira. Reserve floats para a própria taxa de câmbio, e ainda assim apenas até o ponto da multiplicação.
Se você está desenhando essa camada do zero, nosso guia de design de razão multimoeda aprofunda as decisões de esquema.
O pipeline de conversão: unidades menores entram, unidades menores saem
A conversão de moedas tem exatamente quatro passos, e o arredondamento pertence ao passo três — uma única vez, no fim.
- Converta o valor de origem de unidades menores para um valor decimal.
- Multiplique pela taxa de câmbio em precisão total.
- Arredonde para o expoente de unidade menor da moeda de destino.
- Converta de volta para unidades menores inteiras.
Aqui está em JavaScript, com a tabela por moeda fazendo o trabalho:
// Minor unit exponents. Seed from ISO 4217, override per payment provider.
const MINOR_UNITS = {
USD: 2, EUR: 2, GBP: 2, CHF: 2, CAD: 2, AUD: 2, CNY: 2, INR: 2,
JPY: 0, KRW: 0, VND: 0, CLP: 0, ISK: 0, XAF: 0, XOF: 0, XPF: 0,
KWD: 3, BHD: 3, OMR: 3, JOD: 3, TND: 3, IQD: 3, LYD: 3,
};
function exponentFor(currency) {
const e = MINOR_UNITS[currency];
if (e === undefined) throw new Error(`Unknown minor unit for ${currency}`);
return e;
}
/**
* Convert an integer minor-unit amount from one currency to another.
* Returns an integer in the target currency's minor units.
*/
function convertMinor(amountMinor, from, to, rate) {
const fromExp = exponentFor(from);
const toExp = exponentFor(to);
const decimalAmount = amountMinor / 10 ** fromExp; // 1999 -> 19.99
const converted = decimalAmount * rate; // full precision, no rounding yet
return Math.round(converted * 10 ** toExp); // single rounding step
}
convertMinor(1999, "USD", "JPY", 147.2150); // 2943 (¥2,943)
convertMinor(1999, "USD", "KWD", 0.30590); // 6115 (KWD 6.115)
convertMinor(1999, "USD", "EUR", 0.9241); // 1847 (€18.47)Repare no que a função não faz: nunca arredonda a taxa, nunca arredonda um valor intermediário e nunca supõe duas casas decimais. Math.round aqui é half-up para positivos — adequado para um checkout, mas leia a próxima seção antes de usá-lo em algo regulado.
Escolhendo um modo de arredondamento
"Arredondar para duas casas" não é uma especificação. Há pelo menos cinco formas defensáveis de desempatar, e sistemas financeiros se importam com qual você escolhe.
| Modo | 2,5 → | 3,5 → | −2,5 → | Uso típico |
|---|---|---|---|---|
| Half up | 3 | 4 | −3 | Preços ao consumidor, totais de checkout |
| Half even (do banqueiro) | 2 | 4 | −2 | Contabilidade, juros, impostos, relatórios |
| Half down | 2 | 3 | −2 | Raro; ocasionalmente em código financeiro legado |
| Ceiling (para cima) | 3 | 4 | −2 | Taxas que você nunca pode cobrar a menos |
| Floor (para baixo / truncar) | 2 | 3 | −3 | Repasses que você nunca pode pagar a mais |
Math.round() faz com números positivos. É intuitivo e apropriado para um preço que o cliente está prestes a ver.Half even, também chamado de arredondamento do banqueiro, manda metades exatas para o dígito par mais próximo. Ao longo de muitas transações ele cancela o viés sistemático de alta introduzido pelo half-up, e por isso é o padrão em sistemas contábeis, no módulo decimal do Python e no próprio IEEE 754. Se você agrega milhares de valores convertidos em um relatório de receita, o half-up inflará o total silenciosamente; o half-even não.
Ceiling e floor existem para riscos assimétricos. Um marketplace que repassa a vendedores pode aplicar floor a cada repasse para nunca distribuir mais do que possui; a diferença vai para uma conta de arredondamento.
O Python torna a escolha explícita, o que é a ergonomia correta:
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
MINOR_UNITS = {"USD": 2, "EUR": 2, "JPY": 0, "KWD": 3}
def convert_minor(amount_minor: int, src: str, dst: str,
rate: str, mode=ROUND_HALF_EVEN) -> int:
"""Convert integer minor units to integer minor units, exactly once."""
src_exp, dst_exp = MINOR_UNITS[src], MINOR_UNITS[dst]
amount = Decimal(amount_minor) / (Decimal(10) ** src_exp)
converted = amount * Decimal(rate) # rate passed as a string, not a float
quantum = Decimal(1).scaleb(-dst_exp) # 0.01, 1, or 0.001
rounded = converted.quantize(quantum, rounding=mode)
return int(rounded.scaleb(dst_exp))
convert_minor(1999, "USD", "JPY", "147.2150") # 2943
convert_minor(1999, "USD", "KWD", "0.30590") # 6115Passar a taxa como string para Decimal importa. Decimal(0.9241) herda o erro do float; Decimal("0.9241") não.
Três bugs de arredondamento que custam dinheiro de verdade
1. Arredondar a taxa antes de multiplicar
Taxas de câmbio costumam carregar de quatro a seis decimais significativos, e truncá-las não é inofensivo. Considere USD/JPY a 147,2150 e uma transferência de US$ 10.000:
- Taxa completa:
10000 × 147.2150 = ¥1.472.150 - Taxa arredondada para duas casas (147,21):
10000 × 147.21 = ¥1.472.100
Uma discrepância de ¥50 em uma única transação, puramente por formatar a taxa antes de usá-la. Armazene a taxa na precisão que seu provedor retorna, arredonde apenas o valor resultante e persista a taxa exata usada junto da transação para auditoria. Nosso guia sobre de onde as APIs de câmbio tiram seus dados explica por que essa precisão é significativa em primeiro lugar.
2. Arredondar duas vezes numa conversão com várias pernas
Se você roteia USD → EUR → JPY e arredonda na etapa EUR, jogou fora precisão que a segunda multiplicação depois amplifica. Convertendo US$ 12,34 com USD/EUR a 0,9241 e EUR/JPY a 159,3063:
- Direto:
12.34 × 147.2150 = 1816.63→ ¥1.817 - Via perna EUR arredondada:
12.34 × 0.9241 = 11.4034→ arredondado para € 11,40 →11.40 × 159.3063 = 1816.09→ ¥1.816
Um iene, por um passo de arredondamento desnecessário. Numa rodada de repasses de cinquenta mil transações, isso é um chamado de conciliação. Sempre que houver um par direto disponível, use-o; quando precisar triangular, mantenha o intermediário em precisão total. Veja taxas cruzadas explicadas para a mecânica.
3. Itens de linha que não somam o total
Arredonde cada linha de uma fatura de forma independente e as partes nem sempre somarão o total arredondado. O caso clássico é uma divisão:
$10.00 split three ways
10.00 / 3 = 3.3333...
→ 3.33 + 3.33 + 3.33 = 9.99 ✗ one cent missingA correção é alocação, não arredondamento. Arredonde o total uma vez e depois distribua entre as partes, repassando o resto uma unidade menor por vez:
/**
* Split an integer minor-unit total into `n` parts whose sum is exactly the total.
* Remainder units are distributed to the earliest parts (largest-remainder method).
*/
function allocate(totalMinor, ratios) {
const sum = ratios.reduce((a, b) => a + b, 0);
const shares = ratios.map(r => Math.floor((totalMinor * r) / sum));
let remainder = totalMinor - shares.reduce((a, b) => a + b, 0);
for (let i = 0; remainder > 0; i = (i + 1) % shares.length, remainder--) {
shares[i] += 1;
}
return shares;
}
allocate(1000, [1, 1, 1]); // [334, 333, 333] → sums to exactly 1000
allocate(9247, [3, 2, 1]); // [4624, 3082, 1541] → sums to exactly 9247Aplique o mesmo padrão após uma conversão cambial: converta e arredonde o total da fatura, depois aloque esse total entre as linhas. As linhas sempre fecharão, porque foram derivadas do total em vez de calculadas isoladamente. Isso importa mais em faturamento multimoeda e em cobrança SaaS com proporcionalização, onde um desvio de um centavo aparece num PDF que o cliente vê.
Arredondamento de dinheiro em espécie é outra regra
A unidade menor de uma moeda diz o menor valor que pode ser registrado. Nem sempre diz o menor valor que pode ser pago em espécie. Vários países retiraram suas menores moedas e arredondam pagamentos em dinheiro no caixa:
- Suíça — dinheiro arredonda para os 0,05 CHF mais próximos
- Canadá — a moeda de um centavo foi retirada em 2013; dinheiro arredonda para os 5 centavos mais próximos
- Suécia — dinheiro arredonda para a coroa inteira mais próxima
- Países Baixos — dinheiro arredonda para os 5 cêntimos mais próximos
Crucialmente, isso se aplica ao pagamento em espécie, não à fatura. Uma fatura suíça de CHF 12,32 continua registrada como 12,32; só a liquidação em dinheiro arredonda para 12,30, e a diferença de 0,02 é lançada como ajuste de arredondamento. Se você desenvolve software de ponto de venda, modele o arredondamento de espécie como um passo separado e posterior aplicado ao pagamento — nunca o embuta no valor armazenado, ou suas transações eletrônicas e em dinheiro vão divergir.
Formatação é o último passo, não o cálculo
Feita a aritmética, entregue a exibição a um formatador ciente de locale. O Intl.NumberFormat já conhece a contagem de decimais, a posição do símbolo e os separadores de cada moeda:
function formatMinor(amountMinor, currency, locale = "en-US") {
const exp = exponentFor(currency);
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(amountMinor / 10 ** exp);
}
formatMinor(1999, "USD"); // "$19.99"
formatMinor(2943, "JPY", "ja-JP"); // "¥2,943"
formatMinor(6115, "KWD"); // "KWD 6.115"
formatMinor(1847, "EUR", "de-DE"); // "18,47 €"Duas notas práticas. Primeiro, instâncias de Intl.NumberFormat são caras de construir — mantenha uma em cache por par locale-moeda em vez de criar uma por linha. Segundo, a divisão por 10 ** exp na última linha é o único lugar em que um float deveria tocar um valor monetário, e apenas porque o resultado vira string imediatamente.
Juntando tudo com a API da Finexly
Busque a taxa em precisão total, converta uma vez, arredonde uma vez e guarde a taxa usada:
curl "https://api.finexly.com/v1/latest?base=USD&symbols=JPY,KWD,EUR" \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"base": "USD",
"timestamp": 1755244800,
"rates": {
"JPY": 147.2150,
"KWD": 0.30590,
"EUR": 0.9241
}
}async function quote(amountMinor, from, to) {
const res = await fetch(
`https://api.finexly.com/v1/latest?base=${from}&symbols=${to}`,
{ headers: { Authorization: `Bearer ${process.env.FINEXLY_API_KEY}` } }
);
const data = await res.json();
const rate = data.rates[to];
return {
amount_minor: convertMinor(amountMinor, from, to, rate),
currency: to,
rate, // persist the exact rate used
rate_timestamp: data.timestamp, // and when it was captured
};
}
await quote(1999, "USD", "JPY");
// { amount_minor: 2943, currency: "JPY", rate: 147.215, rate_timestamp: 1755244800 }Guardar rate e rate_timestamp na linha da transação é o que torna uma disputa respondível seis meses depois. Detalhes completos de endpoints e parâmetros estão na documentação da API Finexly, e se você faz cache de taxas entre requisições, nossas notas sobre cache e tratamento de erros cobrem os trade-offs de defasagem.
Um checklist de testes
Bugs de dinheiro se escondem nos casos que ninguém testa. No mínimo, cubra:
- Um destino sem decimais — converta para JPY ou KRW e verifique que o resultado não tem parte fracionária.
- Um destino com três decimais — converta para KWD ou BHD e verifique que as três casas sobrevivem.
- Metades exatas — verifique o modo de arredondamento escolhido, nos dois sentidos, incluindo negativos.
- Desvio de ida e volta — converta USD → EUR → USD e verifique que o resultado está dentro de uma unidade menor, não que seja igual.
- Invariância da alocação — verifique que as partes divididas sempre somam exatamente o total, de 1 a 100 partes.
- Códigos de moeda desconhecidos — verifique que o código lança erro em vez de assumir duas casas silenciosamente.
- Valores muito grandes — verifique que não há perda de precisão além de
Number.MAX_SAFE_INTEGERem JavaScript; useBigIntse lidar com IDR ou VND em escala.
Perguntas frequentes
Quantas casas decimais tem cada moeda?
A maioria tem duas. Cerca de duas dúzias não têm nenhuma — incluindo JPY, KRW, VND, CLP e ISK — e sete têm três: KWD, BHD, OMR, JOD, TND, IQD e LYD. A ISO 4217 é a fonte autoritativa, mas confira também a tabela do seu provedor de pagamentos, já que alguns se desviam por razões operacionais.
Devo arredondar taxas de câmbio ou valores convertidos?
Somente valores convertidos. Mantenha a taxa na precisão total que seu provedor retorna, multiplique e depois arredonde o resultado uma única vez para a unidade menor da moeda de destino. Arredondar uma taxa antes de multiplicar introduz um erro proporcional ao tamanho da transação.
Qual a diferença entre half-up e arredondamento do banqueiro?
Half-up sempre afasta do zero uma metade exata (2,5 → 3). O arredondamento do banqueiro — metade para o par — a envia ao dígito par mais próximo (2,5 → 2, 3,5 → 4), o que remove o viés sistemático de alta ao agregar muitos valores. Use half-up para preços exibidos a clientes e half-even para contabilidade e relatórios.
Por que meus itens convertidos não somam o total convertido?
Porque cada linha foi arredondada de forma independente e os erros se acumulam. Arredonde o total uma vez e depois aloque esse total entre as linhas usando uma divisão por maior resto. As partes então somarão o todo por construção.
Posso simplesmente armazenar dinheiro como float com duas casas?
Não. O ponto flutuante binário não representa exatamente valores como 0,1, então os erros se acumulam em somas e multiplicações e acabam invertendo uma decisão de arredondamento. Armazene inteiros em unidades menores, ou use um tipo decimal exato. Isso não é uma preocupação teórica — é a causa raiz mais comum de falhas de conciliação por um centavo.
Obtenha taxas em precisão total
O arredondamento correto começa com uma taxa em que você confia e com a precisão que você não jogou fora. Pegue sua chave de API Finexly gratuita — sem cartão de crédito. Comece com 1.000 requisições grátis por mês em mais de 170 moedas, use o conversor de moedas para conferir suas contas e revise os planos de preços quando seu volume crescer.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →