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. Whether you're building trading systems (covered in our forex trading guide), need a free forex API with historical data, or analyzing exchange rate patterns, historical data is essential.
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 calculate_translation_adjustment(self, subsidiary_amounts, from_date, 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
"""
conversion_rate_from = self.api.get_rate(
from_currency='EUR',
to_currency='USD',
date=from_date
)
conversion_rate_to = self.api.get_rate(
from_currency='EUR',
to_currency='USD',
date=to_date
)
amount_usd_from = subsidiary_amounts['EUR'] * conversion_rate_from
amount_usd_to = subsidiary_amounts['EUR'] * conversion_rate_to
fx_adjustment = amount_usd_to - amount_usd_from
return {
'original_currency': 'EUR',
'original_amount': subsidiary_amounts['EUR'],
'conversion_date_from': from_date,
'rate_from': conversion_rate_from,
'usd_from': amount_usd_from,
'conversion_date_to': to_date,
'rate_to': conversion_rate_to,
'usd_to': amount_usd_to,
'fx_adjustment': fx_adjustment
}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 backtest_moving_average_strategy(self, pair, start_date, 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
historical_data = self.api.get_historical_rates(
pair=pair,
start_date=start_date,
end_date=end_date,
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
recent_closes = [d['close'] for d in historical_data[i-window:i]]
ma_fast = sum(recent_closes[-5:]) / 5
ma_slow = sum(recent_closes) / window
# Generate signal
if ma_fast > ma_slow and pair not in positions:
# Buy signal
positions[pair] = {
'entry_price': day['close'],
'entry_date': day['date'],
'quantity': 100
}
elif ma_fast < ma_slow and pair in positions:
# Sell signal
pos = positions[pair]
exit_price = day['close']
pnl = (exit_price - pos['entry_price']) * pos['quantity']
equity += pnl
self.trades.append({
'entry_date': pos['entry_date'],
'exit_date': day['date'],
'entry_price': pos['entry_price'],
'exit_price': exit_price,
'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,
'max_drawdown': self._calculate_max_drawdown(),
'win_rate': self._calculate_win_rate()
}
def _calculate_max_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 _calculate_win_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
start_date = self._subtract_days(date, days + 10)
data = self.api.get_historical_rates(
pair=pair,
start_date=start_date,
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 = avg_gain / avg_loss if avg_loss != 0 else 0
rsi = 100 - (100 / (1 + rs))
return rsi
def find_resistance_levels(self, pair, start_date, end_date):
"""
Identify price resistance levels from historical data
Resistance levels are prices where the pair
has trouble moving higher
"""
data = self.api.get_historical_rates(
pair=pair,
start_date=start_date,
end_date=end_date,
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 _subtract_days(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 calculate_historical_volatility(self, pair, start_date, end_date, window=30):
"""
Calculate rolling historical volatility
Volatility = standard deviation of returns
Used to determine option pricing and hedging costs
"""
data = self.api.get_historical_rates(
pair=pair,
start_date=start_date,
end_date=end_date,
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]
mean_return = sum(window_returns) / len(window_returns)
variance = sum((r - mean_return) ** 2 for r in window_returns) / 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 var_calculation(self, position_value, 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
sorted_returns = sorted(historical_returns)
# Find percentile corresponding to confidence level
percentile_index = int(len(sorted_returns) * (1 - confidence))
worst_return = sorted_returns[percentile_index]
# Calculate maximum loss
max_loss = position_value * worst_return
return {
'confidence_level': confidence,
'max_loss_percentage': worst_return * 100,
'max_loss_dollars': 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
For practical examples of integrating historical rates with live data, see our API documentation. Here's how to fetch historical data:
import requests
from datetime import datetime, timedelta
class HistoricalRatesAPI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = 'https://finexly.com/v1/historical'
def get_rate_on_date(self, from_currency, 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,
'api_key': self.api_key
}
)
response.raise_for_status()
return response.json()
def get_historical_range(self, from_currency, to_currency, start_date, end_date, 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,
'start_date': start_date,
'end_date': end_date,
'interval': interval,
'api_key': self.api_key
}
)
response.raise_for_status()
return response.json()['rates']
def get_rates_for_all_pairs(self, pairs, date):
"""
Efficiently fetch rates for multiple pairs on same date
Args:
pairs: List of (from_curr, to_curr) tuples
date: Single date string
Returns:
Dictionary mapping pairs to rates
"""
results = {}
for from_curr, to_curr in pairs:
rate_data = self.get_rate_on_date(from_curr, to_curr, date)
results[f"{from_curr}/{to_curr}"] = rate_data['rate']
return resultsEfficient Data Storage
For analysis work, store historical data locally:
import sqlite3
from datetime import datetime
class HistoricalRatesCache:
def __init__(self, db_path='historical_rates.db'):
self.db_path = db_path
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
self._initialize_db()
def _initialize_db(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,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(pair, date)
)
''')
self.conn.commit()
def store_rates(self, pair, rate_data):
"""Store rate data (supports bulk insert)"""
for data_point in rate_data:
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 get_rates(self, pair, start_date, 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, start_date, end_date))
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. Check our pricing page to see how much historical data access is included in each plan. The free tier provides enough historical data to get started with backtesting and analysis work.
Ready to compare different API architectures for accessing this data? See our REST vs WebSocket comparison to choose the best approach for your use case.
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.
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 →