{"id":19671717,"url":"https://github.com/skyscanner/historical-bank-ruby","last_synced_at":"2025-04-29T01:30:38.715Z","repository":{"id":55881899,"uuid":"59844395","full_name":"Skyscanner/historical-bank-ruby","owner":"Skyscanner","description":"A Ruby Bank that serves historical exchange rates","archived":false,"fork":false,"pushed_at":"2022-09-13T13:01:48.000Z","size":88,"stargazers_count":15,"open_issues_count":1,"forks_count":8,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-04-05T12:04:09.646Z","etag":null,"topics":["currency","currency-converter","currency-exchange-rates","gem","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Skyscanner.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-05-27T15:25:25.000Z","updated_at":"2024-04-17T22:04:53.000Z","dependencies_parsed_at":"2022-08-15T08:31:17.948Z","dependency_job_id":null,"html_url":"https://github.com/Skyscanner/historical-bank-ruby","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Skyscanner%2Fhistorical-bank-ruby","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Skyscanner%2Fhistorical-bank-ruby/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Skyscanner%2Fhistorical-bank-ruby/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Skyscanner%2Fhistorical-bank-ruby/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Skyscanner","download_url":"https://codeload.github.com/Skyscanner/historical-bank-ruby/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251415579,"owners_count":21585856,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["currency","currency-converter","currency-exchange-rates","gem","ruby"],"created_at":"2024-11-11T17:09:33.508Z","updated_at":"2025-04-29T01:30:38.431Z","avatar_url":"https://github.com/Skyscanner.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# historical-bank-ruby\n\n[![Gem Version](https://badge.fury.io/rb/historical-bank.svg)](https://badge.fury.io/rb/historical-bank)\n[![Build Status](https://travis-ci.org/Skyscanner/historical-bank-ruby.svg?branch=master)](https://travis-ci.org/Skyscanner/historical-bank-ruby)\n\n## Description\n\nThis gem provides a bank that can serve historical rates,\ncontrary to most bank implementations that provide only current market rates.\n[Open Exchange Rates](https://openexchangerates.org/) (OER) is used as the provider of the rates.\nAs the HTTP requests to OER can add latency to your calls, a `RatesStore` (cache) based on Redis was added, making it super-fast.\n\nYou can use it as your default bank and keep calling the standard `money` gem methods (`Money#exchange_to`, `Bank#exchange_with`). On top of that, we've added a few more methods that allow accessing historical rates (`Money#exchange_to_historical`, `Bank#exchange_with_historical`).\n\n\n### Base currency\n\nAn **exchange rate** has a **base currency** and a **quote (or counter) currency**.\nMore specifically, it is the price of 1 unit of base currency in the quote currency.\nFor example, if base currency is EUR, quote currency is USD, and the rate is 1.25,\nthis means that 1 EUR is equal to 1.25 USD.\n\nAll the rates fetched and cached by this `Bank` are relative to a single base currency which is defined in the configuration block.\nThis helps to optimize fetching and caching rates.\n\nDefault base currency is EUR, but it can be changed in the config.\n\n\n### Dates, times, timezones\n\nThe timezone used throughout this gem is the UTC timezone.\n\nAll processed rates (fetched from OER, added manually, cached in Redis) are considered to be the [closing (end of day) rates](https://openexchangerates.org/faq/#eod-values) for their associated dates in UTC.\nFor example, when we have a cached rate of EUR-\u003eUSD on January 10th 2017 with value 1.25,\nthis means that 1 EUR was equivalent to 1.25 USD on January 10th at 23:59:59.\nThis is the historical rate that the bank will use for exchanging EUR with USD on that date.\nConsequently, a rate for a certain `date` fetched from OER becomes available at 00:00 UTC on `date+1`.\n\nFor convenience, methods that accept `Date`s as arguments can accept `Time`s as well.\nWhen a `Time` is used, it is first converted into the UTC-equivalent `Date`,\nand method is executed as if that `Date` was passed instead.\nFor example, when the `Time` `2017-01-10 02:50:00 +04:00` is passed as argument to `#exchange_with_historical`,\n\n```ruby\nfrom_money = Money.new(100_00, 'EUR')\nto_currency = 'USD'\n\n# 2017-01-10 02:50:00 +0400\nbank.exchange_with_historical(from_money, to_currency, Time.new(2017, 1, 10, 2, 50, 0, '+04:00'))\n# =\u003e #\u003cMoney fractional:10585 currency:USD\u003e\n```\n\nit is equivalent to passing the `Date` `2017-01-09`\n\n```ruby\n# 2017-01-09\nbank.exchange_with_historical(from_money, to_currency, Date.new(2017, 1, 9))\n# =\u003e #\u003cMoney fractional:10585 currency:USD\u003e\n```\n\n\n### Caching\n\nWe've implemented 2 layers of caching in order to obliterate latency!\nFirst layer is memory (instance variable in the bank object), and second is Redis.\nIf desired rate is not found in memory, the bank tries to look it up in Redis.\nIf that fails too, a request to OER is made.\n\nWhen we fetch rates from OER, they are cached in Redis and memory too.\nSimilarly, when the rate is found in Redis, it is again cached in memory.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"images/cache_diagram.png\" alt=\"Caching diagram\"/\u003e\n\u003c/p\u003e\n\nPretty simple and fast!\n\n\n### Singleton\n\nThe bank follows the Singleton pattern, as it inherits from `money` gem's `Money::Bank::Base`.\nThis also helps preserve the memory cache across calls.\nDon't worry, the bank is thread-safe!\n\n\n## Requirements\n\n- **OpenExchangeRates account** - If on the Free or Developer plan, the `/historical/*.json` endpoint is used for fetching rates for single days. If on the Enterprise or Unlimited plan, the `/time-series.json` endpoint is used for fetching rates for whole months, increasing efficiency.\n- **Redis \u003e= 2.8** (versions of Redis earlier than 2.8 may also work fine, but this gem has only been tested with 2.8 and above.)\n- **Ruby \u003e= 2.0**\n\n\n\n## Installation\n\n```\ngem install historical-bank\n```\n\nAlternatively, if you're using `bundler`, you can add\n``` ruby\ngem 'historical-bank'\n```\nto your `Gemfile` and run `bundle install`\n\n\n\n## Usage\n\nExample scripts demonstrating all functionality can be found in [`examples/`](examples/).\n\n### Configuration\n\n```ruby\nMoney::Bank::Historical.configure do |config|\n  # (required) your OpenExchangeRates App ID\n  config.oer_app_id = 'XXXXXXXXXXXXXXX'\n\n  # (optional) account type on openexchangerate.org (default: 'Enterprise')\n  # replace 'FREE' with 'DEVELOPER', 'ENTERPRISE', or 'UNLIMITED', according to your account type.\n  config.oer_account_type = Money::RatesProvider::OpenExchangeRates::AccountType::FREE\n\n  # (optional) currency relative to which all the rates are stored (default: EUR)\n  config.base_currency = Money::Currency.new('USD')\n\n  # (optional) the URL of the Redis server (default: 'redis://localhost:6379')\n  config.redis_url = 'redis://localhost:6379'\n\n  # (optional) Redis namespace to prefix all keys (default: 'currency')\n  config.redis_namespace = 'currency_historical_gem'\n\n  # (optional) set a timeout for the OER calls (default: 15 seconds)\n  config.timeout = 20\nend\n```\n\n#### Rails\n\nIn Rails, config should be set inside an initializer (`config/initializers`).\nIf you have the `money-rails` gem installed, you can add this on top of the `config/initializers/money.rb` file.\n\n```ruby\n# config/initializers/money.rb\n\nrequire 'money/bank/historical'\n\nMoney::Bank::Historical.configure do |config|\n  # ....\nend\n\n\nMoneyRails.configure do |config|\n  # ...\n\n  # if you want to set it as the default bank\n  config.default_bank = Money::Bank::Historical.instance\n\n  # ...\nend\n```\n\n#### Sinatra\n\nIn a Sinatra app (or any other kind of app), simply set the config before you call `Money::Bank::Historical.instance`.\n\n```ruby\n# app.rb\n\nrequire 'money/bank/historical'\n\nMoney::Bank::Historical.configure do |config|\n  # ....\nend\n\nclass App \u003c Sinatra::Base\n\n  configure do\n    # if you want to set it as the default bank\n    Money.default_bank = Money::Bank::Historical.instance\n  end\n\n  # ...\nend\n```\n\n### Dates\n\nThe minimum date for which we can fetch rates is January 1st 1999.\nThis [limitation](https://docs.openexchangerates.org/docs/api-introduction) is set by the OpenExchangeRates API.\nThe maximum date for which we fetch OER rates is yesterday (in UTC),\nas [today's rates are not yet final](https://openexchangerates.org/faq/#timezone).\n\nHowever, if you want to overcome these limitations manually, you can add past (and even future!) rates using `Historical#add_rate` and `#add_rates`.\n\n### Exchange\n\nYou can use the bank object for the exchange\n```ruby\nfrom_money = Money.new(100_00, 'EUR')\nto_currency = 'GBP'\n\nbank = Money::Bank::Historical.instance\n\n# exchange money with rates from December 10th 2016\nbank.exchange_with_historical(from_money, to_currency, Date.new(2016, 12, 10))\n# =\u003e #\u003cMoney fractional:8399 currency:GBP\u003e\n\n# can also pass a Time/DateTime object, it's converted into the respective UTC Date\nbank.exchange_with_historical(from_money, to_currency, Time.utc(2016, 10, 2, 11, 0, 0))\n# =\u003e #\u003cMoney fractional:8691 currency:GBP\u003e\n```\n\nOr perform it directly on the `Money` object\n```ruby\nfrom_money.exchange_to_historical(to_currency, Date.new(2016, 12, 10))\n# =\u003e #\u003cMoney fractional:8399 currency:GBP\u003e\n```\n\n`Bank#exchange_with` and `Money#exchange_to` can still be used. In this case, recent rates are needed, so yesterday's closing rates are used for the calculation\n```ruby\nbank.exchange_with(from_money, to_currency)\n\n# set the default bank and create a new Money object that will use it\nMoney.default_bank = Money::Bank::Historical.instance\nfrom_money = Money.new(100_00, 'EUR')\nfrom_money.exchange_to(to_currency)\n```\n\n### Adding and retrieving rates\n\nAdding rates will not be needed in most cases as the rates are fetched from OER.\nHowever, `#add_rate` and `#get_rate` were implemented in order to conform to the Bank API.\nAn extra `#add_rates` method was implemented for setting rates in bulk.\n\n`#add_rate` and `add_rates` can prove quite when **testing**,\nas you can't afford HTTP requests there.\nOnly thing you need to do is initialize the bank, and add some default rates\nbefore your tests run.\n\n\n#### Get single rate\n\n`#get_rate` accepts both ISO strings and Money::Currency objects\n```ruby\nbank.get_rate(Money::Currency.new('GBP'), 'CAD', Date.new(2016, 10, 1))\n# =\u003e #\u003cBigDecimal:7fd39fd2cb78,'0.1703941289 451827243E1',27(45)\u003e\n```\n\nGetting without a datetime will return yesterday's closing rate, e.g. `bank.get_rate('CAD', 'CNY')`.\n\n#### Add single rate\n\n`#add_rate` adds a single rate to the Redis cache. It accepts both ISO strings and `Money::Currency` objects. Added rates should be relative to the base currency.\n```ruby\ndate = Date.new(2016, 5, 18)\n\nbank.add_rate('EUR', 'USD', 1.2, date)\nbank.add_rate(Money::Currency.new('USD'), Money::Currency.new('GBP'), 0.8, date)\n\n# 100 EUR = 100 * 1.2 USD = 100 * 1.2 * 0.8 GBP = 96 GBP\nbank.exchange_with_historical(from_money, to_currency, date)\n# =\u003e #\u003cMoney fractional:9600 currency:GBP\u003e\n```\n\nAdding without a datetime will set the rate to yesterday's closing rate\n```ruby\nbank.add_rate('EUR', 'USD', 1.4)\nbank.add_rate(Money::Currency.new('USD'), Money::Currency.new('GBP'), 0.6)\n\n# 100 EUR = 100 * 1.4 USD = 100 * 1.4 * 0.6 GBP = 84 GBP\nbank.exchange_with(from_money, to_currency)\n# =\u003e #\u003cMoney fractional:8400 currency:GBP\u003e\n```\n\nTrying to add a rate that is not relative to the base currency will fail.\nThis is because all cached rates are relative to the base currency.\n```ruby\nbank.add_rate('EUR', 'GBP', 0.96, date)\n# ArgumentError: `from_currency` (EUR) or `to_currency` (GBP) should match the base currency USD\n```\n\n#### Add rates in bulk\n\n`#add_rates` can be used to add multiple historical rates (relative to the base currency) to the Redis cache.\n```ruby\nrates = {\n  'EUR' =\u003e {\n    '2015-09-10' =\u003e 0.11, # 1 USD = 0.11 EUR\n    '2015-09-11' =\u003e 0.22\n  },\n  'GBP' =\u003e {\n    '2015-09-10' =\u003e 0.44, # 1 USD = 0.44 GBP\n    '2015-09-11' =\u003e 0.55\n  }\n}\nbank.add_rates(rates)\n\n# 100 EUR = 100 / 0.11 USD = 100 / 0.11 * 0.44 GBP = 400 GBP\nbank.exchange_with_historical(from_money, to_currency, Date.new(2015, 9, 10))\n# =\u003e #\u003cMoney fractional:40000 currency:GBP\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskyscanner%2Fhistorical-bank-ruby","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskyscanner%2Fhistorical-bank-ruby","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskyscanner%2Fhistorical-bank-ruby/lists"}