Skip to content

Currency handling

Currency handling on oneapi.finance is straightforward except in three places where it is not. This recipe covers all three: sub-unit pricing, portfolio aggregation across currencies, and historical rate lookups for backtesting.

Sub-units (GBX, ILA, ZAC)

Three exchanges quote their main listings in 1/100 of the headline currency:

  • LSE quotes in GBX (pence) for most issues.
  • TASE quotes in ILA (agorot) for most issues.
  • JSE quotes in ZAC (cents) for most issues.

When you fetch a quote for one of these listings, currency is the sub-unit:

{
"quote": {
"symbol": "BARC.L",
"currency": "GBX",
"price": 152.30
}
}

That is 152.30 pence, not £152.30. To convert to the parent currency:

Sub-unitParentConversion
GBXGBPdivide by 100
ILAILSdivide by 100
ZACZARdivide by 100
SUB_UNIT_TO_PARENT = {"GBX": ("GBP", 100), "ILA": ("ILS", 100), "ZAC": ("ZAR", 100)}
def normalize_currency(amount: float, currency: str) -> tuple[float, str]:
if currency in SUB_UNIT_TO_PARENT:
parent, divisor = SUB_UNIT_TO_PARENT[currency]
return amount / divisor, parent
return amount, currency

Portfolio totals across currencies

The end-to-end pattern for “total portfolio value in EUR”:

TARGET = "EUR"
def value_in_target(price: float, currency: str, fx_rates: dict[str, float]) -> float:
# Step 1: normalize sub-units to parent.
amount, ccy = normalize_currency(price, currency)
# Step 2: convert parent currency to target.
if ccy == TARGET:
return amount
pair = f"{ccy}/{TARGET}"
if pair not in fx_rates:
raise KeyError(f"Missing FX rate for {pair}")
return amount * fx_rates[pair]
def portfolio_value(holdings: list[dict], target: str = TARGET) -> float:
quotes = fetch_quotes_batched([h["symbol"] for h in holdings])
needed_currencies = {
normalize_currency(0, q["currency"])[1]
for q in quotes.values()
if q.get("currency")
}
needed_pairs = {f"{c}/{target}" for c in needed_currencies if c != target}
rates = fetch_fx(needed_pairs)
flat = {p: rates["rates"][p]["current"] for p in needed_pairs}
total = 0.0
for h in holdings:
q = quotes.get(h["symbol"])
if q and q.get("price") is not None:
total += value_in_target(q["price"] * h["quantity"], q["currency"], flat)
return total

The two-step normalization (sub-unit → parent → target) avoids lookup misses when no GBX/EUR pair is requested but GBP/EUR is.

Historical rate lookups for backtesting

For a backtest that sums positions over time in a target currency, you need the historical FX rate on each evaluation date, not the current one. /v1/fx/time_series?pair=USD/EUR&start_date=... returns daily closes:

from datetime import date
def historical_rate_index(pair: str, start: date, end: date) -> dict[str, float]:
r = httpx.get(
"https://api.oneapi.finance/v1/fx/time_series",
params={
"pair": pair,
"start_date": start.isoformat(),
"end_date": end.isoformat(),
"outputsize": 5000,
},
headers=HEADERS,
timeout=30.0,
)
r.raise_for_status()
bars = r.json()["rates"][pair]["history"] or []
return {bar["datetime"][:10]: bar["close"] for bar in bars}

Edge cases to watch:

  • Weekends and holidays. FX trades 24×5 globally, but our daily bars are closes from a reference fixing time. Weekend dates have no entry; carry forward the most recent weekday close.
  • Exchange-day vs FX-day mismatch. A LSE listing’s 2026-12-31 close was printed in London time. Use the same calendar day’s USD/GBP close; don’t intersect with US-market calendars.

Cryptocurrency handling

Crypto quotes are in BASE-QUOTE form (BTC-USD, ETH-EUR). currency is the quote currency. Crypto trades 24/7 and has no sub-units. Volume is denominated in base units (BTC, ETH).

Reference: ISO currency code list we use

We use the standard ISO 4217 list plus the three sub-unit additions noted above. The full list is enumerated in conventions.

See also