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:
-
appIdis the API key you get from OER. -
accountLevelis the type of OER account you have. Possible values areUnlimitedAccount,EnterpriseAccount, andDeveloperAccount. -
nowishCacheSizeis 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. -
nowishSecsis 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 tonowishSecsold. -
eodCacheSizeis 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. -
getNearestDayis 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. -
baseCurrencycan 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).nowLook up a near-live rate (caching available):
// JPY => GBP
val jpy2gbp = for {
fx <- fForex
result <- fx.rate(CurrencyUnit.JPY).to(CurrencyUnit.GBP).nowish
} yield resultLook 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 resultLook 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 resultLook 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 resultLook 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 resultLook 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 resultConversion 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 resultConversion 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 resultNote 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 resultConversion 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 resultLookup 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 resultConversion 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 resultConversion 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 resultThe 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.