Historical Exchange Rates API: Complete Developer Guide
Historical exchange rate data is essential for financial reporting, algorithmic trading backtests, audit compliance, and time-series analysis. This guide teaches you how to work with historical currency APIs, from simple date lookups to complex multi-year analysis pipelines, with production-ready code examples in Python and JavaScript.
While most developers focus on real-time exchange rates, historical data unlocks powerful capabilities: accurate financial reporting in multiple currencies, backtesting trading strategies, generating audit trails for compliance, and analyzing currency trends over decades.
However, working with historical currency data introduces unique challenges. Exchange markets close on weekends and holidays. Different currencies use different market hours. APIs have varying data depths (some offer 5 years, others 25+ years). This guide solves these problems with practical patterns you can implement today.
1. Use Cases for Historical Exchange Rates
Financial Reporting and Accounting
International businesses must convert foreign transactions to their reporting currency using the exchange rate on the transaction date, not today's rate.
Example Scenario
A US company (reporting in USD) received a €50,000 payment on March 15, 2023. For accurate financial statements, you need the EUR/USD rate from March 15, 2023 (not today's rate).
Transaction Date: 2023-03-15
Amount: €50,000
EUR/USD Rate (2023-03-15): 1.0650
USD Value: $53,250
Algorithmic Trading Backtesting
Before deploying a forex trading algorithm with real money, test it against historical data to validate the strategy and measure expected returns.
- Test strategies across multiple market conditions (bull markets, bear markets, high volatility)
- Calculate Sharpe ratio and maximum drawdown using historical prices
- Identify optimal entry and exit points based on historical patterns
- Validate machine learning models with train/test splits
Audit and Compliance
Regulatory audits require you to prove the exchange rates used in historical transactions came from reliable, documented sources.
Best Practice: Store the exchange rate and its source with every transaction. Don't recalculate historical transactions with current rates. This creates an auditable trail and prevents disputes about which rate was used.
Trend Analysis and Forecasting
Analyze decades of exchange rate data to identify patterns, seasonality, and correlations with economic indicators.
- Build ARIMA or LSTM models for exchange rate forecasting
- Calculate moving averages and identify support/resistance levels
- Correlate currency movements with commodity prices or interest rates
- Generate reports showing currency exposure over time
2. Data Availability and Coverage
Historical Data Depth by Provider
Not all APIs offer the same historical depth. Here's what major providers offer:
| Provider | Historical Depth | Free Tier Access | Data Source |
|---|---|---|---|
| Frankfurter | 1999-present (26 years) | Full access | ECB |
| UniRateAPI | 1999-present (26 years) | Full access | ECB + Multiple |
| ExchangeRate-API | 2000-present (25 years) | Limited | Commercial banks |
| Open Exchange Rates | 1999-present (26 years) | No access | Aggregated |
| Fixer.io | 2000-present (25 years) | No access | ECB + Commercial |
Understanding Data Granularity
Historical exchange rate APIs typically provide daily snapshots (end-of-day rates). Intraday historical data (hourly, minute-by-minute) is rare and expensive.
Important: Daily historical rates don't include the exact timestamp. Most APIs return the rate as of market close in the data provider's timezone (typically GMT or EST). For precise audit requirements, verify your API's timestamp convention.
Currency Availability Over Time
Not all currencies existed throughout the entire historical period. Key events to remember:
- EUR (Euro): Launched January 1, 1999. No EUR data before this date.
- Legacy European currencies: Deutsche Mark (DEM), French Franc (FRF), etc. ceased 2002.
- Cryptocurrencies: BTC data only available from 2010 onwards.
- Emerging market currencies: May have limited or unreliable data before 2000s.
3. Historical API Endpoints Explained
Single Date Lookup
Retrieve exchange rates for a specific historical date:
# Get rates for March 15, 2023
GET https://api.unirateapi.com/v1/historical/2023-03-15/USD?api_key=YOUR_KEY
Response:
{
"date": "2023-03-15",
"base": "USD",
"rates": {
"EUR": 0.9390,
"GBP": 0.8210,
"JPY": 132.50,
...
}
}
Time Series (Date Range)
Retrieve rates across a date range for trend analysis:
# Get EUR/USD rates for January 2023
GET https://api.unirateapi.com/v1/timeseries?
start_date=2023-01-01&
end_date=2023-01-31&
base=EUR&
currencies=USD&
api_key=YOUR_KEY
Response:
{
"start_date": "2023-01-01",
"end_date": "2023-01-31",
"base": "EUR",
"rates": {
"2023-01-01": { "USD": 1.0650 },
"2023-01-02": { "USD": 1.0672 },
"2023-01-03": { "USD": 1.0590 },
...
}
}
Currency Fluctuation Analysis
Some APIs provide endpoints that calculate change, percentage change, and high/low values automatically:
# Get fluctuation metrics for EUR/USD in 2023
GET https://api.unirateapi.com/v1/fluctuation?
start_date=2023-01-01&
end_date=2023-12-31&
base=EUR&
currencies=USD&
api_key=YOUR_KEY
Response:
{
"base": "EUR",
"start_date": "2023-01-01",
"end_date": "2023-12-31",
"fluctuation": {
"USD": {
"start_rate": 1.0650,
"end_rate": 1.1050,
"change": 0.0400,
"change_pct": 3.76,
"high": 1.1250,
"low": 1.0450
}
}
}
Performance Tip: Use fluctuation endpoints when you need aggregated statistics. Calculating change percentages client-side from time-series data wastes bandwidth and processing power.
4. Python Integration with Pandas
Basic Historical Data Fetcher
Here's a production-ready class for fetching and caching historical rates:
import requests
import pandas as pd
from datetime import datetime, timedelta
from typing import Dict, Optional
import json
import os
class HistoricalRatesFetcher:
"""Fetch and cache historical exchange rates."""
def __init__(self, api_key: str, cache_dir: str = './rate_cache'):
self.api_key = api_key
self.base_url = 'https://api.unirateapi.com/v1'
self.cache_dir = cache_dir
os.makedirs(cache_dir, exist_ok=True)
def get_historical_rate(
self,
date: datetime,
from_currency: str,
to_currency: str
) -> float:
"""
Get exchange rate for a specific historical date.
Args:
date: Date for exchange rate
from_currency: Source currency code
to_currency: Target currency code
Returns:
Exchange rate as float
"""
date_str = date.strftime('%Y-%m-%d')
cache_key = f"{date_str}_{from_currency}_{to_currency}"
cache_file = os.path.join(self.cache_dir, f"{cache_key}.json")
# Check cache
if os.path.exists(cache_file):
with open(cache_file, 'r') as f:
return json.load(f)['rate']
# Fetch from API
response = requests.get(
f"{self.base_url}/historical/{date_str}/{from_currency}",
params={'api_key': self.api_key},
timeout=10
)
response.raise_for_status()
data = response.json()
if to_currency not in data['rates']:
raise ValueError(f"Currency {to_currency} not available for {date_str}")
rate = data['rates'][to_currency]
# Cache the result
with open(cache_file, 'w') as f:
json.dump({'rate': rate, 'date': date_str}, f)
return rate
def get_time_series(
self,
start_date: datetime,
end_date: datetime,
from_currency: str,
to_currency: str
) -> pd.DataFrame:
"""
Get time series of exchange rates.
Returns:
DataFrame with dates as index and rates as column
"""
response = requests.get(
f"{self.base_url}/timeseries",
params={
'api_key': self.api_key,
'start_date': start_date.strftime('%Y-%m-%d'),
'end_date': end_date.strftime('%Y-%m-%d'),
'base': from_currency,
'currencies': to_currency
},
timeout=30
)
response.raise_for_status()
data = response.json()
# Convert to DataFrame
rates_dict = {
date: values[to_currency]
for date, values in data['rates'].items()
}
df = pd.DataFrame.from_dict(
rates_dict,
orient='index',
columns=['rate']
)
df.index = pd.to_datetime(df.index)
df = df.sort_index()
return df
# Usage Example
if __name__ == "__main__":
fetcher = HistoricalRatesFetcher(api_key='your_api_key_here')
# Get single historical rate
rate = fetcher.get_historical_rate(
date=datetime(2023, 3, 15),
from_currency='USD',
to_currency='EUR'
)
print(f"USD/EUR on 2023-03-15: {rate}")
# Get time series for analysis
df = fetcher.get_time_series(
start_date=datetime(2023, 1, 1),
end_date=datetime(2023, 12, 31),
from_currency='EUR',
to_currency='USD'
)
print(f"\nEUR/USD Statistics for 2023:")
print(f"Mean: {df['rate'].mean():.4f}")
print(f"Std Dev: {df['rate'].std():.4f}")
print(f"Min: {df['rate'].min():.4f}")
print(f"Max: {df['rate'].max():.4f}")
Financial Reporting with Pandas
Convert a DataFrame of transactions to reporting currency using historical rates:
import pandas as pd
def convert_transactions_to_reporting_currency(
transactions: pd.DataFrame,
reporting_currency: str,
fetcher: HistoricalRatesFetcher
) -> pd.DataFrame:
"""
Convert foreign currency transactions to reporting currency.
Args:
transactions: DataFrame with columns ['date', 'amount', 'currency']
reporting_currency: Target currency (e.g., 'USD')
fetcher: HistoricalRatesFetcher instance
Returns:
DataFrame with additional 'converted_amount' column
"""
transactions = transactions.copy()
transactions['converted_amount'] = 0.0
for idx, row in transactions.iterrows():
if row['currency'] == reporting_currency:
transactions.at[idx, 'converted_amount'] = row['amount']
else:
try:
rate = fetcher.get_historical_rate(
date=pd.to_datetime(row['date']),
from_currency=row['currency'],
to_currency=reporting_currency
)
transactions.at[idx, 'converted_amount'] = row['amount'] * rate
except Exception as e:
print(f"Error converting row {idx}: {e}")
transactions.at[idx, 'converted_amount'] = None
return transactions
# Example usage
transactions = pd.DataFrame({
'date': ['2023-03-15', '2023-06-20', '2023-09-10'],
'amount': [50000, 75000, 100000],
'currency': ['EUR', 'GBP', 'JPY'],
'description': ['Payment A', 'Payment B', 'Payment C']
})
fetcher = HistoricalRatesFetcher(api_key='your_api_key')
converted = convert_transactions_to_reporting_currency(
transactions,
reporting_currency='USD',
fetcher=fetcher
)
print(converted)
5. JavaScript Integration and Charting
Historical Data Fetcher Class
class HistoricalRatesFetcher {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.unirateapi.com/v1';
this.cache = new Map();
}
/**
* Get historical rate for a specific date
*/
async getHistoricalRate(date, fromCurrency, toCurrency) {
const dateStr = date.toISOString().split('T')[0];
const cacheKey = `${dateStr}_${fromCurrency}_${toCurrency}`;
// Check cache
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
// Fetch from API
const response = await fetch(
`${this.baseUrl}/historical/${dateStr}/${fromCurrency}?api_key=${this.apiKey}`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (!data.rates[toCurrency]) {
throw new Error(`Currency ${toCurrency} not available for ${dateStr}`);
}
const rate = data.rates[toCurrency];
this.cache.set(cacheKey, rate);
return rate;
}
/**
* Get time series data
*/
async getTimeSeries(startDate, endDate, fromCurrency, toCurrency) {
const startStr = startDate.toISOString().split('T')[0];
const endStr = endDate.toISOString().split('T')[0];
const response = await fetch(
`${this.baseUrl}/timeseries?` +
`start_date=${startStr}&` +
`end_date=${endStr}&` +
`base=${fromCurrency}&` +
`currencies=${toCurrency}&` +
`api_key=${this.apiKey}`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Convert to array format for charting
return Object.entries(data.rates).map(([date, rates]) => ({
date: new Date(date),
rate: rates[toCurrency]
}));
}
}
Charting with Chart.js
Display historical exchange rates in an interactive chart:
async function createExchangeRateChart(canvasId, fromCurrency, toCurrency, months = 12) {
const fetcher = new HistoricalRatesFetcher('your_api_key');
// Calculate date range
const endDate = new Date();
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - months);
// Fetch data
const timeSeriesData = await fetcher.getTimeSeries(
startDate,
endDate,
fromCurrency,
toCurrency
);
// Prepare chart data
const chartData = {
labels: timeSeriesData.map(d => d.date.toLocaleDateString()),
datasets: [{
label: `${fromCurrency}/${toCurrency}`,
data: timeSeriesData.map(d => d.rate),
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
fill: true,
tension: 0.1
}]
};
// Create chart
const ctx = document.getElementById(canvasId).getContext('2d');
new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: `${fromCurrency}/${toCurrency} Exchange Rate - Last ${months} Months`
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: 'Exchange Rate'
}
}
}
}
});
}
// Usage
createExchangeRateChart('myChart', 'EUR', 'USD', 12);
6. Handling Missing Data and Weekends
The Weekend Problem
Foreign exchange markets close on weekends and holidays. APIs typically return the last available rate (Friday's close) for Saturday and Sunday.
from datetime import datetime, timedelta
def get_nearest_business_day_rate(
date: datetime,
from_currency: str,
to_currency: str,
fetcher: HistoricalRatesFetcher,
max_lookback_days: int = 7
) -> tuple:
"""
Get exchange rate for nearest business day.
Returns:
Tuple of (rate, actual_date)
"""
current_date = date
for _ in range(max_lookback_days):
try:
rate = fetcher.get_historical_rate(
current_date,
from_currency,
to_currency
)
return rate, current_date
except Exception:
# Try previous day
current_date -= timedelta(days=1)
raise ValueError(
f"No rate found within {max_lookback_days} days of {date}"
)
Forward Filling Missing Data
For time-series analysis, fill missing dates with the last known rate:
def fill_missing_dates(df: pd.DataFrame) -> pd.DataFrame:
"""
Forward-fill missing dates in time series.
Args:
df: DataFrame with DatetimeIndex and 'rate' column
Returns:
DataFrame with all dates filled
"""
# Create complete date range
full_range = pd.date_range(
start=df.index.min(),
end=df.index.max(),
freq='D'
)
# Reindex and forward fill
df_filled = df.reindex(full_range)
df_filled = df_filled.fillna(method='ffill')
return df_filled
Audit Consideration: When using forward-fill for financial reporting, document this methodology clearly. Some regulations require using the actual transaction date's rate, even if it means using Friday's rate for a Saturday transaction.
7. Performance Optimization Strategies
Batch Requests for Multiple Currencies
Instead of making separate API calls for each currency, request all needed currencies in one call:
# Bad: Multiple requests
eur_rate = fetch_rate('2023-03-15', 'USD', 'EUR')
gbp_rate = fetch_rate('2023-03-15', 'USD', 'GBP')
jpy_rate = fetch_rate('2023-03-15', 'USD', 'JPY')
# Good: Single request
response = fetch_all_rates('2023-03-15', 'USD')
eur_rate = response['EUR']
gbp_rate = response['GBP']
jpy_rate = response['JPY']
Persistent Caching Strategy
Historical data never changes, so cache it permanently:
import redis
import json
class CachedHistoricalFetcher:
def __init__(self, api_key: str, redis_client: redis.Redis):
self.api_key = api_key
self.redis = redis_client
def get_historical_rate(self, date: str, base: str, target: str) -> float:
cache_key = f"hist_rate:{date}:{base}:{target}"
# Check Redis cache (no expiration for historical data)
cached = self.redis.get(cache_key)
if cached:
return float(cached)
# Fetch from API
rate = self._fetch_from_api(date, base, target)
# Cache permanently (historical data never changes)
self.redis.set(cache_key, str(rate))
return rate
Pro Tip: Historical data older than 1 year is essentially immutable. Cache it aggressively with no expiration. This dramatically reduces API costs and improves performance for backtesting and analysis workloads.
8. Compliance and Audit Requirements
Creating an Audit Trail
Store complete metadata with every exchange rate used:
class AuditableRateFetcher:
"""Fetch rates with full audit trail."""
def get_rate_with_audit(
self,
date: datetime,
from_currency: str,
to_currency: str
) -> dict:
"""
Get rate with complete audit metadata.
Returns:
Dict with rate, source, timestamp, etc.
"""
rate = self.fetch_rate(date, from_currency, to_currency)
return {
'rate': rate,
'from_currency': from_currency,
'to_currency': to_currency,
'date': date.isoformat(),
'source': 'UniRateAPI',
'fetched_at': datetime.utcnow().isoformat(),
'api_version': 'v1',
'data_source': 'ECB',
'methodology': 'Daily close rate'
}
# Store in database with transaction
transaction_record = {
'amount': 50000,
'currency': 'EUR',
'date': '2023-03-15',
'exchange_rate_metadata': audit_data
}
Regulatory Compliance Requirements
Different regulations have different requirements for exchange rates:
- GAAP (US): Requires use of actual transaction date rates or weighted average rates for the period
- IFRS (International): Similar to GAAP; allows month-end rates for monthly statements
- SOX Compliance: Requires documented, auditable data sources and methodology
- GDPR (EU): Doesn't directly affect exchange rates but impacts how you store user transaction data
Important: This article provides technical guidance only. Always consult with qualified accountants and legal counsel for compliance with specific regulations in your jurisdiction. Exchange rate treatment varies by industry and use case.
Conclusion
Historical exchange rate APIs unlock powerful capabilities for financial applications. Whether you're building a reporting system, backtesting trading algorithms, or ensuring audit compliance, the patterns in this guide provide a solid foundation.
Key takeaways:
- Choose APIs with deep historical data (20+ years) for comprehensive analysis
- Always cache historical data permanently - it never changes
- Handle weekends and holidays by using the nearest business day rate
- Store complete audit metadata with every exchange rate used
- Use time-series endpoints for efficient bulk data retrieval
- Understand your regulatory requirements before implementation
Start with simple single-date lookups for transaction conversion, then expand to time-series analysis as your needs grow. The investment in proper historical data handling pays dividends in accuracy, compliance, and analytical capabilities.
Related Articles
Access 25+ Years of Historical Exchange Rates
Get reliable historical exchange rate data from 1999 to present. Perfect for financial reporting, backtesting trading strategies, and audit compliance. Full API access with no credit card required on the free tier.
Explore Historical Data API