While real-time exchange rates grab most attention, historical exchange rate data is equally valuable. Financial analysts use history to understand trends, traders backtest strategies on past data, accountants need historical rates for financial records, and businesses forecast future currency movements based on patterns. This guide covers everything you need to know about working with a historical exchange rates API.
Why Historical Exchange Rate Data Matters
Historical exchange rate data serves purposes that real-time rates cannot:
Regulatory Compliance: Companies with international operations must report financial transactions at the historical exchange rate used on the transaction date. GAAP and IFRS both require this.
Accurate Financial Reporting: When you invoice a customer in EUR but receive payment in USD three months later, you need the rate from both dates to properly record currency gains or losses.
Trend Analysis: You can't identify trends without history. Are EUR/USD rates rising or falling? By how much? Over what timeframe?
Strategy Backtesting: Before deploying a trading algorithm in production, you backtest it against historical data to validate performance under past market conditions.
Forecasting: Machine learning models for currency forecasting require years of historical data to train effectively.
Audit Trails: For audit purposes, you need to prove what rate was used when, with timestamps and source documentation.
Types of Historical Data
Daily Exchange Rates
The most common historical exchange rate data format. A single rate (usually the closing price) for each currency pair per day.
{
"date": "2026-04-01",
"pair": "EURUSD",
"open": 1.08510,
"high": 1.08650,
"low": 1.08420,
"close": 1.08542,
"volume": 150000000
}Daily data works for:
- Long-term trend analysis
- Financial reporting
- Compliance records
- Portfolio analysis
Intraday Data (Hourly, Minute-Level)
More granular historical data capturing prices at frequent intervals throughout the trading day.
{
"timestamp": "2026-04-01T14:30:00Z",
"pair": "EURUSD",
"bid": 1.08542,
"ask": 1.08544,
"volume": 500000
}Intraday data required for:
- Algorithm backtesting
- Technical analysis
- High-frequency trading strategy development
- Market microstructure studies
Tick Data
Every individual price change, often with microsecond timestamps.
{
"timestamp": "2026-04-01T14:30:45.123456Z",
"tick_id": 1234567,
"pair": "EURUSD",
"bid": 1.08542,
"ask": 1.08544,
"bid_volume": 5000000,
"ask_volume": 4800000
}Tick data used by:
- Algorithmic traders
- Market makers
- High-frequency trading firms
- Academic research
Common Use Cases
Financial Reporting
Companies with international operations must value foreign currency assets at appropriate historical rates:
class CurrencyExposure:
def init(self, historical_api):
self.api = historical_api def calculatetranslationadjustment(self, subsidiaryamounts, fromdate, to_date):
"""
Calculate foreign exchange gain/loss on balance sheet consolidation
Example: French subsidiary has €1M in revenue
Need to translate both at historical rates for accuracy
"""
conversionratefrom = self.api.get_rate(
from_currency='EUR',
to_currency='USD',
date=from_date
)
conversionrateto = self.api.get_rate(
from_currency='EUR',
to_currency='USD',
date=to_date
)
amountusdfrom = subsidiaryamounts['EUR'] * conversionrate_from
amountusdto = subsidiaryamounts['EUR'] * conversionrate_to
fxadjustment = amountusdto - amountusd_from
return {
'original_currency': 'EUR',
'originalamount': subsidiaryamounts['EUR'],
'conversiondatefrom': from_date,
'ratefrom': conversionrate_from,
'usdfrom': amountusd_from,
'conversiondateto': to_date,
'rateto': conversionrate_to,
'usdto': amountusd_to,
'fxadjustment': fxadjustment
}
Backtesting Trading Strategies
Before deploying a trading strategy with real money, test it against historical data:
class StrategyBacktester:
def init(self, historical_api):
self.api = historical_api
self.trades = []
self.equity_curve = [] def backtestmovingaveragestrategy(self, pair, startdate, end_date, window=20):
"""
Test a simple moving average crossover strategy
Strategy: Buy when fast MA > slow MA, sell when fast MA < slow MA
"""
# Fetch historical daily data
historicaldata = self.api.gethistorical_rates(
pair=pair,
startdate=startdate,
enddate=enddate,
interval='daily'
)
positions = {}
equity = 100000 # Starting capital
for i, day in enumerate(historical_data):
if i < window:
continue # Need enough data for MA calculation
# Calculate moving averages
recentcloses = [d['close'] for d in historicaldata[i-window:i]]
mafast = sum(recentcloses[-5:]) / 5
maslow = sum(recentcloses) / window
# Generate signal
if mafast > maslow and pair not in positions:
# Buy signal
positions[pair] = {
'entry_price': day['close'],
'entry_date': day['date'],
'quantity': 100
}
elif mafast < maslow and pair in positions:
# Sell signal
pos = positions[pair]
exit_price = day['close']
pnl = (exitprice - pos['entryprice']) * pos['quantity']
equity += pnl
self.trades.append({
'entrydate': pos['entrydate'],
'exit_date': day['date'],
'entryprice': pos['entryprice'],
'exitprice': exitprice,
'quantity': pos['quantity'],
'pnl': pnl
})
del positions[pair]
self.equity_curve.append({
'date': day['date'],
'equity': equity,
'open_position': pair in positions
})
return {
'starting_equity': 100000,
'ending_equity': equity,
'total_return': (equity - 100000) / 100000,
'trades': self.trades,
'maxdrawdown': self.calculatemaxdrawdown(),
'winrate': self.calculatewinrate()
}
def calculatemax_drawdown(self):
"""Calculate maximum drawdown during backtest"""
if not self.equity_curve:
return 0
peak = self.equity_curve[0]['equity']
max_dd = 0
for point in self.equity_curve:
if point['equity'] > peak:
peak = point['equity']
dd = (peak - point['equity']) / peak
if dd > max_dd:
max_dd = dd
return max_dd
def calculatewin_rate(self):
"""Calculate percentage of winning trades"""
if not self.trades:
return 0
winning_trades = sum(1 for t in self.trades if t['pnl'] > 0)
return winning_trades / len(self.trades)
Technical Analysis
Historical data enables technical analysis—identifying patterns and trends:
class TechnicalAnalysis:
def init(self, historical_api):
self.api = historical_api def calculate_rsi(self, pair, date, days=14):
"""
Calculate Relative Strength Index
RSI measures overbought/oversold conditions
Values above 70 = overbought, below 30 = oversold
"""
# Fetch last N days of data
startdate = self.subtract_days(date, days + 10)
data = self.api.gethistoricalrates(
pair=pair,
startdate=startdate,
end_date=date,
interval='daily'
)
closes = [d['close'] for d in data]
# Calculate gains and losses
gains = []
losses = []
for i in range(1, len(closes)):
change = closes[i] - closes[i-1]
if change > 0:
gains.append(change)
losses.append(0)
else:
gains.append(0)
losses.append(-change)
avg_gain = sum(gains[-days:]) / days
avg_loss = sum(losses[-days:]) / days
rs = avggain / avgloss if avg_loss != 0 else 0
rsi = 100 - (100 / (1 + rs))
return rsi
def findresistancelevels(self, pair, startdate, enddate):
"""
Identify price resistance levels from historical data
Resistance levels are prices where the pair
has trouble moving higher
"""
data = self.api.gethistoricalrates(
pair=pair,
startdate=startdate,
enddate=enddate,
interval='hourly'
)
highs = [d['high'] for d in data]
# Find local maxima (price bounced down from here)
resistance_levels = []
for i in range(1, len(highs) - 1):
if highs[i] > highs[i-1] and highs[i] > highs[i+1]:
resistance_levels.append({
'level': highs[i],
'touches': 0,
'date': data[i]['date']
})
# Count how many times price touched each level
for resistance in resistance_levels:
for high in highs:
if abs(high - resistance['level']) < 0.0001:
resistance['touches'] += 1
# Filter to significant levels (touched multiple times)
return [r for r in resistance_levels if r['touches'] >= 3]
def subtractdays(self, date_str, days):
"""Subtract days from a date string (YYYY-MM-DD format)"""
from datetime import datetime, timedelta
date = datetime.strptime(date_str, '%Y-%m-%d')
result = date - timedelta(days=days)
return result.strftime('%Y-%m-%d')
Risk Management and Hedging
Historical volatility helps determine hedging requirements:
class VolatilityCalculator:
def init(self, historical_api):
self.api = historical_api def calculatehistoricalvolatility(self, pair, startdate, enddate, window=30):
"""
Calculate rolling historical volatility
Volatility = standard deviation of returns
Used to determine option pricing and hedging costs
"""
data = self.api.gethistoricalrates(
pair=pair,
startdate=startdate,
enddate=enddate,
interval='daily'
)
closes = [d['close'] for d in data]
# Calculate returns
returns = []
for i in range(1, len(closes)):
ret = (closes[i] - closes[i-1]) / closes[i-1]
returns.append(ret)
volatility_readings = []
for i in range(window, len(returns)):
window_returns = returns[i-window:i]
meanreturn = sum(windowreturns) / len(window_returns)
variance = sum((r - meanreturn) 2 for r in windowreturns) / len(window_returns)
volatility = variance 0.5
volatility_readings.append({
'date': data[i]['date'],
'volatility': volatility,
'annualized': volatility (252 * 0.5) # 252 trading days/year
})
return volatility_readings
def varcalculation(self, positionvalue, historical_returns, confidence=0.95):
"""
Calculate Value at Risk using historical returns
VAR = maximum expected loss at given confidence level
"""
# Sort returns from worst to best
sortedreturns = sorted(historicalreturns)
# Find percentile corresponding to confidence level
percentileindex = int(len(sortedreturns) * (1 - confidence))
worstreturn = sortedreturns[percentile_index]
# Calculate maximum loss
maxloss = positionvalue * worst_return
return {
'confidence_level': confidence,
'maxlosspercentage': worst_return * 100,
'maxlossdollars': max_loss,
'interpretation': f"There is a {(1-confidence)*100}% chance of losing more than ${abs(max_loss):.2f}"
}
API Implementation Patterns
Basic Historical Data Retrieval
import requests
from datetime import datetime, timedeltaclass HistoricalRatesAPI:
def init(self, api_key):
self.apikey = apikey
self.base_url = 'https://finexly.com/v1/historical'
def getrateondate(self, fromcurrency, to_currency, date):
"""
Get exchange rate for specific date
Args:
from_currency: Source currency (e.g., 'EUR')
to_currency: Target currency (e.g., 'USD')
date: Date string (YYYY-MM-DD format)
Returns:
Exchange rate for that date
"""
response = requests.get(
f"{self.base_url}/rate",
params={
'from': from_currency,
'to': to_currency,
'date': date,
'apikey': self.apikey
}
)
response.raiseforstatus()
return response.json()
def gethistoricalrange(self, fromcurrency, tocurrency, startdate, enddate, interval='daily'):
"""
Fetch historical rates for a date range
Args:
start_date: Start date (YYYY-MM-DD)
end_date: End date (YYYY-MM-DD)
interval: 'daily', 'hourly', or 'minute'
Returns:
List of rate data points
"""
response = requests.get(
f"{self.base_url}/range",
params={
'from': from_currency,
'to': to_currency,
'startdate': startdate,
'enddate': enddate,
'interval': interval,
'apikey': self.apikey
}
)
response.raiseforstatus()
return response.json()['rates']
def getratesforallpairs(self, pairs, date):
"""
Efficiently fetch rates for multiple pairs on same date
Args:
pairs: List of (fromcurr, tocurr) tuples
date: Single date string
Returns:
Dictionary mapping pairs to rates
"""
results = {}
for fromcurr, tocurr in pairs:
ratedata = self.getrateondate(fromcurr, tocurr, date)
results[f"{fromcurr}/{tocurr}"] = rate_data['rate']
return results
Efficient Data Storage
For analysis work, store historical data locally:
import sqlite3
from datetime import datetimeclass HistoricalRatesCache:
def init(self, dbpath='historicalrates.db'):
self.dbpath = dbpath
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
self.initializedb()
def initializedb(self):
"""Create tables if they don't exist"""
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS rates (
id INTEGER PRIMARY KEY,
pair TEXT,
date TEXT,
open REAL,
high REAL,
low REAL,
close REAL,
volume INTEGER,
createdat TIMESTAMP DEFAULT CURRENTTIMESTAMP,
UNIQUE(pair, date)
)
''')
self.conn.commit()
def storerates(self, pair, ratedata):
"""Store rate data (supports bulk insert)"""
for datapoint in ratedata:
try:
self.cursor.execute('''
INSERT INTO rates (pair, date, open, high, low, close, volume)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
pair,
data_point['date'],
data_point['open'],
data_point['high'],
data_point['low'],
data_point['close'],
data_point.get('volume', 0)
))
except sqlite3.IntegrityError:
pass # Duplicate, skip
self.conn.commit()
def getrates(self, pair, startdate, end_date):
"""Retrieve rates from cache"""
self.cursor.execute('''
SELECT date, open, high, low, close, volume
FROM rates
WHERE pair = ? AND date BETWEEN ? AND ?
ORDER BY date
''', (pair, startdate, enddate))
return self.cursor.fetchall()
def del(self):
self.conn.close()
Conclusion
Historical exchange rate data enables the analytical work that separates sophisticated financial applications from simple currency converters. Whether you're building tools for compliance reporting, backtesting trading strategies, or analyzing currency trends, reliable historical data is essential.
Finexly provides both recent historical data and deep archives, accessible through simple REST APIs. Store the data locally for analysis-heavy work, or query on-demand for sporadic needs. The free tier provides enough historical data to get started with backtesting and analysis work.
Start by downloading a few months of historical data, experiment with the analysis patterns shown here, and expand your time horizons as you develop more sophisticated analysis. Historical data is the foundation of smarter financial decision-making.
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 →