Foreign Exchange (FX) Data¶
Comprehensive guide to foreign exchange rate handling for multi-currency trading strategies.
Overview¶
RustyBT provides a flexible FX rate system for: - Multi-currency portfolios: Track positions in different currencies - Currency conversion: Convert values between currencies at historical rates - Rate management: Store and retrieve exchange rates efficiently - Cross-currency analysis: Compare performance across currency zones
Architecture¶
┌────────────────────────────────────┐
│ Trading Strategy │
│ (Multi-currency portfolio) │
└──────────────┬─────────────────────┘
│
┌──────▼──────┐
│ FXRateReader│
└──────┬──────┘
│
┌──────────┼──────────┐
│ │ │
┌───▼───┐ ┌───▼──┐ ┌───▼────┐
│In Mem │ │ HDF5 │ │Explode │
└───────┘ └──────┘ └────────┘
Core Concepts¶
Rate Names¶
Rates represent different exchange rate quotes:
- mid: Mid-market rate (average of bid/ask)
- bid: Bid rate (bank buys base currency)
- ask: Ask rate (bank sells base currency)
- close: Official closing rate
- Custom: Any user-defined rate name
Currency Codes¶
Standard 3-letter ISO 4217 codes:
- USD - US Dollar
- EUR - Euro
- GBP - British Pound
- JPY - Japanese Yen
- CHF - Swiss Franc
- etc.
Quote vs Base¶
# Converting 100 EUR to USD at rate 1.10
# EUR is "base" (what you have)
# USD is "quote" (what you want)
# Rate: 1 EUR = 1.10 USD
usd_value = 100 * 1.10 # 110 USD
Quick Start¶
In-Memory FX Rates (Testing)¶
from rustybt.data.fx import InMemoryFXRateReader
import pandas as pd
import numpy as np
# Create rate data
dates = pd.date_range('2024-01-01', '2024-01-31', freq='D')
currencies = ['EUR', 'GBP', 'JPY']
# EUR/USD rates (how many USD per 1 EUR)
eur_usd_rates = np.linspace(1.08, 1.12, len(dates))
gbp_usd_rates = np.linspace(1.25, 1.28, len(dates))
jpy_usd_rates = np.linspace(0.0067, 0.0070, len(dates)) # 1 JPY to USD
# Build data structure
data = {
'mid': { # Rate name
'USD': pd.DataFrame({ # Quote currency
'EUR': eur_usd_rates,
'GBP': gbp_usd_rates,
'JPY': jpy_usd_rates
}, index=dates)
}
}
# Create reader
fx_reader = InMemoryFXRateReader(
data=data,
default_rate='mid'
)
# Get single rate
rate = fx_reader.get_rate_scalar(
rate='mid',
quote='USD',
base='EUR',
dt=pd.Timestamp('2024-01-15')
)
print(f"EUR/USD rate: {rate}")
# Get multiple rates
rates = fx_reader.get_rates(
rate='mid',
quote='USD',
bases=np.array(['EUR', 'GBP']),
dts=pd.DatetimeIndex(['2024-01-15', '2024-01-16'])
)
print(f"Rates shape: {rates.shape}") # (2 dates, 2 currencies)
HDF5 FX Rates (Production)¶
from rustybt.data.fx import HDF5FXRateReader, HDF5FXRateWriter
import pandas as pd
# Write rates to HDF5
writer = HDF5FXRateWriter('/path/to/fx_rates.h5')
# Write EUR/USD rates
writer.write(
rate='mid',
quote='USD',
rates=pd.DataFrame({
'EUR': [1.08, 1.09, 1.10],
'GBP': [1.25, 1.26, 1.27],
'JPY': [0.0067, 0.0068, 0.0069]
}, index=pd.date_range('2024-01-01', periods=3))
)
# Read rates
reader = HDF5FXRateReader('/path/to/fx_rates.h5')
rate = reader.get_rate_scalar(
rate='mid',
quote='USD',
base='EUR',
dt=pd.Timestamp('2024-01-02')
)
FX Rate Readers¶
Base Interface¶
All FX readers implement FXRateReader:
from rustybt.data.fx import FXRateReader
class FXRateReader(ABC):
"""Base interface for FX rate readers."""
def get_rates(self, rate, quote, bases, dts):
"""Get 2D array of rates.
Args:
rate: Rate name ('mid', 'bid', 'ask', etc.)
quote: Currency to convert TO
bases: Array of currencies to convert FROM
dts: Timestamps for rate lookups
Returns:
Array of shape (len(dts), len(bases))
"""
def get_rate_scalar(self, rate, quote, base, dt):
"""Get single rate value."""
def get_rates_columnar(self, rate, quote, bases, dts):
"""Get rates for parallel (base, dt) pairs."""
InMemoryFXRateReader¶
Use for: Testing, small datasets, prototyping
from rustybt.data.fx import InMemoryFXRateReader
reader = InMemoryFXRateReader(
data={
'mid': {
'USD': pd.DataFrame(...), # Indexed by dates, columns=currencies
'EUR': pd.DataFrame(...),
}
},
default_rate='mid'
)
Pros: - Fast lookup - Simple setup - Good for testing
Cons: - Limited by RAM - No persistence
HDF5FXRateReader¶
Use for: Production, large datasets, historical rates
Pros: - Efficient storage - Fast random access - Persistent - Industry standard format
Cons: - Requires HDF5 file - Write overhead
ExplodingFXRateReader¶
Use for: Testing multi-currency code without real rates
from rustybt.data.fx import ExplodingFXRateReader
reader = ExplodingFXRateReader()
# Raises error on any rate lookup
# Useful to ensure single-currency code doesn't accidentally use FX
Usage in Strategies¶
Multi-Currency Portfolio¶
from rustybt.algorithm import TradingAlgorithm
from rustybt.data.fx import HDF5FXRateReader
class GlobalEquityStrategy(TradingAlgorithm):
def initialize(self, context):
# Assets in different currencies
context.us_stock = self.symbol('AAPL') # USD
context.uk_stock = self.symbol('VOD.L') # GBP
context.jp_stock = self.symbol('7203.T') # JPY
# Load FX rates
context.fx_reader = HDF5FXRateReader('fx_rates.h5')
def handle_data(self, context, data):
# Get current prices (in local currencies)
aapl_price_usd = data.current(context.us_stock, 'close')
vod_price_gbp = data.current(context.uk_stock, 'close')
toyota_price_jpy = data.current(context.jp_stock, 'close')
# Convert to USD for comparison
dt = context.self.get_datetime()
gbp_usd_rate = context.fx_reader.get_rate_scalar(
rate='mid',
quote='USD',
base='GBP',
dt=dt
)
jpy_usd_rate = context.fx_reader.get_rate_scalar(
rate='mid',
quote='USD',
base='JPY',
dt=dt
)
vod_price_usd = vod_price_gbp * gbp_usd_rate
toyota_price_usd = toyota_price_jpy * jpy_usd_rate
# Equal-weight allocation in USD terms
order_target_percent(context.us_stock, 0.33)
order_target_percent(context.uk_stock, 0.33)
order_target_percent(context.jp_stock, 0.33)
Currency Conversion Utility¶
def convert_currency(
amount: float,
from_currency: str,
to_currency: str,
dt: pd.Timestamp,
fx_reader: FXRateReader,
rate_name: str = 'mid'
) -> float:
"""Convert amount from one currency to another.
Args:
amount: Amount in from_currency
from_currency: Source currency code
to_currency: Target currency code
dt: Date for exchange rate lookup
fx_reader: FX rate reader
rate_name: Rate to use ('mid', 'bid', 'ask')
Returns:
Amount in to_currency
"""
if from_currency == to_currency:
return amount
rate = fx_reader.get_rate_scalar(
rate=rate_name,
quote=to_currency,
base=from_currency,
dt=dt
)
return amount * rate
# Usage
eur_amount = 100.0
usd_amount = convert_currency(
amount=eur_amount,
from_currency='EUR',
to_currency='USD',
dt=pd.Timestamp('2024-01-15'),
fx_reader=fx_reader
)
Data Sources¶
Historical FX Rates¶
Free Sources: - European Central Bank (ECB) - Daily rates - Federal Reserve - Daily rates - OANDA - Historical rates (limited)
Paid Sources: - Bloomberg - Real-time and historical - Refinitiv - Professional FX data - AlphaVantage - API access
Fetching ECB Rates¶
import pandas as pd
import requests
def fetch_ecb_rates(start_date, end_date):
"""Fetch EUR exchange rates from ECB."""
url = "https://data-api.ecb.europa.eu/service/data/EXR/..."
# Implementation details...
response = requests.get(url)
# Parse XML response
# Return DataFrame
pass
Best Practices¶
1. Consistent Rate Names¶
# Good: Use standard rate names
RATE_NAMES = {
'mid': 'Mid-market rate',
'bid': 'Bid rate',
'ask': 'Ask rate',
'close': 'Official close'
}
# Bad: Inconsistent naming
# 'midpoint', 'mid_rate', 'middle', etc.
2. Handle Missing Rates¶
def safe_get_rate(fx_reader, rate, quote, base, dt):
"""Get rate with fallback to 1.0 for same currency."""
if quote == base:
return 1.0
try:
rate_value = fx_reader.get_rate_scalar(rate, quote, base, dt)
if np.isnan(rate_value):
raise ValueError(f"No rate for {base}/{quote} on {dt}")
return rate_value
except Exception as e:
logger.error(f"FX rate lookup failed: {e}")
raise
3. Cache Frequently Used Rates¶
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_fx_rate(rate_name, quote, base, dt_str):
"""Cached FX rate lookup."""
dt = pd.Timestamp(dt_str)
return fx_reader.get_rate_scalar(rate_name, quote, base, dt)
4. Validate Rate Consistency¶
def validate_fx_triangle(fx_reader, currency_a, currency_b, currency_c, dt):
"""Validate triangular arbitrage consistency."""
# A->B->C should equal A->C
ab = fx_reader.get_rate_scalar('mid', currency_b, currency_a, dt)
bc = fx_reader.get_rate_scalar('mid', currency_c, currency_b, dt)
ac = fx_reader.get_rate_scalar('mid', currency_c, currency_a, dt)
indirect = ab * bc
tolerance = 0.001 # 0.1% tolerance
if abs(indirect - ac) / ac > tolerance:
raise ValueError(f"FX triangle inconsistency: {indirect} vs {ac}")
Performance Considerations¶
HDF5 Storage Efficiency¶
# Efficient: Use HDF5 for large rate datasets
writer = HDF5FXRateWriter('fx_rates.h5')
# Write all rates for one quote currency at once
for quote in ['USD', 'EUR', 'GBP']:
writer.write(
rate='mid',
quote=quote,
rates=large_dataframe # Many dates and currencies
)
# Fast random access during backtest
rate = reader.get_rate_scalar('mid', 'USD', 'EUR', dt)
Batch Lookups¶
# Efficient: Batch lookup
rates = fx_reader.get_rates(
rate='mid',
quote='USD',
bases=np.array(['EUR', 'GBP', 'JPY']),
dts=pd.date_range('2024-01-01', '2024-01-31')
)
# Inefficient: Loop of scalar lookups
for dt in dates:
for base in currencies:
rate = fx_reader.get_rate_scalar('mid', 'USD', base, dt)
Troubleshooting¶
Issue: Missing Rate Data¶
# Check available rates
with pd.HDFStore('fx_rates.h5', mode='r') as store:
print(store.keys()) # List all stored rates
# Verify date coverage
rates_df = reader.get_rates('mid', 'USD', ['EUR'], date_range)
print(f"Coverage: {(~np.isnan(rates_df)).sum()} / {len(rates_df)}")
Issue: Inverted Rates¶
# If you have USD/EUR but need EUR/USD
usd_eur = fx_reader.get_rate_scalar('mid', 'EUR', 'USD', dt)
eur_usd = 1.0 / usd_eur # Invert
Issue: Cross Rates¶
def get_cross_rate(fx_reader, quote, base, via, dt):
"""Calculate cross rate via intermediate currency."""
# Get EUR/USD via GBP: EUR->GBP->USD
eur_gbp = fx_reader.get_rate_scalar('mid', via, base, dt)
gbp_usd = fx_reader.get_rate_scalar('mid', quote, via, dt)
return eur_gbp * gbp_usd
# EUR/USD via GBP
eur_usd_cross = get_cross_rate(fx_reader, 'USD', 'EUR', 'GBP', dt)
API Reference¶
See detailed guides: - FX Rate Providers - Data sources and fetching - FX Converters - Currency conversion utilities - FX Storage - In-memory vs HDF5 storage comparison
See Also¶
- Data Portal - Data access integration
- Multi-Asset Strategies (Coming soon) - Multi-currency examples
- Performance (Coming soon) - FX lookup optimization