Source code for energyunits.exchange_rate
"""Exchange rate utilities for currency conversions with year-dependent rates."""
import json
import warnings
from pathlib import Path
from typing import Optional
[docs]
class ExchangeRateRegistry:
"""Registry for historical exchange rate data.
Provides year-dependent exchange rates for currency conversions.
All rates are expressed as: 1 unit of currency = X USD.
"""
def __init__(self):
self._exchange_rate_data = {}
self._load_defaults()
def _load_defaults(self):
"""Load default exchange rate data from JSON."""
data_path = Path(__file__).parent / "data" / "exchange_rates.json"
with open(data_path) as f:
data = json.load(f)
# Convert string years to integers, skip metadata fields
self._exchange_rate_data = {
currency: {int(year): rate for year, rate in years.items()}
for currency, years in data.items()
if not currency.startswith("_")
}
# USD is always 1.0 for any year (base currency)
self._exchange_rate_data["USD"] = {}
[docs]
def load_exchange_rates(self, file_path: str):
"""Load custom exchange rate data from JSON file."""
with open(file_path) as f:
data = json.load(f)
for currency, year_data in data.items():
# Skip metadata fields
if currency.startswith("_"):
continue
if currency not in self._exchange_rate_data:
self._exchange_rate_data[currency] = {}
# Convert string years to integers
self._exchange_rate_data[currency].update(
{int(year): rate for year, rate in year_data.items()}
)
[docs]
def get_exchange_rate(self, currency: str, year: Optional[int] = None) -> float:
"""Get exchange rate for a currency in a specific year.
Args:
currency: Currency code (EUR, GBP, JPY, CNY)
year: Year for exchange rate. If None, uses most recent available.
Returns:
Exchange rate (1 currency unit = X USD)
Raises:
ValueError: If currency not supported or year not available
"""
if currency == "USD":
return 1.0
if currency not in self._exchange_rate_data:
raise ValueError(
f"Currency '{currency}' not supported for historical exchange rates. "
f"Supported: {self.get_supported_currencies()}"
)
currency_data = self._exchange_rate_data[currency]
if year is None:
# Use most recent year available
year = max(currency_data.keys())
if year not in currency_data:
available_years = sorted(currency_data.keys())
raise ValueError(
f"No exchange rate data for {currency} in year {year}. "
f"Available years: {available_years[0]}-{available_years[-1]}"
)
return currency_data[year]
[docs]
def convert_currency(
self,
value: float,
from_currency: str,
to_currency: str,
year: Optional[int] = None
) -> float:
"""Convert amount from one currency to another using rates from a specific year.
Args:
value: Amount to convert
from_currency: Source currency code
to_currency: Target currency code
year: Year for exchange rate. If None, uses most recent available.
Returns:
Converted amount
"""
if from_currency == to_currency:
return value
# Convert to USD as intermediate
from_rate = self.get_exchange_rate(from_currency, year)
to_rate = self.get_exchange_rate(to_currency, year)
# value * (from_rate USD/from_currency) / (to_rate USD/to_currency)
return value * from_rate / to_rate
[docs]
def get_conversion_factor(
self,
from_currency: str,
to_currency: str,
year: Optional[int] = None
) -> float:
"""Get conversion factor between two currencies for a specific year.
Returns the factor such that: amount_to = amount_from * factor
"""
return self.convert_currency(1.0, from_currency, to_currency, year)
[docs]
def get_supported_currencies(self):
"""Get list of supported currencies."""
currencies = set(self._exchange_rate_data.keys())
currencies.add("USD")
return sorted(currencies)
[docs]
def detect_currency_from_unit(self, unit: str) -> Optional[str]:
"""Detect currency from unit string.
Args:
unit: Unit string (e.g., "USD", "EUR/MWh", "USD/kW")
Returns:
Currency code if detected, None otherwise
"""
# Split compound units into components for exact matching
components = unit.upper().replace("/", " ").split()
for currency in self.get_supported_currencies():
if currency in components:
return currency
# Check for $ symbol
if "$" in unit or "DOLLAR" in unit.upper():
return "USD"
return None
[docs]
def is_currency(self, unit: str) -> bool:
"""Check if a unit is a pure currency (not compound like USD/MWh)."""
return unit in self.get_supported_currencies()
[docs]
def warn_currency_inflation_combination(self):
"""Issue warning about currency conversion + inflation adjustment path dependency."""
warnings.warn(
"Combining currency conversion with inflation adjustment involves economic assumptions. "
"This library uses the convention: INFLATE FIRST (in original currency), THEN CONVERT (using target year exchange rate). "
"Example: 50 EUR (2015) → 2024 USD inflates EUR to 2024, then converts using 2024 EUR/USD rate. "
"This approximates purchasing power parity but may not reflect actual financial returns. "
"For precise financial analysis, use dedicated economic/forex tools.",
UserWarning,
stacklevel=3
)
exchange_rate_registry = ExchangeRateRegistry()