The finance data-table in Yahoo APIs has recently stopped working, returning error "No definition found for Table yahoo.finance.xchange"
. I need currency exchange rates for my money tracking app ,
so I had to look for another provider.
After googling around I found CurrencyLayer - a service which provides
exchange rates for 168 currencies and even have a free plan. This plan is
limited with 1,000 requests per month.
I’ve used webtask.io before in other projects and
I got an idea to write a microservice which would cache exchange rate from CurrencyLayer.
I would only need to cache USD-based rates and calculate non-USD based rates
using cross-rate.
Webtasks provide a single JSON document storage
which would be perfect for our use-case.
If we cache the exchange rate for 1 hour, it will use max. 744 requests per month (24 hours x 31 days),
which should fit into the free plan 😉
Design
The microservice will have a very simple interface - an HTTP GET endpoint with a single pairs
parameter.
This parameter will containt a list of currency exchange pairs separated by comma.
Sample URL request would look like this:
curl -XGET https://endpoint-url/?pairs= USDEUR,EURUSD,USDJPY
and will respond with a list of exchange rates for given pairs:
{
"ok" : "true" ,
"rates" : [
{
"id" : "USDEUR" ,
"rate" : "0.834303"
},
{
"id" : "EURUSD" ,
"rate" : "1.198605"
},
{
"id" : "USDJPY" ,
"rate" : "109.678001"
}
]
}
Implementation
Implementing this is pretty straight forward.
A webtask is just a JavaScript function with two arguments.
First, context
- gives us access to URL query parameters,
webtask secrets (to keep our API keys separated from code)
and storage API
to cache data.
Second, respond
- is a callback function which we should call when we’re finished.
It takes an error
and result
arguments and converts them to JSON.
Here’s the code of this webtask.
module . exports = function ( context , respond ) {
const request = require ( 'request' )
const BASE = 'USD'
const pairs = context . query . pairs . toUpperCase (). split ( ',' )
return getBaseRate ()
. then ( baseRate =>
respond ( null , {
ok : true ,
rates : pairs . map ( pair => getRateForPair ( baseRate , pair ))
})
)
. catch ( error => respond ( error ))
/**
* Get exchange rate for given pair using given base exchange rate.
* Use cross rate if pair's base is not equal to rate base (USD).
*
* @param {object} baseRate - dict { USD: 1, EUR: 0.834499, ... }
* @param {string} pair - "USDEUR", "EURUSD", "EURJPY", etc
* @return {object} dict { id: "USDEUR", rate: "0.834499" }
*/
function getRateForPair ( baseRate , pair ) {
if ( pair . length != 6 ) {
throw new Error (
`Invalid pair " ${ pair } ". Must be 6-char string, e.g. "USDEUR"`
)
}
const source = pair . substr ( 0 , 3 )
const target = pair . substr ( 3 , 3 )
if ( ! baseRate [ source ]) throw new Error ( `Unknown currency code " ${ source } "` )
if ( ! baseRate [ target ]) throw new Error ( `Unknown currency code " ${ target } "` )
return {
id : pair ,
rate : Number (
source === BASE
? baseRate [ target ]
: 1 / baseRate [ source ] * baseRate [ target ]
). toFixed ( 6 )
}
}
/**
* Get exchange rate for base currency (USD).
*
* @return {object} dict { USD: 1, EUR: 0.834499, ... }
*/
function getBaseRate () {
return fetchCachedRate ()
. then ( rate => rate , error => fetchLiveRate ())
. then ( checkCachedRateAge )
. then ( convertRate )
}
/**
* Read cached rate from webtask storage.
*
* @see https://webtask.io/docs/storage
* @return {Promise}
*/
function fetchCachedRate () {
return new Promise (( resolve , reject ) => {
context . storage . get (( error , rate ) => {
if ( error ) return reject ( error )
if ( rate === undefined ) return reject ()
resolve ( rate )
})
})
}
/**
* Fetch base exchange rate from CurrencyLayer live API.
* Fallback to cached rate if API is not available.
*
* @see https://currencylayer.com/documentation
* @see https://webtask.io/docs/editor/secrets
* @return {Promise}
*/
function fetchLiveRate () {
return new Promise ( resolve => {
const apiKey = context . secrets . apiKey
request (
{
method : 'GET' ,
uri : `http://apilayer.net/api/live?access_key= ${ apiKey } ` ,
json : true
},
( error , response , body ) => {
if ( error || ! body . success ) {
fetchCachedRate (). then ( rate => resolve ( rate ))
} else {
writeCachedRate ( body ). then (() => resolve ( body ))
}
}
)
})
}
/**
* Write given rate to webtask cache.
*
* @see https://webtask.io/docs/storage
* @param {object} rate
* @return {Promise}
*/
function writeCachedRate ( rate ) {
return new Promise (( resolve , reject ) => {
context . storage . set (
rate ,
{ force : 1 },
error => ( error ? reject ( error ) : resolve ())
)
})
}
/**
* Validate cached rate expiry date.
*
* @param {object} rate
*/
function checkCachedRateAge ( rate ) {
const expiryDate = Math . floor ( Date . now () / 1000 ) - 3600
return rate . timestamp < expiryDate ? fetchLiveRate () : rate
}
/**
* Convert response from API service to internal rate object.
*
* @param {object} dict { ..., quotes: { USDUSD: 1, USDEUR: 0.834499, ... } }
* @return {object} dict { USD: 1, EUR: 0.834499, ... }
*/
function convertRate ( rate ) {
return Object . keys ( rate . quotes ). reduce (( acc , pair ) => {
const code = pair . substr ( 3 , 3 )
acc [ code ] = rate . quotes [ pair ]
return acc
}, {})
}
}
You can check my webtask running here .
If you plan to use this approach in your own projects -
I suggest getting your own CurrencyLayer account and running
your own webtask.