historical-bank 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 598c943f0debd7880311ebe7be452417b8482e7d
4
+ data.tar.gz: 85c1004279edde93a2a2e56b4c1fef411d98f273
5
+ SHA512:
6
+ metadata.gz: 0ce3fb2034576449270f62515454bfe48654c669f0696702358fe4fc5f0bb1c77e960c304c32d03d38267a8d3b8c3ed065691c2c11bb3b93b84ad1e1a265c7cf
7
+ data.tar.gz: 0b312998d80ceb7e1f9a0e464484aa11b2c96b0977185cbc9993b9d542a47a57390158c139888adc1b8eb38ea02df0183c2ebe4fcdc14bfdaabc4ef64ad70816
data/AUTHORS ADDED
@@ -0,0 +1,2 @@
1
+ Kostis Dadamis
2
+ Emili Parreno
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+ - Added basic functionality and documentation.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,84 @@
1
+ # Contributing to historical-bank-ruby
2
+
3
+ We're glad you want to make a contribution!
4
+
5
+ Fork this repository and send in a pull request when you're finished with your changes. Link any relevant issues in too.
6
+
7
+ Take note of the build status of your pull request, only builds that pass will be accepted. Please also keep to our conventions and style so we can keep this repository as clean as possible.
8
+
9
+
10
+ ## Steps
11
+
12
+ 1. Fork the repo
13
+ 2. Grab dependencies: `bundle install`
14
+ 3. Make sure specs are green: `./spec/redis-server.sh` to run the Redis server and `bundle exec rspec` to run the specs. Alternatively, you can run your own Redis server in `localhost`, and add the port as an env var before running the specs, `REDIS_PORT=6379 bundle exec rspec`
15
+ 4. Make your changes
16
+ 5. Run `rubocop -a`
17
+ 6. Make sure specs are still green
18
+ 7. Update `CHANGELOG.md` and `AUTHORS`
19
+ 8. Issue a Pull Request and link any relevant issues
20
+
21
+
22
+ ## Tips
23
+
24
+ Please make sure you read [README.md](README.md) before you start hacking so that you understand how the gem works :)
25
+
26
+ ### Redis usage
27
+
28
+ The Redis cache that we use stores Hashes with keys formatted as `[namespace]:[base_currency]:[quote_currency]`.
29
+ These hashes contain ISO dates as keys with base currency rates as values.
30
+ For example, if `currency` is the Redis namespace we used in the config,
31
+ USD is the base currency, and we're looking for HUF rates, they can be found in the `currency:USD:HUF` key, and their format will be
32
+ ```
33
+ {
34
+ "2016-05-01": "0.272511002E3", # Rates are stored as BigDecimal Strings
35
+ "2016-05-02": "0.270337998E3",
36
+ "2016-05-03": "0.271477498E3"
37
+ }
38
+ ```
39
+
40
+ The first key of this Hash translates to: "on May 1st 2016, 1 USD was equivalent to 272.511002 HUF".
41
+
42
+
43
+ ### Memory caching
44
+
45
+ Apart from Redis, rates are also cached in the Bank's memory,
46
+ in `@rates`. `@rates` is a `Hash[iso_currency][iso_date]`
47
+ containing `BigDecimal` exchange rates of `iso_currency` on `iso_date`.
48
+ When they don't exist in memory, they are retrieved from the Store,
49
+ and when they don't exist in the Store, they are retrieved from the
50
+ Provider.
51
+
52
+
53
+ ## Possible improvements
54
+
55
+ - Make Redis optional
56
+ - Add another provider (e.g. XE.com)
57
+ - Add another store (e.g. DynamoDB, MongoDB, Cassandra, Postgres, etc)
58
+
59
+
60
+ ## License
61
+
62
+ By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/Skyscanner/historical-bank-ruby/blob/master/LICENSE
63
+
64
+ All files are released with the Apache 2.0 license.
65
+
66
+ If you are adding a new file it should have a header like this:
67
+
68
+ ```
69
+ #
70
+ # Copyright 2017 Skyscanner Limited.
71
+ #
72
+ # Licensed under the Apache License, Version 2.0 (the "License");
73
+ # you may not use this file except in compliance with the License.
74
+ # You may obtain a copy of the License at
75
+ #
76
+ # http://www.apache.org/licenses/LICENSE-2.0
77
+ #
78
+ # Unless required by applicable law or agreed to in writing, software
79
+ # distributed under the License is distributed on an "AS IS" BASIS,
80
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
81
+ # See the License for the specific language governing permissions and
82
+ # limitations under the License.
83
+ #
84
+ ```
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ #
2
+ # Copyright 2017 Skyscanner Limited.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ # frozen_string_literal: true
18
+
19
+ source 'https://rubygems.org'
20
+
21
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2017 Skyscanner Ltd
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ 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.
data/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # historical-bank-ruby
2
+
3
+ [![Build Status](https://travis-ci.org/Skyscanner/historical-bank-ruby.svg?branch=master)](https://travis-ci.org/Skyscanner/historical-bank-ruby)
4
+
5
+ ## Description
6
+
7
+ This gem provides a bank that can serve historical rates,
8
+ contrary to most bank implementations that provide only current market rates.
9
+ [Open Exchange Rates](https://openexchangerates.org/) (OER) is used as the provider of the rates and an Enterprise or Unlimited plan is required.
10
+ As the HTTP requests to OER can add latency to your calls, a `RatesStore` (cache) based on Redis was added, making it super-fast.
11
+
12
+ You 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`).
13
+
14
+
15
+ ### Base currency
16
+
17
+ An **exchange rate** has a **base currency** and a **quote (or counter) currency**.
18
+ More specifically, it is the price of 1 unit of base currency in the quote currency.
19
+ For example, if base currency is EUR, quote currency is USD, and the rate is 1.25,
20
+ this means that 1 EUR is equal to 1.25 USD.
21
+
22
+ All the rates fetched and cached by this `Bank` are relative to a single base currency which is defined in the configuration block.
23
+ This helps to optimize fetching and caching rates.
24
+
25
+ Default base currency is EUR, but it can be changed in the config.
26
+
27
+
28
+ ### Dates, times, timezones
29
+
30
+ The timezone used throughout this gem is the UTC timezone.
31
+
32
+ All 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.
33
+ For example, when we have a cached rate of EUR->USD on January 10th 2017 with value 1.25,
34
+ this means that 1 EUR was equivalent to 1.25 USD on January 10th at 23:59:59.
35
+ This is the historical rate that the bank will use for exchanging EUR with USD on that date.
36
+ Consequently, a rate for a certain `date` fetched from OER becomes available at 00:00 UTC on `date+1`.
37
+
38
+ For convenience, methods that accept `Date`s as arguments can accept `Time`s as well.
39
+ When a `Time` is used, it is first converted into the UTC-equivalent `Date`,
40
+ and method is executed as if that `Date` was passed instead.
41
+ For example, when the `Time` `2017-01-10 02:50:00 +04:00` is passed as argument to `#exchange_with_historical`,
42
+
43
+ ```ruby
44
+ from_money = Money.new(100_00, 'EUR')
45
+ to_currency = 'USD'
46
+
47
+ # 2017-01-10 02:50:00 +0400
48
+ bank.exchange_with_historical(from_money, to_currency, Time.new(2017, 1, 10, 2, 50, 0, '+04:00'))
49
+ # => #<Money fractional:10585 currency:USD>
50
+ ```
51
+
52
+ it is equivalent to passing the `Date` `2017-01-09`
53
+
54
+ ```ruby
55
+ # 2017-01-09
56
+ bank.exchange_with_historical(from_money, to_currency, Date.new(2017, 1, 9))
57
+ # => #<Money fractional:10585 currency:USD>
58
+ ```
59
+
60
+
61
+ ### Caching
62
+
63
+ We've implemented 2 layers of caching in order to obliterate latency!
64
+ First layer is memory (instance variable in the bank object), and second is Redis.
65
+ If desired rate is not found in memory, the bank tries to look it up in Redis.
66
+ If that fails too, a request to OER is made.
67
+
68
+ When we fetch rates from OER, they are cached in Redis and memory too.
69
+ Similarly, when the rate is found in Redis, it is again cached in memory.
70
+
71
+ <p align="center">
72
+ <img src="images/cache_diagram.png" alt="Caching diagram"/>
73
+ </p>
74
+
75
+ Pretty simple and fast!
76
+
77
+
78
+ ### Singleton
79
+
80
+ The bank follows the Singleton pattern, as it inherits from `money` gem's `Money::Bank::Base`.
81
+ This also helps preserve the memory cache across calls.
82
+ Don't worry, the bank is thread-safe!
83
+
84
+
85
+ ## Requirements
86
+
87
+ - **OpenExchangeRates Enterprise or Unlimited plan** - It is needed for calling the `/time-series.json` endpoint which serves historical rates.
88
+ - **Redis >= 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.)
89
+ - **Ruby >= 2.0**
90
+
91
+
92
+
93
+ ## Installation
94
+
95
+ ```
96
+ gem install historical-bank
97
+ ```
98
+
99
+ Alternatively, if you're using `bundler`, you can add
100
+ ``` ruby
101
+ gem 'historical-bank'
102
+ ```
103
+ to your `Gemfile` and run `bundle install`
104
+
105
+
106
+
107
+ ## Usage
108
+
109
+ Example scripts demonstrating all functionality can be found in [`examples/`](examples/).
110
+
111
+ ### Configuration
112
+
113
+ ```ruby
114
+ Money::Bank::Historical.configure do |config|
115
+ # (required) your OpenExchangeRates App ID
116
+ config.oer_app_id = 'XXXXXXXXXXXXXXX'
117
+
118
+ # (optional) currency relative to which all the rates are stored (default: EUR)
119
+ config.base_currency = Money::Currency.new('USD')
120
+
121
+ # (optional) the URL of the Redis server (default: 'redis://localhost:6379')
122
+ config.redis_url = 'redis://localhost:6379'
123
+
124
+ # (optional) Redis namespace to prefix all keys (default: 'currency')
125
+ config.redis_namespace = 'currency_historical_gem'
126
+
127
+ # (optional) set a timeout for the OER calls (default: 15 seconds)
128
+ config.timeout = 20
129
+ end
130
+ ```
131
+
132
+ #### Rails
133
+
134
+ In Rails, config should be set inside an initializer (`config/initializers`).
135
+ If you have the `money-rails` gem installed, you can add this on top of the `config/initializers/money.rb` file.
136
+
137
+ ```ruby
138
+ # config/initializers/money.rb
139
+
140
+ require 'money/bank/historical'
141
+
142
+ Money::Bank::Historical.configure do |config|
143
+ # ....
144
+ end
145
+
146
+
147
+ MoneyRails.configure do |config|
148
+ # ...
149
+
150
+ # if you want to set it as the default bank
151
+ config.default_bank = Money::Bank::Historical.instance
152
+
153
+ # ...
154
+ end
155
+ ```
156
+
157
+ #### Sinatra
158
+
159
+ In a Sinatra app (or any other kind of app), simply set the config before you call `Money::Bank::Historical.instance`.
160
+
161
+ ```ruby
162
+ # app.rb
163
+
164
+ require 'money/bank/historical'
165
+
166
+ Money::Bank::Historical.configure do |config|
167
+ # ....
168
+ end
169
+
170
+ class App < Sinatra::Base
171
+
172
+ configure do
173
+ # if you want to set it as the default bank
174
+ Money.default_bank = Money::Bank::Historical.instance
175
+ end
176
+
177
+ # ...
178
+ end
179
+ ```
180
+
181
+ ### Dates
182
+
183
+ The minimum date for which we can fetch rates is January 1st 1999.
184
+ This [limitation](https://docs.openexchangerates.org/docs/api-introduction) is set by the OpenExchangeRates API.
185
+ The maximum date for which we fetch OER rates is yesterday (in UTC),
186
+ as [today's rates are not yet final](https://openexchangerates.org/faq/#timezone).
187
+
188
+ However, if you want to overcome these limitations manually, you can add past (and even future!) rates using `Historical#add_rate` and `#add_rates`.
189
+
190
+ ### Exchange
191
+
192
+ You can use the bank object for the exchange
193
+ ```ruby
194
+ from_money = Money.new(100_00, 'EUR')
195
+ to_currency = 'GBP'
196
+
197
+ bank = Money::Bank::Historical.instance
198
+
199
+ # exchange money with rates from December 10th 2016
200
+ bank.exchange_with_historical(from_money, to_currency, Date.new(2016, 12, 10))
201
+ # => #<Money fractional:8399 currency:GBP>
202
+
203
+ # can also pass a Time/DateTime object, it's converted into the respective UTC Date
204
+ bank.exchange_with_historical(from_money, to_currency, Time.utc(2016, 10, 2, 11, 0, 0))
205
+ # => #<Money fractional:8691 currency:GBP>
206
+ ```
207
+
208
+ Or perform it directly on the `Money` object
209
+ ```ruby
210
+ from_money.exchange_to_historical(to_currency, Date.new(2016, 12, 10))
211
+ # => #<Money fractional:8399 currency:GBP>
212
+ ```
213
+
214
+ `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
215
+ ```ruby
216
+ bank.exchange_with(from_money, to_currency)
217
+
218
+ # set the default bank and create a new Money object that will use it
219
+ Money.default_bank = Money::Bank::Historical.instance
220
+ from_money = Money.new(100_00, 'EUR')
221
+ from_money.exchange_to(to_currency)
222
+ ```
223
+
224
+ ### Adding and retrieving rates
225
+
226
+ Adding rates will not be needed in most cases as the rates are fetched from OER.
227
+ However, `#add_rate` and `#get_rate` were implemented in order to conform to the Bank API.
228
+ An extra `#add_rates` method was implemented for setting rates in bulk.
229
+
230
+ `#add_rate` and `add_rates` can prove quite when **testing**,
231
+ as you can't afford HTTP requests there.
232
+ Only thing you need to do is initialize the bank, and add some default rates
233
+ before your tests run.
234
+
235
+
236
+ #### Get single rate
237
+
238
+ `#get_rate` accepts both ISO strings and Money::Currency objects
239
+ ```ruby
240
+ bank.get_rate(Money::Currency.new('GBP'), 'CAD', Date.new(2016, 10, 1))
241
+ # => #<BigDecimal:7fd39fd2cb78,'0.1703941289 451827243E1',27(45)>
242
+ ```
243
+
244
+ Getting without a datetime will return yesterday's closing rate, e.g. `bank.get_rate('CAD', 'CNY')`.
245
+
246
+ #### Add single rate
247
+
248
+ `#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.
249
+ ```ruby
250
+ date = Date.new(2016, 5, 18)
251
+
252
+ bank.add_rate('EUR', 'USD', 1.2, date)
253
+ bank.add_rate(Money::Currency.new('USD'), Money::Currency.new('GBP'), 0.8, date)
254
+
255
+ # 100 EUR = 100 * 1.2 USD = 100 * 1.2 * 0.8 GBP = 96 GBP
256
+ bank.exchange_with_historical(from_money, to_currency, date)
257
+ # => #<Money fractional:9600 currency:GBP>
258
+ ```
259
+
260
+ Adding without a datetime will set the rate to yesterday's closing rate
261
+ ```ruby
262
+ bank.add_rate('EUR', 'USD', 1.4)
263
+ bank.add_rate(Money::Currency.new('USD'), Money::Currency.new('GBP'), 0.6)
264
+
265
+ # 100 EUR = 100 * 1.4 USD = 100 * 1.4 * 0.6 GBP = 84 GBP
266
+ bank.exchange_with(from_money, to_currency)
267
+ # => #<Money fractional:8400 currency:GBP>
268
+ ```
269
+
270
+ Trying to add a rate that is not relative to the base currency will fail.
271
+ This is because all cached rates are relative to the base currency.
272
+ ```ruby
273
+ bank.add_rate('EUR', 'GBP', 0.96, date)
274
+ # ArgumentError: `from_currency` (EUR) or `to_currency` (GBP) should match the base currency USD
275
+ ```
276
+
277
+ #### Add rates in bulk
278
+
279
+ `#add_rates` can be used to add multiple historical rates (relative to the base currency) to the Redis cache.
280
+ ```ruby
281
+ rates = {
282
+ 'EUR' => {
283
+ '2015-09-10' => 0.11, # 1 USD = 0.11 EUR
284
+ '2015-09-11' => 0.22
285
+ },
286
+ 'GBP' => {
287
+ '2015-09-10' => 0.44, # 1 USD = 0.44 GBP
288
+ '2015-09-11' => 0.55
289
+ }
290
+ }
291
+ bank.add_rates(rates)
292
+
293
+ # 100 EUR = 100 / 0.11 USD = 100 / 0.11 * 0.44 GBP = 400 GBP
294
+ bank.exchange_with_historical(from_money, to_currency, Date.new(2015, 9, 10))
295
+ # => #<Money fractional:40000 currency:GBP>
296
+ ```
@@ -0,0 +1,110 @@
1
+ #
2
+ # Copyright 2017 Skyscanner Limited.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ # frozen_string_literal: true
18
+
19
+ require 'money/bank/historical'
20
+
21
+ redis_url = 'redis://localhost:6379'
22
+ namespace = 'currency_example'
23
+
24
+ Money::Bank::Historical.configure do |config|
25
+ # (required) your OpenExchangeRates App ID
26
+ config.oer_app_id = 'XXXXXXXXXXXXXXXXXXXXX'
27
+
28
+ # (optional) currency relative to which all the rates are stored (default: EUR)
29
+ config.base_currency = Money::Currency.new('USD')
30
+
31
+ # (optional) the URL of the Redis server (default: 'redis://localhost:6379')
32
+ config.redis_url = redis_url
33
+
34
+ # (optional) Redis namespace to prefix all keys (default: 'currency')
35
+ config.redis_namespace = namespace
36
+
37
+ # (optional) set a timeout for the OER calls (default: 15 seconds)
38
+ config.timeout = 20
39
+ end
40
+
41
+ bank = Money::Bank::Historical.instance
42
+
43
+ from_money = Money.new(100_00, 'EUR')
44
+ to_currency = 'GBP'
45
+
46
+ ############ Get single rate #################
47
+
48
+ # It accepts both ISO strings and Money::Currency objects.
49
+ bank.get_rate(Money::Currency.new('GBP'), 'CAD', Date.new(2016, 10, 1))
50
+ # => #<BigDecimal:7fd39fd2cb78,'0.1703941289 451827243E1',27(45)>
51
+
52
+ # Getting without a datetime will return yesterday's closing rate
53
+ bank.get_rate('CAD', 'CNY')
54
+
55
+ ############ Add single rate #################
56
+
57
+ # It accepts both ISO strings and Money::Currency objects.
58
+ # Added rates should be relative to the base currency
59
+ date = Date.new(2016, 5, 18)
60
+
61
+ bank.add_rate('EUR', 'USD', 1.2, date)
62
+ bank.add_rate(Money::Currency.new('USD'), Money::Currency.new('GBP'), 0.8, date)
63
+
64
+ # 100 EUR = 100 * 1.2 USD = 100 * 1.2 * 0.8 GBP = 96 GBP
65
+ bank.exchange_with_historical(from_money, to_currency, date)
66
+ # => #<Money fractional:9600 currency:GBP>
67
+
68
+ # Adding without a datetime will set the rate to yesterday's closing rate
69
+ bank.add_rate('EUR', 'USD', 1.4)
70
+ bank.add_rate(Money::Currency.new('USD'), Money::Currency.new('GBP'), 0.6)
71
+
72
+ # 100 EUR = 100 * 1.4 USD = 100 * 1.4 * 0.6 GBP = 84 GBP
73
+ bank.exchange_with(from_money, to_currency)
74
+ # => #<Money fractional:8400 currency:GBP>
75
+
76
+ # trying to add a rate that is not relative to the base currency will fail
77
+ bank.add_rate('EUR', 'GBP', 0.96, date)
78
+ # ArgumentError: `from_currency` (EUR) or `to_currency` (GBP) should match the base currency USD
79
+
80
+ ############ Add rates in bulk #################
81
+
82
+ # add historical exchange rates (relative to the base currency) in bulk
83
+ rates = {
84
+ 'EUR' => {
85
+ '2015-09-10' => 0.11, # 1 USD = 0.11 EUR
86
+ '2015-09-11' => 0.22,
87
+ '2015-09-12' => 0.33
88
+ },
89
+ 'GBP' => {
90
+ '2015-09-10' => 0.44, # 1 USD = 0.44 GBP
91
+ '2015-09-11' => 0.55,
92
+ '2015-09-12' => 0.66
93
+ },
94
+ 'VND' => {
95
+ '2015-09-10' => 0.77, # 1 USD = 0.77 VND
96
+ '2015-09-11' => 0.88,
97
+ '2015-09-12' => 0.99
98
+ }
99
+ }
100
+ bank.add_rates(rates)
101
+
102
+ # 100 EUR = 100 / 0.11 USD = 100 / 0.11 * 0.44 GBP = 400 GBP
103
+ bank.exchange_with_historical(from_money, to_currency, Date.new(2015, 9, 10))
104
+ # => #<Money fractional:40000 currency:GBP>
105
+
106
+ ########## Clean up Redis keys used here #########
107
+
108
+ redis = Redis.new(url: redis_url)
109
+ keys = redis.keys("#{namespace}*")
110
+ redis.del(keys)