Note de la rédaction : ce panorama s'appuie principalement sur des sources et services des marchés russophones (CEI) ; les principes décrits restent applicables aux marchés francophones et européens.
Le taux de change est l'une de ces valeurs qui semblent inoffensives (« juste un nombre »), mais qui, manipulées sans précaution, deviennent une source de bugs difficiles à traquer : écarts d'un centime, nominal mal pris en compte, taux « perdu » le week-end, erreur d'arrondi accumulée dans le rapport annuel. Cet article est un tour d'horizon pratique : où récupérer les taux (banques centrales de la CEI et services internationaux), comment les parser dans cinq langages, dans quel type de données les stocker, et pourquoi les taux bancaires sont une histoire à part, avec leurs propres agrégateurs.
1. D'où viennent les taux de change
Il est important de distinguer d'emblée deux types de taux fondamentalement différents :
Le taux officiel de la banque centrale. Une seule valeur par date, sans achat/vente. C'est un taux « comptable » : il sert au calcul des impôts, des droits de douane, de la comptabilité, des contrats. Il est stable, publié selon un calendrier fixe (en général une fois par jour), et presque toutes les banques centrales proposent une source gratuite et lisible par machine.
Les taux des banques commerciales et des bureaux de change. Chaque banque a ses propres cours d'achat et de vente avec un spread ; ils changent au fil de la journée, s'écartent du taux officiel et dépendent de la banque, de la ville, du montant, du règlement en espèces ou par virement. Ici, pas de source unique : ce sont les agrégateurs qui s'en chargent (section 6).
Pour la plupart des besoins (comptabilité, prix multidevises, convertisseurs), c'est le taux officiel de la banque centrale qu'il faut. Si l'objectif est de « montrer à l'utilisateur où acheter des dollars au meilleur prix », ce sont les taux bancaires qu'il faut.
2. Banques centrales de la CEI et des pays voisins
Presque toutes ces banques centrales fournissent leurs données gratuitement et sans clé API. Les formats varient : ici un JSON propre, là un XML encodé en windows-1251, ailleurs du RSS.
| Pays | Organisme | Endpoint (taux officiel du jour) | Format |
|---|---|---|---|
| Russie | Banque de Russie (CBR) | https://www.cbr.ru/scripts/XML_daily.asp (historique — XML_dynamic.asp) |
XML (cp1251) |
| Kazakhstan | Banque nationale (NBK) | https://nationalbank.kz/rss/get_rates.cfm?fdate=DD.MM.YYYY |
XML/RSS |
| Biélorussie | Banque nationale (NBRB) | https://api.nbrb.by/exrates/rates?periodicity=0 |
JSON |
| Ouzbékistan | Banque centrale (CBU) | https://cbu.uz/ru/arkhiv-kursov-valyut/json/ |
JSON/XML |
| Kirghizistan | Banque nationale (NBKR) | https://www.nbkr.kg/XML/daily.xml |
XML |
| Azerbaïdjan | Banque centrale (CBAR) | https://www.cbar.az/currencies/DD.MM.YYYY.xml |
XML |
| Arménie | Banque centrale (CBA) | service de taux sur cb.am |
XML/SOAP |
| Moldavie | Banque nationale (BNM) | https://www.bnm.md/ru/official_exchange_rates?get_xml=1&date=DD.MM.YYYY |
XML |
| Géorgie | Banque nationale (NBG) | https://nbg.gov.ge/gw/api/ct/monetarypolicy/currencies/ru/json/ |
JSON |
| Tadjikistan | Banque nationale (NBT) | taux sur nbt.tj |
XML |
Les endpoints sont donnés à la date de rédaction. Les banques font régulièrement évoluer leur infrastructure (la NBK comme la CBR ont par exemple, en leur temps, imposé le passage complet au HTTPS) ; avant la mise en production, vérifiez donc la page officielle « pour les webmasters » de la banque centrale concernée.
Points de vigilance dans les données des banques centrales :
- Le nominal (nominal / scale / quant). Le taux est souvent donné non pas pour 1 unité, mais pour 10, 100 ou 1000. Chez la NBRB, par exemple, le yen japonais est coté pour 100 unités. Ignorer ce champ est l'erreur classique qui donne un taux surévalué d'un facteur 100.
- L'encodage. La CBR fournit son XML en windows-1251, pas en UTF-8. Lisez-le « tel quel » et vous obtiendrez des caractères illisibles dans les libellés.
- Le séparateur décimal. Dans le XML de la CBR, la partie décimale est séparée par une virgule (
74,1234), pas par un point. - Le calendrier. Le taux est publié à l'avance pour le jour ouvré suivant et/ou à l'issue des séances de bourse. Au Kazakhstan, par exemple, le taux officiel du tenge face au dollar est le cours boursier moyen pondéré à 15 h 30 (heure d'Astana), fixé pour le jour ouvré suivant.
3. API internationales
Quand il faut des taux croisés, de nombreuses devises ou une source unique « tout vers USD/EUR », les services internationaux sont plus pratiques.
| Service | Source des données | Clé | Quota gratuit | Remarques |
|---|---|---|---|---|
Frankfurter (api.frankfurter.dev) |
BCE | non requise | pas de limite mensuelle (seulement une protection anti-abus) | ~30 devises, historique depuis 1999, open-source, self-host possible |
BCE en direct (fichier eurofxref-daily.xml sur ecb.europa.eu) |
BCE | non requise | oui | fichier XML de la source primaire, tout vers EUR |
NBP (Pologne, api.nbp.pl) |
Banque nationale de Pologne | non requise | oui | table C avec buy/sell |
| exchangerate-api.com | mélange de plusieurs banques centrales | requise | ~1500/mois | 160+ devises, midpoint moyenné |
| Open Exchange Rates | agrégat | requise | 1000/mois | en free, base USD uniquement |
| Fixer.io / currencylayer (apilayer) | BCE et autres | requise | ~100/mois | |
| currencyapi.com | agrégat | requise | ~300/mois | crypto disponible |
| Twelve Data / Alpha Vantage | cotations boursières | requise | limité | forex intraday, pas un taux « comptable » |
Nuance essentielle avec les agrégateurs internationaux : leurs taux sont un midpoint indicatif (sans spread). Ils conviennent très bien à une conversion indicative de prix en e-commerce ou à des dashboards, mais ils ne conviennent pas au trading forex réel ni au calcul du montant que la banque prélèvera réellement lors d'une conversion. Frankfurter et la BCE, de plus, ne publient de taux que les jours ouvrés : une requête au 1er janvier renverra le taux du dernier jour ouvré, et la réponse contiendra un champ date avec la date effective du taux — c'est elle qui fait foi, pas la date demandée.
4. Cinq solutions dans différents langages
Pour montrer la diversité des sources et des langages, chaque exemple cible sa propre banque centrale. Partout, l'accent est mis délibérément sur deux points : la prise en compte du nominal et le stockage de la valeur sans float binaire.
4.1. PHP — CBR (XML, windows-1251, BCMath)
<?php
declare(strict_types=1);
/**
* Renvoie les taux de la CBR pour 1 unité de devise, sous forme de chaînes.
* Chaînes + BCMath — pour ne pas perdre de précision sur des float.
*/
function fetchCbrRates(): array
{
$raw = file_get_contents('https://www.cbr.ru/scripts/XML_daily.asp');
if ($raw === false) {
throw new RuntimeException('Impossible de récupérer les données de la CBR');
}
// le XML arrive encodé en windows-1251 — on le convertit
$utf8 = mb_convert_encoding($raw, 'UTF-8', 'windows-1251');
$xml = simplexml_load_string($utf8);
if ($xml === false) {
throw new RuntimeException('Erreur de parsing XML');
}
$rates = [];
foreach ($xml->Valute as $valute) {
$code = (string) $valute->CharCode; // USD, EUR...
$nominal = (string) $valute->Nominal; // 1, 10, 100
// le séparateur décimal du XML de la CBR est la virgule
$value = str_replace(',', '.', (string) $valute->Value);
// taux pour UNE unité, précision 6 décimales, sous forme de chaîne
$rates[$code] = bcdiv($value, $nominal, 6);
}
return $rates;
}
$rates = fetchCbrRates();
echo "USD: {$rates['USD']} RUB\n";
echo "EUR: {$rates['EUR']} RUB\n";
Le point notable ici est bcdiv : la division par le nominal se fait en arithmétique sur chaînes, sans conversion en float. Nécessite l'extension bcmath.
4.2. Python — NBRB (JSON, Decimal)
from decimal import Decimal, getcontext
import requests
getcontext().prec = 28 # marge de précision suffisante
def fetch_nbrb_rates() -> dict[str, Decimal]:
"""Taux officiels de la NBRB pour 1 unité de devise (type Decimal)."""
resp = requests.get(
"https://api.nbrb.by/exrates/rates",
params={"periodicity": 0}, # 0 — taux quotidiens
timeout=10,
)
resp.raise_for_status()
rates: dict[str, Decimal] = {}
for item in resp.json():
code = item["Cur_Abbreviation"] # USD, EUR, RUB...
scale = Decimal(str(item["Cur_Scale"])) # nominal (1, 100...)
rate = Decimal(str(item["Cur_OfficialRate"]))
rates[code] = rate / scale # taux pour une unité
return rates
if __name__ == "__main__":
rates = fetch_nbrb_rates()
print(f"USD: {rates['USD']:.4f} BYN")
print(f"RUB(pour 100): {rates['RUB'] * 100:.4f} BYN")
Le point crucial : Decimal(str(value)), et non Decimal(value). Si le nombre est déjà arrivé en float, le passage par str fige exactement la représentation décimale qui figurait dans le JSON.
4.3. JavaScript / Node.js — CBU (JSON, fetch natif)
// Node 18+ — fetch intégré
async function fetchCbuRates() {
const res = await fetch("https://cbu.uz/ru/arkhiv-kursov-valyut/json/");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); // tableau d'objets, un par devise
const rates = {};
for (const item of data) {
const code = item.Ccy; // USD, EUR, RUB...
const nominal = Number(item.Nominal); // en général 1
// Rate arrive en chaîne — on garde la valeur brute et la valeur normalisée
rates[code] = {
raw: item.Rate, // chaîne, sans perte
perOne: Number(item.Rate) / nominal, // pour l'affichage
date: item.Date,
};
}
return rates;
}
fetchCbuRates().then((rates) => {
console.log(`USD: ${rates.USD.perOne} UZS (au ${rates.USD.date})`);
console.log(`EUR: ${rates.EUR.perOne} UZS`);
});
JavaScript n'a pas de type décimal natif, et number est un double IEEE 754. C'est pourquoi on conserve ici à la fois la chaîne brute raw et une valeur numérique pour l'affichage. Pour les calculs monétaires (et pas seulement l'affichage d'un taux), on prend en JS une bibliothèque comme decimal.js ou big.js et on calcule sur des chaînes.
4.4. Go — NBK (XML/RSS, typage strict)
package main
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
)
type Rates struct {
Items []Item `xml:"item"`
}
type Item struct {
Title string `xml:"title"` // code devise : USD, EUR
Description string `xml:"description"` // valeur du taux
Quant string `xml:"quant"` // nominal
}
func fetchNbkRates() (map[string]string, error) {
url := "https://nationalbank.kz/rss/get_rates.cfm?fdate=" +
time.Now().Format("02.01.2006") // format DD.MM.YYYY
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var rates Rates
if err := xml.Unmarshal(body, &rates); err != nil {
return nil, err
}
result := make(map[string]string, len(rates.Items))
for _, it := range rates.Items {
// on garde la chaîne brute ; pour l'arithmétique — shopspring/decimal
result[it.Title] = it.Description
}
return result, nil
}
func main() {
rates, err := fetchNbkRates()
if err != nil {
panic(err)
}
fmt.Printf("USD: %s KZT\n", rates["USD"])
fmt.Printf("EUR: %s KZT\n", rates["EUR"])
}
Go n'a pas de decimal intégré : le taux est donc conservé en chaîne, et pour les calculs on ajoute github.com/shopspring/decimal. Le week-end, le fichier peut arriver vide — pensez à gérer ce cas (reprendre la date précédente).
4.5. C# / .NET — Frankfurter (BCE, source internationale, decimal)
using System.Net.Http.Json;
using System.Text.Json.Serialization;
public record FrankfurterResponse(
[property: JsonPropertyName("base")] string Base,
[property: JsonPropertyName("date")] string Date,
[property: JsonPropertyName("rates")] Dictionary<string, decimal> Rates
);
public static class CurrencyClient
{
private static readonly HttpClient Http = new();
public static async Task<FrankfurterResponse> FetchEcbRatesAsync(string baseCcy = "EUR")
{
var url = $"https://api.frankfurter.dev/v1/latest?base={baseCcy}";
return await Http.GetFromJsonAsync<FrankfurterResponse>(url)
?? throw new InvalidOperationException("Réponse vide de Frankfurter");
}
}
class Program
{
static async Task Main()
{
var data = await CurrencyClient.FetchEcbRatesAsync("USD");
Console.WriteLine($"Taux à la date : {data.Date}"); // date effective de la BCE
Console.WriteLine($"USD->EUR: {data.Rates["EUR"]}");
Console.WriteLine($"USD->GBP: {data.Rates["GBP"]}");
}
}
System.Text.Json désérialise les nombres JSON directement en decimal (en lisant le littéral texte), donc sans perte de précision. En .NET, decimal est le bon type aussi bien pour les taux que pour l'argent.
5. Dans quel type de données stocker un taux
C'est sans doute la principale question technique du sujet — et celle où l'on se trompe le plus souvent.
Pourquoi pas float/double
Les nombres binaires à virgule flottante (IEEE 754) sont physiquement incapables de représenter exactement la plupart des fractions décimales. 0.1 + 0.2 n'est pas égal à 0.3. Sur un taux isolé, cela passe inaperçu ; mais avec la multiplication par des montants, les reconversions répétées et l'agrégation sur une période, les erreurs s'accumulent — et le rapport financier affiche des écarts inexpliqués d'un centime qui ne passent pas le rapprochement comptable. Pour l'argent et les taux, float/double sont à proscrire.
Que faut-il utiliser
| Niveau | Bon choix |
|---|---|
| Base de données | DECIMAL / NUMERIC avec precision et scale fixes |
| Python | decimal.Decimal |
| PHP | BCMath / chaînes (ou une bibliothèque Money) |
| Java / C# | BigDecimal / decimal |
| Go | github.com/shopspring/decimal |
| JavaScript | decimal.js / big.js (stocker en chaîne) |
Combien de décimales
Les taux sont généralement publiés avec 4–6 décimales, mais les devises à grand nominal (UZS, IDR, anciennes dénominations) donnent de grandes parties entières. Le compromis sûr en base de données est NUMERIC(20, 6) ; pour le « taux pour 1 unité » normalisé, on prend parfois plus de décimales, par exemple NUMERIC(24, 10), afin que la division par le nominal ne perde pas de précision.
Que stocker en plus du nombre lui-même
Un taux sans contexte ne sert à rien. Un enregistrement minimal utile contient la source, les deux devises, le nominal, la date d'effet et le type de taux :
CREATE TABLE exchange_rates (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(32) NOT NULL, -- 'CBR', 'NBRB', 'CBU', 'NBK', 'ECB'
base_ccy CHAR(3) NOT NULL, -- devise de cotation : RUB, BYN, UZS, KZT
quote_ccy CHAR(3) NOT NULL, -- devise cotée : USD, EUR...
nominal INTEGER NOT NULL DEFAULT 1,
rate NUMERIC(20,6) NOT NULL, -- taux pour `nominal` unités (comme à la source)
rate_per_one NUMERIC(24,10) NOT NULL, -- taux normalisé pour 1 unité
rate_type VARCHAR(8) NOT NULL DEFAULT 'official', -- official | buy | sell
effective_date DATE NOT NULL, -- date d'effet du taux
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source, base_ccy, quote_ccy, rate_type, effective_date)
);
Stocker à la fois le rate « brut » (tel que fourni par la source, avec son nominal) et le rate_per_one normalisé est pratique : le premier sert au rapprochement avec la source primaire, le second aux calculs. Les codes devises suivent la norme ISO 4217 (trois lettres), ce qui élimine le problème des graphies multiples.
Un mot à part sur les montants monétaires (et non les taux) : on les stocke souvent sous forme d'entier en unités mineures — kopecks, cents, tiyins. Autrement dit, 19.99 USD =
1999. Cela exclut par principe toute arithmétique fractionnaire. Mais les taux eux-mêmes ne se stockent pas ainsi : il leur faut une fraction décimale.
6. Taux bancaires et agrégateurs
Le taux officiel de la banque centrale est unique. Mais la personne qui va changer des devises voit de tout autres chiffres : chaque banque a ses propres cours d'achat et de vente, avec un spread, qui évoluent au fil de la journée. Réunir en un seul endroit les taux de dizaines de banques, c'est précisément la mission des agrégateurs.
Exemples d'agrégateurs par pays :
- Russie : banki.ru, sravni.ru, finmarket.ru — taux des banques, meilleures offres d'achat/vente, par ville.
- Biélorussie : myfin.by, select.by, banki24.by — taux des bureaux de change et des banques, souvent détaillés par ville et par agence.
- Kazakhstan : kurs.kz, mybanker.kz, bank.kz — taux des bureaux de change par ville.
- Ouzbékistan : bank.uz, kurs.uz — taux des banques face au soum.
- International / crypto P2P : p2p.army, fexant et services similaires agrègent les taux des bourses et des plateformes P2P.
Nuance technique : beaucoup d'agrégateurs n'ont pas d'API ouverte — il faut extraire les données du HTML par scraping. Le respect des règles est ici critique : respecter le robots.txt et les conditions d'utilisation, ne pas marteler le site de requêtes (rate limiting, mise en cache), citer la source quand c'est possible. Certains services proposent une API, mais payante. Les données des banques centrales, en revanche, peuvent en règle générale être librement réutilisées.
Si vous construisez votre propre agrégateur, l'architecture raisonnable est un ensemble de collecteurs indépendants (un par source) → une couche de normalisation (conversion vers ISO 4217 et vers le taux par unité, séparation buy/sell) → un stockage unique → votre propre API par-dessus. Dans le modèle de données, contrairement aux taux des banques centrales, apparaissent obligatoirement des dimensions : la banque, le type d'opération (achat/vente, espèces/virement), parfois la ville et le montant, et presque toujours l'heure exacte du relevé, car un tel taux ne vit que quelques minutes.
7. Conseils pratiques
- Mettez en cache. Les banques centrales actualisent leur taux une fois par jour (rarement deux). Appeler l'API plus souvent que les données ne changent est inutile et contre-productif — mettez en cache côté application.
- Tenez compte des week-ends et jours fériés. Les jours chômés, les banques centrales ne publient pas de nouveau taux — c'est en général le dernier qui est renvoyé. Regardez toujours la date du taux dans la réponse, pas la date demandée.
- Prévoyez un fallback. Une source peut tomber en panne. Il est utile d'avoir une solution de repli (par exemple une API internationale comme Frankfurter, ou une autre banque centrale) ainsi que des timeouts avec retries à délai exponentiel.
- Normalisez à l'entrée. Conversion d'encodage (windows-1251 chez la CBR), remplacement de la virgule par le point, division par le nominal, alignement des codes sur ISO 4217 — tout cela se fait de préférence dès le chargement, pour ne mettre en base que des données propres.
- Surveillez les anomalies. Un taux qui bondit soudainement à plusieurs fois sa valeur est plus souvent le signe d'une erreur de la source ou du parsing (nominal oublié !) que d'un événement réel. Un simple contrôle « écart par rapport à la veille inférieur à N % » attrape la plupart de ces cas.
8. Domaines d'application
- E-commerce — prix multidevises, tarifs localisés pour l'acheteur.
- Comptabilité et gestion — conversion des opérations au taux de la banque centrale à date, impôts, droits de douane.
- Fintech, portefeuilles, échange P2P — conversion et affichage des soldes.
- Analytique et dashboards BI — consolidation d'un chiffre d'affaires multidevises en une seule devise.
- Convertisseurs et services de voyage — conversion rapide pour l'utilisateur.
- Contrats et facturation — fixation du taux à la date d'émission de la facture.
En résumé
Le scraping des taux de change est une tâche où 80 % de la difficulté ne tient pas à la requête HTTP mais aux détails : nominal, encodage, séparateur décimal, jours non ouvrés et — surtout — le choix d'un type décimal à la place de float sur tout le chemin, du scraper à la base. Pour les taux officiels, il existe presque toujours une source gratuite chez la banque centrale elle-même ; pour les taux bancaires, il faut des agrégateurs et un scraping soigné, respectueux de la source. Ajoutez-y la mise en cache et la normalisation à l'entrée — et vous obtenez des données auxquelles on peut se fier dans des calculs financiers.