Scala Forex is a high-performance Scala library for performing exchange rate lookups and currency conversions, using Joda-Money.
It includes configurable LRU (Least Recently Used) caches to minimize calls to the API; this makes the library usable in high-volume environments such as Hadoop and Storm.
Currently Scala Forex uses the Open Exchange Rates API to perform currency lookups.
First sign up to Open Exchange Rates to get your App ID for API access.
There are three types of accounts supported by OER API, Unlimited, Enterprise and Developer levels. See the sign up page for specific account descriptions. For Scala Forex, we recommend an Enterprise or Unlimited account, unless all of your conversions are to or from USD (see section 4.5 OER accounts for an explanation). For 10-minute rate updates, you will need an Unlimited account (other accounts are hourly).
The latest version of Scala Forex is 3.0.0, which is cross-built against 2.12 & 2.13.
If you're using SBT, add the following lines to your build file:
libraryDependencies ++= Seq(
"com.snowplowanalytics" %% "scala-forex" % "3.0.0"
)
Note the double percent (%%
) between the group and artifactId. That'll ensure you get the right package for your Scala version.
The Scala Forex library supports two types of usage:
- Exchange rate lookups
- Currency conversions
Both usage types support live, near-live or historical (end-of-day) exchange rates.
For all code samples below we are assuming the following imports:
import org.joda.money.CurrencyUnit
import com.snowplowanalytics.forex._
Scala Forex is configured via ForexConfig
case class. Except appId
and accountLevel
fields, it provides
some sensible defaults.
case class ForexConfig(
appId: String,
accountLevel: AccountType,
nowishCacheSize: Int = 13530,
nowishSecs: Int = 300,
eodCacheSize: Int = 405900,
getNearestDay: EodRounding = EodRoundDown,
baseCurrency: CurrencyUnit = CurrencyUnit.USD
)
To go through each in turn:
-
appId
is the API key you get from OER. -
accountLevel
is the type of OER account you have. Possible values areUnlimitedAccount
,EnterpriseAccount
, andDeveloperAccount
. -
nowishCacheSize
is the size configuration for near-live (nowish) lookup cache, it can be disabled by setting its value to 0. The key to nowish cache is a currency pair so the size of the cache equals to the number of pairs of currencies available. -
nowishSecs
is the time configuration for near-live lookup. A call to this cache will use the exchange rates stored in nowish cache if its time stamp is less than or equal tonowishSecs
old. -
eodCacheSize
is the size configuration for end-of-day (eod) lookup cache, it can be disabled by setting its value to 0. The key to eod cache is a tuple of currency pair and time stamp, so the size of eod cache equals to the number of currency pairs times the days which the cache will remember the data for. -
getNearestDay
is the rounding configuration for latest eod (at) lookup. The lookup will be performed on the next day if the rounding mode is set to EodRoundUp, and on the previous day if EodRoundDown. -
baseCurrency
can be configured to different currencies by the users.
For an explanation for the default values please see section 5.4 Explanation of defaults below.
Unless specified otherwise, assume forex
value is initialized as:
val config: ForexConfig = ForexConfig("YOUR_API_KEY", DeveloperAccount)
def fForex[F[_]: Sync]: F[Forex] = CreateForex[F].create(config)
CreateForex[F].create
returns F[Forex]
instead of Forex
, because creation of the underlying
caches is a side effect. You can flatMap
over the result (or use a for-comprehension, as seen
below). All examples below return F[Either[OerResponseError, Money]]
, which means they are not
executed.
If you don't care about side effects, we also provide instance of Forex
for cats.Eval
and
cats.Id
:
val evalForex: Eval[Forex] = CreateForex[Eval].create(config)
val idForex: Forex = CreateForex[Id].create(config)
For distributed applications, such as Spark or Beam apps, where lazy values might be an issue, you may want to use the Id
instance.
Look up a live rate (no caching available):
// USD => JPY
val usd2jpyF: F[Either[OerResponseError, Money]] = for {
fx <- fForex
result <- fx.rate.to(CurrencyUnit.JPY).now
} yield result
// using Eval
val usd2jpyE: Eval[Either[OerResponseError, Money]] = for {
fx <- evalForex
result <- fx.rate.to(CurrencyUnit.JPY).now
} yield result
// using Id
val usd2jpyI: Either[OerResponseError, Money] =
idForex.rate.to(CurrencyUnit.JPY).now
Look up a near-live rate (caching available):
// JPY => GBP
val jpy2gbp = for {
fx <- fForex
result <- fx.rate(CurrencyUnit.JPY).to(CurrencyUnit.GBP).nowish
} yield result
Look up a near-live rate (uses cache selectively):
// JPY => GBP
val jpy2gbp = for {
fx <- CreateForex[IO].create(ForexConfig("YOU_API_KEY", DeveloperAccount, nowishCacheSize = 0))
result <- fx.rate(CurrencyUnit.JPY).to(CurrencyUnit.GBP).nowish
} yield result
Look up the latest EOD (end-of-date) rate prior to your event (caching available):
import java.time.{ZonedDateTime, ZoneId}
// USD => JPY at the end of 13/03/2011
val tradeDate = ZonedDateTime.of(2011, 3, 13, 11, 39, 27, 567, ZoneId.of("America/New_York"))
val usd2jpy = for {
fx <- fForex
result <- fx.rate.to(CurrencyUnit.JPY).at(tradeDate)
} yield result
Look up the latest EOD (end-of-date) rate post to your event (caching available):
// USD => JPY at the end of 13/03/2011
val tradeDate = ZonedDateTime.of(2011, 3, 13, 11, 39, 27, 567, ZoneId.of("America/New_York"))
val usd2jpy = for {
fx <- Forex.getClient[IO](ForexConfig("YOU_API_KEY", DeveloperAccount, getNearestDay = EodRoundUp))
result <- fx.rate.to(CurrencyUnit.JPY).at(tradeDate)
} yield result
Look up the EOD rate for a specific date (caching available):
// GBP => JPY at the end of 13/03/2011
val tradeDate = ZonedDateTime.of(2011, 3, 13, 0, 0, 0, 0, ZoneId.of("America/New_York"))
val gbp2jpy = for {
fx <- Forex.getClient[IO](ForexConfig("YOU_API_KEY", EnterpriseAccount, baseCurrency= CurrencyUnit.GBP))
result <- fx.rate.to(CurrencyUnit.JPY).eod(eodDate)
} yield result
Look up the EOD rate for a specific date (no caching):
// GBP => JPY at the end of 13/03/2011
val tradeDate = ZonedDateTime.of(2011, 3, 13, 0, 0, 0, 0, ZoneId.of("America/New_York"))
val gbp2jpy = for {
fx <- Forex.getClient[IO](ForexConfig("YOU_API_KEY", EnterPriseAccount,
baseCurrency = CurrencyUnit.GBP, eodCacheSize = 0))
result <- fx.rate.to(CurrencyUnit.JPY).eod(eodDate)
} yield result
Conversion using the live exchange rate (no caching available):
// 9.99 USD => EUR
val priceInEuros = for {
fx <- forex
result <- fx.convert(9.99).to(CurrencyUnit.EUR).now
} yield result
Conversion using a near-live exchange rate with 500 seconds nowishSecs
(caching available):
// 9.99 GBP => EUR
val priceInEuros = for {
fx <- CreateForex[IO].create(ForexConfig("YOU_API_KEY", DeveloperAccount, nowishSecs = 500))
result <- fx.convert(9.99, CurrencyUnit.GBP).to(CurrencyUnit.EUR).nowish
} yield result
Note that this will be a live rate conversion if cache is not available.
Conversion using a live exchange rate with 500 seconds nowishSecs
,
this conversion will be done via HTTP request:
// 9.99 GBP => EUR
val priceInEuros = for {
fx <- CreateForex[IO].create(ForexConfig("YOUR_API_KEY", DeveloperAccount, nowishSecs = 500, nowishCacheSize = 0))
result <- fx.convert(9.99, CurrencyUnit.GBP).to(CurrencyUnit.EUR).nowish
} yield result
Conversion using the latest EOD (end-of-date) rate prior to your event (caching available):
// 10000 GBP => JPY at the end of 12/03/2011
val tradeDate = ZonedDateTime.of(2011, 3, 13, 11, 39, 27, 567, ZoneId.of("America/New_York"))
val tradeInYen = for {
fx <- forex
result <- fx.convert(10000, CurrencyUnit.GBP).to(CurrencyUnit.JPY).at(tradeDate)
} yield result
Lookup the latest EOD (end-of-date) rate following your event (caching available):
// 10000 GBP => JPY at the end of 13/03/2011
val tradeDate = ZonedDateTime.of(2011, 3, 13, 11, 39, 27, 567, ZoneId.of("America/New_York"))
val usd2jpy = for {
fx <- CreateForex[IO].create(ForexConfig("Your API key / app id", DeveloperAccount, getNearestDay = EodRoundUp))
result <- fx.convert(10000, CurrencyUnit.GBP).to(CurrencyUnit.JPY).at(tradeDate)
} yield result
Conversion using the EOD rate for a specific date (caching available):
// 10000 GBP => JPY at the end of 13/03/2011
val eodDate = ZonedDateTime.of(2011, 3, 13, 11, 39, 27, 567, ZoneId.of("America/New_York"))
val tradeInYen = for {
fx <- Forex.getClient[IO](ForexConfig("YOU_API_KEY", DeveloperAccount, baseCurrency = CurrencyUnit.GBP))
result <- fx.convert(10000).to(CurrencyUnit.JPY).eod(eodDate)
} yield result
Conversion using the EOD rate for a specific date, (no caching):
// 10000 GBP => JPY at the end of 13/03/2011
val eodDate = ZonedDateTime.of(2011, 3, 13, 0, 0, 0, 0, ZoneId.of("America/New_York"))
val tradeInYen = for {
fx <- Forex.getClient[IO](ForexConfig("YOU_API_KEY", DeveloperAccount,
baseCurrency = CurrencyUnit.GBP, eodCacheSize = 0))
result <- fx.convert(10000).to(CurrencyUnit.JPY).eod(eodDate)
} yield result
The eodCacheSize
and nowishCacheSize
values determine the maximum number of values to keep in the LRU cache,
which the Client will check prior to making an API lookup. To disable either LRU cache, set its size to zero,
i.e. eodCacheSize = 0
.
A default "from currency" can be specified for all operations, using the baseCurrency
argument to the ForexConfig
object.
If not specified, baseCurrency
is set to USD by default.
You must export your OER_KEY
or else the tests will be skipped. To run the test suite locally:
$ export OER_KEY=<<key>>
$ git clone https://github.com/snowplow/scala-forex.git
$ cd scala-forex
$ sbt test
The end of today is 00:00 on the next day.
When .now
is specified, the live exchange rate available from Open Exchange Rates is used.
When .nowish
is specified, a cached version of the live exchange rate is used, if the timestamp of that exchange rate is less than or equal to nowishSecs
(see above) ago. Otherwise a new lookup is performed.
When .at(...)
is specified, the latest end-of-day rate prior to the datetime is used by default. Or users can configure that Scala Forex "round up" to the end of the occurring day.
When .eod(...)
is specified, the end-of-day rate for the specified day is used. Any hour/minute/second/etc portion of the datetime is ignored.
We recommend trying different LRU cache sizes to see what works best for you.
Please note that the LRU cache implementation is not thread-safe. Switch it off if you are working with threads.
There are 165 currencies provided by the OER API, hence 165 * 164 pairs of currency combinations. The key in nowish cache is a tuple of source currency and target currency, and the nowish cache was implemented in a way such that a lookup from CurrencyA to CurrencyB or from CurrencyB to CurrencyA will use the same exchange rate, so we don't need to store both in the caches. Hence there are (165 * 164 / 2) pairs of currencies for nowish cache.
Assume the eod cache stores the rates to each pair of currencies for 1 month(i.e. 30 days). There are 165 * 164 / 2 pairs of currencies, hence (165 * 164 / 2) * 30 entries.
Assume nowish cache stores data for 5 mins.
By convention, we are always interested in the exchange rates prior to the query date, hence EodRoundDown.
We selected USD for the base currency because this is the OER default as well.
With Open Exchange Rates' Unlimited and Enterprise accounts, Scala Forex can specify the base currency to use when looking up exchange rates; Developer-level accounts will always retrieve rates against USD, so a rate lookup from e.g. GBY to EUR will require two conversions (GBY -> USD -> EUR). For this reason, we recommend Unlimited and Enterprise-level accounts for slightly more accurate non-USD-related lookups.
The Scaladoc pages for this library can be found here.
Scala Forex is copyright 2013-2022 Snowplow Analytics Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.