Convert with Oanda FX Rates
What is this endpoint for?
This add-on endpoint ensures IFRS-compliant currency conversions for over 70 fiat pairs when a synthetic price is used in Fair Market Value Pricing. While synthetic prices can be generated in any fiat currency, this endpoint guarantees compliance for non-USD currencies. In low-liquidity cases, users request a synthetic price quoted in USD, then convert it into their desired fiat currency using this endpoint.
HTTP Request
https://us.market-api.kaiko.io/v2/data/analytics.{data_version}/oanda_fx_rates
Parameters
quote
Yes
The quote fiat currency. Automatically included in continuation tokens.
base
Yes
The base fiat currency. Automatically included in continuation tokens.
continuation_token
No
end_time
No
Ending time in ISO 8601 (exclusive). Automatically included in continuation tokens.
interval
No
The interval parameter is suffixed with m
, h
or d
to specify seconds, minutes, hours or days, respectively.
Any arbitrary value between one minute and one day can be used, as long as it sums up to a maximum of 1 day. The suffixes are m
(minute), h
(hour) and d
(day).
Default 1h
.
For each interval, the resulting FX rate is an average of all available FX rates over that period.
Automatically included in continuation tokens.
page_size
No
Default: 100
Automatically included in continuation tokens.
start_time
No
Starting time in ISO 8601 (inclusive). Automatically included in continuation tokens.
sort
No
Return the data in ascending (asc
) or descending (desc
) order. Default is desc
.
Automatically included in continuation tokens.
Fields
timestamp
Timestamp at which the interval ends.
fx_rate
Average fx rate over the interval.
Request examples
curl --compressed -H 'Accept: application/json' -H 'X-Api-Key: <client-api-key>' \
'https://us.market-api.kaiko.io/v2/data/analytics.v2/oanda_fx_rates?base=eur&page_size=2&sort=desc&interval=1d"e=jpy'
This example first requests a digital asset price in USD, requests an equivalent price from Oanda and then uses the conversion from Oanda to calculate the fiat price, representing the real-life application of this product. Note, where values are unavailable from Kaiko Fair Market Value or Oanda, the most recent suitable value is used.
import http.client
import json
import bisect
from datetime import datetime, timezone
# Enter your Kaiko API Key
api_key = "KAIKO_API_KEY"
api_host = "us.market-api.kaiko.io"
### USER INPUT ###
user_base_asset = "crv"
user_quote_asset = "jpy"
common_interval = "1d"
common_start_time = "2025-01-01T00:00:00.000Z"
common_end_time = "2025-01-31T00:00:00.000Z"
### Intermediary currency - Always USD ###
intermediate_currency = "usd"
### FMV API Endpoint ###
fmv_base_endpoint = f"/v2/data/trades.v2/spot_exchange_rate/{intermediate_currency}/{user_base_asset}"
### OANDA API Endpoint ###
oanda_base_endpoint = "/v2/data/analytics.v2/oanda_fx_rates"
def convert_to_iso(timestamp_ms):
"""Convert epoch milliseconds to ISO 8601 format for Oanda API"""
# Use format with milliseconds as required by the API
dt = datetime.utcfromtimestamp(timestamp_ms / 1000)
# Format with milliseconds (.000Z)
return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")
def fetch_data(endpoint, query_params):
"""Fetch paginated data from Kaiko API"""
conn = http.client.HTTPSConnection(api_host)
headers = {
'Accept': 'application/json',
'X-Api-Key': api_key
}
params = query_params.copy()
param_str = "&".join([f"{key}={value}" for key, value in params.items()])
url = f"{endpoint}?{param_str}"
all_data = []
continuation_token = None
while True:
if continuation_token:
final_url = f"{endpoint}?continuation_token={continuation_token}"
print(f"Fetching (paginated): {final_url}")
else:
final_url = url
print(f"Fetching: {final_url}")
conn.request("GET", final_url, headers=headers)
response = conn.getresponse()
raw_data = response.read().decode()
try:
data = json.loads(raw_data)
# Print a shorter version of the response to keep logs cleaner
print(f"Response status: {data.get('result', 'unknown')}")
if "data" in data:
print(f"Received {len(data['data'])} data points")
except json.JSONDecodeError:
print("ERROR: Failed to decode JSON response")
print(f"Raw response: {raw_data}")
return None
if "data" in data and len(data["data"]) > 0:
all_data.extend(data["data"])
else:
print("ERROR: No data found in response")
break
continuation_token = data.get("continuation_token")
if not continuation_token:
break
conn.close()
return all_data
# STEP 1: Fetch BASE ā USD from FMV
print(f"Fetching FMV data for {user_base_asset.upper()} ā USD...")
fmv_data = fetch_data(
fmv_base_endpoint,
{
"interval": common_interval,
"sort": "desc",
"start_time": common_start_time,
"end_time": common_end_time,
"extrapolate_missing_values": "true"
},
)
if not fmv_data or all(item.get("price") is None for item in fmv_data):
print(f"ā ERROR: FMV API returned no valid prices for {user_base_asset.upper()} ā USD. Possible low liquidity or missing data.")
exit()
fmv_dict = {item["timestamp"]: float(item["price"]) for item in fmv_data if item.get("price") is not None}
if not fmv_dict:
print(f"ā ERROR: No valid FMV timestamps found.")
exit()
print(f"ā
Successfully retrieved {len(fmv_dict)} FMV data points")
# STEP 2: Fetch USD ā QUOTE from Oanda for each FMV timestamp with a wider window
oanda_data = {}
print(f"Fetching OANDA data for USD ā {user_quote_asset.upper()} for each FMV timestamp...")
# We'll use a 5-day window for each request to ensure we get data
window_ms = 5 * 24 * 60 * 60 * 1000 # 5 days in milliseconds
# Group FMV timestamps by day to reduce number of API calls
fmv_timestamps_by_day = {}
for ts in fmv_dict.keys():
# Convert to date (just the day part)
day = ts // (24 * 60 * 60 * 1000) * (24 * 60 * 60 * 1000)
if day not in fmv_timestamps_by_day:
fmv_timestamps_by_day[day] = []
fmv_timestamps_by_day[day].append(ts)
# Make OANDA requests for each day's worth of FMV timestamps
for day, day_timestamps in sorted(fmv_timestamps_by_day.items()):
# Set window to cover the entire day
start_ts = day
end_ts = day + (24 * 60 * 60 * 1000)
# Convert to ISO format
start_time_iso = convert_to_iso(start_ts)
end_time_iso = convert_to_iso(end_ts)
print(f"Requesting OANDA data for day {convert_to_iso(day)} ({len(day_timestamps)} timestamps)")
# Make Oanda request for this day
oanda_response = fetch_data(
oanda_base_endpoint,
{
"base": intermediate_currency,
"quote": user_quote_asset,
"interval": common_interval,
"sort": "desc",
"start_time": start_time_iso,
"end_time": end_time_iso
}
)
if oanda_response and len(oanda_response) > 0:
for item in oanda_response:
if "timestamp" in item and "fx_rate" in item and item["fx_rate"] is not None:
oanda_data[item["timestamp"]] = float(item["fx_rate"])
print(f" ā
Got {len(oanda_response)} OANDA data points for this day")
else:
print(f" ā ļø No OANDA data found for day {convert_to_iso(day)}, trying wider window...")
wide_start_ts = day - (window_ms // 2)
wide_end_ts = day + (window_ms // 2) + (24 * 60 * 60 * 1000)
wide_start_time_iso = convert_to_iso(wide_start_ts)
wide_end_time_iso = convert_to_iso(wide_end_ts)
oanda_response = fetch_data(
oanda_base_endpoint,
{
"base": intermediate_currency,
"quote": user_quote_asset,
"interval": common_interval,
"sort": "desc",
"start_time": wide_start_time_iso,
"end_time": wide_end_time_iso
}
)
if oanda_response and len(oanda_response) > 0:
for item in oanda_response:
if "timestamp" in item and "fx_rate" in item and item["fx_rate"] is not None:
oanda_data[item["timestamp"]] = float(item["fx_rate"])
print(f" ā
Got {len(oanda_response)} OANDA data points from wider window")
else:
print(f" ā No OANDA data found even with wider window")
print(f"ā
Successfully retrieved {len(oanda_data)} OANDA data points total")
# Ensure at least some valid Oanda data was retrieved
if not oanda_data:
print(f"ā ERROR: Oanda API returned no valid prices for {user_quote_asset.upper()}.")
exit()
print(f"ā
Successfully retrieved {len(oanda_data)} OANDA data points")
#Convert FMV Prices Using Matched Oanda Rates
converted_prices = []
oanda_timestamps = sorted(oanda_data.keys()) # Sort timestamps
for timestamp in sorted(fmv_dict.keys(), reverse=True): # ā
Ensure descending order
# Find closest Oanda timestamp to current FMV timestamp
pos = bisect.bisect_right(oanda_timestamps, timestamp) - 1
# Find closest Oanda timestamp
if pos < 0:
nearest_oanda_timestamp = oanda_timestamps[0] # Default to earliest Oanda timestamp
elif pos >= len(oanda_timestamps):
nearest_oanda_timestamp = oanda_timestamps[-1] # Default to latest Oanda timestamp
else:
nearest_oanda_timestamp = oanda_timestamps[pos] # Select closest available timestamp
# Convert FMV price using matched Oanda rate
base_usd_price = fmv_dict[timestamp]
usd_quote_price = oanda_data[nearest_oanda_timestamp]
# Calculate final price - QNT/USD * USD/EUR = QNT/EUR
# The FMV endpoint returned USD/QNT, so we need to invert that first
qnt_usd_price = 1 / base_usd_price # This gives QNT/USD
final_price = qnt_usd_price * usd_quote_price # QNT/USD * USD/EUR = QNT/EUR
converted_prices.append((timestamp, nearest_oanda_timestamp, qnt_usd_price, usd_quote_price, final_price))
# ā
Print results
if converted_prices:
print("\nā
Conversion Successful!")
print(f"{user_base_asset.upper()} to {user_quote_asset.upper()} Prices Over Time:")
print("---------------------------------------------------------------------------------------------------------")
print("FMV Timestamp | OANDA Timestamp | Base to USD | USD to Quote | Final Price")
print("---------------------------------------------------------------------------------------------------------")
for fmv_time, oanda_time, base_usd, usd_quote, final in converted_prices:
fmv_time_str = convert_to_iso(fmv_time)
oanda_time_str = convert_to_iso(oanda_time)
print(f"{fmv_time_str} | {oanda_time_str} | {base_usd:.6f} | {usd_quote:.6f} | {final:.6f}")
print("---------------------------------------------------------------------------------------------------------")
print("\nā
Done.")
else:
print("ā ERROR: Unable to retrieve valid pricing data.")
import http.client
import json
# Enter your Kaiko API Key
api_key = "KAIKO_API_KEY"
api_host = "us.market-api.kaiko.io"
api_base_endpoint = "/v2/data/analytics.v2/oanda_fx_rates"
# Start of mandatory parameter configuration
mandatory_params = {
"base": "usd",
"quote": "jpy",
"start_time" : "2024-09-27T13:13:53.441Z",
"end_time" : "2024-09-27T13:27:53.441Z",
}
# End of mandatory parameter configuration
# Start of optional parameter configuration
optional_params = {
"interval": "1m",
"sort": "desc"
}
# End of optional parameter configuration
conn = http.client.HTTPSConnection(api_host)
headers = {
"X-Api-Key": api_key,
"Accept": "application/json"
}
all_params = {**mandatory_params, **optional_params}
url_params = []
for param, value in all_params.items():
url_params.append(f"{param}={value}")
url_params = '&'.join(url_params)
# Initial request
endpoint_with_params = f"{api_base_endpoint}?{url_params}"
# Pagination for next pages
all_data = []
next_url = endpoint_with_params
while next_url:
conn.request("GET", next_url, headers=headers)
response = conn.getresponse()
data = json.loads(response.read().decode("utf-8"))
all_data.extend(data.get("data", []))
print(f"Fetched {len(data.get('data', []))} datapoints. Total: {len(all_data)}")
next_url = data.get("next_url", "").replace("https://us.market-api.kaiko.io", "")
if not next_url:
break
conn.close()
print(f" datapoints fetched: {(all_data)}")
Response examples
-------------------------
FMV Timestamp | OANDA Timestamp | Base to USD | USD to Quote | Final Price
---------------------------------------------------------------------------------------------------------
2025-01-30T00:00:00.000Z | 2025-01-30T00:00:00.000Z | 0.768336 | 154.391542 | 118.624604
2025-01-29T00:00:00.000Z | 2025-01-29T00:00:00.000Z | 0.700815 | 155.310636 | 108.844066
2025-01-28T00:00:00.000Z | 2025-01-28T00:00:00.000Z | 0.717522 | 155.537318 | 111.601388
2025-01-27T00:00:00.000Z | 2025-01-27T00:00:00.000Z | 0.716854 | 154.891123 | 111.034396
2025-01-26T00:00:00.000Z | 2025-01-26T00:00:00.000Z | 0.810300 | 155.717333 | 126.177788
2025-01-25T00:00:00.000Z | 2025-01-24T00:00:00.000Z | 0.810947 | 155.802879 | 126.347909
2025-01-24T00:00:00.000Z | 2025-01-24T00:00:00.000Z | 0.819695 | 155.802879 | 127.710833
2025-01-23T00:00:00.000Z | 2025-01-23T00:00:00.000Z | 0.769308 | 156.301177 | 120.243731
2025-01-22T00:00:00.000Z | 2025-01-22T00:00:00.000Z | 0.819786 | 156.350285 | 128.173796
2025-01-21T00:00:00.000Z | 2025-01-21T00:00:00.000Z | 0.847833 | 155.526267 | 131.860349
2025-01-20T00:00:00.000Z | 2025-01-20T00:00:00.000Z | 0.870170 | 155.932273 | 135.687508
2025-01-19T00:00:00.000Z | 2025-01-19T00:00:00.000Z | 0.907083 | 156.360415 | 141.831932
2025-01-18T00:00:00.000Z | 2025-01-17T00:00:00.000Z | 0.931774 | 155.754251 | 145.127692
2025-01-17T00:00:00.000Z | 2025-01-17T00:00:00.000Z | 0.977871 | 155.754251 | 152.307544
2025-01-16T00:00:00.000Z | 2025-01-16T00:00:00.000Z | 0.956103 | 155.708593 | 148.873509
2025-01-15T00:00:00.000Z | 2025-01-15T00:00:00.000Z | 0.880997 | 156.506112 | 137.881475
2025-01-14T00:00:00.000Z | 2025-01-14T00:00:00.000Z | 0.829693 | 157.792641 | 130.919486
2025-01-13T00:00:00.000Z | 2025-01-13T00:00:00.000Z | 0.784495 | 157.453368 | 123.521310
2025-01-12T00:00:00.000Z | 2025-01-12T00:00:00.000Z | 0.834389 | 157.784911 | 131.654059
2025-01-11T00:00:00.000Z | 2025-01-10T00:00:00.000Z | 0.813998 | 158.048879 | 128.651394
2025-01-10T00:00:00.000Z | 2025-01-10T00:00:00.000Z | 0.830917 | 158.048879 | 131.325484
2025-01-09T00:00:00.000Z | 2025-01-09T00:00:00.000Z | 0.829882 | 158.025537 | 131.142551
2025-01-08T00:00:00.000Z | 2025-01-08T00:00:00.000Z | 0.858147 | 158.372457 | 135.906785
2025-01-07T00:00:00.000Z | 2025-01-07T00:00:00.000Z | 0.972995 | 157.882541 | 153.618955
2025-01-06T00:00:00.000Z | 2025-01-06T00:00:00.000Z | 1.005259 | 157.538276 | 158.366806
2025-01-05T00:00:00.000Z | 2025-01-05T00:00:00.000Z | 1.034215 | 157.369308 | 162.753724
2025-01-04T00:00:00.000Z | 2025-01-03T00:00:00.000Z | 1.062482 | 157.252971 | 167.078507
2025-01-03T00:00:00.000Z | 2025-01-03T00:00:00.000Z | 1.016461 | 157.252971 | 159.841579
2025-01-02T00:00:00.000Z | 2025-01-02T00:00:00.000Z | 1.013144 | 157.229690 | 159.296318
2025-01-01T00:00:00.000Z | 2025-01-01T00:00:00.000Z | 0.909161 | 157.335604 | 143.043384
-------------------------------------------
{
"query": {
"base": "eur",
"quote": "jpy",
"interval": "1d",
"page_size": "2",
"sort": "desc",
"start_time": "null",
"end_time": "2022-08-31T08:38:25.883Z"
},
"time": "2022-08-31T08:38:25.905Z",
"timestamp": 1661935105,
"data": [
{
"timestamp": 1660780800000,
"fx_rate": "137.38630485436903"
},
{
"timestamp": 1660694400000,
"fx_rate": "137.15155641205303"
}
],
"continuation_token": "4tvMKJPYA6ESWsE7s87P2ujFvr6XRNvegzst2eg1EpdyQEPKWpuNic5XPGrhz47RzbbqC598E3XusLo34Hivgw4sYrrvdmYxL7WQVtebjtYVMUPPd97vqo2VjL22A6cTSNojTQsvHh8T6MPRjuJAMfx5LWyVZQWYyzLrSE",
"next_url": "https://us.market-api.kaiko.io/v2/data/analytics.v2/oanda_fx_rates?continuation_token=4tvMKJPYA6ESWsE7s87P2ujFvr6XRNvegzst2eg1EpdyQEPKWpuNic5XPGrhz47RzbbqC598E3XusLo34Hivgw4sYrrvdmYxL7WQVtebjtYVMUPPd97vqo2VjL22A6cTSNojTQsvHh8T6MPRjuJAMfx5LWyVZQWYyzLrSE"
}
Last updated
Was this helpful?