oanda_bank 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 225b610a4b164f13d07652c05d10a287d3ed9118
4
+ data.tar.gz: 850de2fddc116b1ad31284d4d9614d75bfd3e26e
5
+ SHA512:
6
+ metadata.gz: 5a73a3a3a34199069de1862d4e71302ebac9915831c8ae4b2f4bd979e895a627311803d2b0907eecbe86dba6ac4fc30158ccc57fc5e15ef18681efe74df9e3c0
7
+ data.tar.gz: d03f427678b4b0300be316db24a22c1c77de04c3910bc6f708ab4a407c686d0a532cebf952b593b41bec0217b09abd719cb39859f3153760caf53e6d34bdd0d0
data/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /lib/bundler/man/
26
+
27
+ # for a library or gem, you might want to ignore these files since the code is
28
+ # intended to run in multiple environments; otherwise, check them in:
29
+ # Gemfile.lock
30
+ # .ruby-version
31
+ # .ruby-gemset
32
+
33
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
34
+ .rvmrc
35
+
36
+ .env
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,63 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ oanda_bank (1.0.0)
5
+ money (~> 6.0)
6
+ oauth2 (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.3.6)
12
+ crack (0.4.2)
13
+ safe_yaml (~> 1.0.0)
14
+ diff-lcs (1.2.5)
15
+ docile (1.1.5)
16
+ faraday (0.9.0)
17
+ multipart-post (>= 1.2, < 3)
18
+ i18n (0.6.11)
19
+ jwt (1.0.0)
20
+ money (6.1.1)
21
+ i18n (~> 0.6.4)
22
+ multi_json (1.10.1)
23
+ multi_xml (0.5.5)
24
+ multipart-post (2.0.0)
25
+ oauth2 (1.0.0)
26
+ faraday (>= 0.8, < 0.10)
27
+ jwt (~> 1.0)
28
+ multi_json (~> 1.3)
29
+ multi_xml (~> 0.5)
30
+ rack (~> 1.2)
31
+ rack (1.5.2)
32
+ rspec (3.0.0)
33
+ rspec-core (~> 3.0.0)
34
+ rspec-expectations (~> 3.0.0)
35
+ rspec-mocks (~> 3.0.0)
36
+ rspec-core (3.0.3)
37
+ rspec-support (~> 3.0.0)
38
+ rspec-expectations (3.0.3)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.0.0)
41
+ rspec-mocks (3.0.3)
42
+ rspec-support (~> 3.0.0)
43
+ rspec-support (3.0.3)
44
+ safe_yaml (1.0.3)
45
+ simplecov (0.9.0)
46
+ docile (~> 1.1.0)
47
+ multi_json
48
+ simplecov-html (~> 0.8.0)
49
+ simplecov-html (0.8.0)
50
+ vcr (2.9.2)
51
+ webmock (1.18.0)
52
+ addressable (>= 2.3.6)
53
+ crack (>= 0.3.2)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ oanda_bank!
60
+ rspec (~> 3.0)
61
+ simplecov (~> 0.7)
62
+ vcr (~> 2.9)
63
+ webmock (~> 1.18)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Alex Ianus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ oanda_bank
2
+ ==========
3
+
4
+ Ruby Money::Bank interface for OANDA currency rate data
5
+
6
+ This gem extends Money::Bank::VariableExchange with Money::Bank::OANDA to give you access to OANDA fxTrade exchange rates.
7
+
8
+ ## What rates are available?
9
+
10
+ Any pair of currencies that are both part of the money gem and part of a tradeable instrument in your fxTrade account. In addition, if there is no tradeable instrument containing both of your currencies the gem will attempt to go through USD. For example, CAD -> SEK is supported because although there is no CAD/SEK instrument, there exist both USD/CAD and USD/SEK instruments.
11
+
12
+ ## Usage
13
+
14
+ ``` ruby
15
+ require 'oanda_bank'
16
+
17
+ oanda_bank = Money::Bank::OANDA(ENV['FXTRADE_ACCOUNT_ID'], ENV['FXTRADE_ACCESS_TOKEN'])
18
+
19
+ # Call this before calculating exchange rates
20
+ # This will download the rates from OANDA
21
+ oanda_bank.update_rates!
22
+
23
+ # Exchange 100 USD to CAD
24
+ # API is the same as the money gem
25
+ oanda_bank.exchange_with(Money.new(10000, :USD), :CAD)
26
+
27
+ # Set as default bank to do arithmetic and comparisons on Money objects
28
+ Money.default_bank = oanda_bank
29
+ money1 = Money.new(10)
30
+ money1.bank # oanda_bank
31
+
32
+ Money.us_dollar(10000).exchange_to(:CAD)
33
+ '1'.to_money(:USD) > '1'.to_money(:CAD) # true
34
+
35
+ # Refresh rates after some number of seconds (by default, rates are only updated when you call update_rates!)
36
+ oanda_bank.ttl_in_seconds = 3600 # Cache rates for one hour
37
+ ```
38
+
39
+ ## Testing
40
+
41
+ If you'd like to contribute code or modify this gem, you can run the test suite with:
42
+
43
+ ```ruby
44
+ gem install oanda_bank --dev
45
+ bundle exec rspec
46
+ ```
47
+
48
+ ## Contributing
49
+
50
+ 1. Fork this repo and make changes in your own copy
51
+ 2. Add a test if applicable and run the existing tests with `rspec` to make sure they pass
52
+ 3. Commit your changes and push to your fork `git push origin master`
53
+ 4. Create a new pull request and submit it back to me
data/lib/oanda_bank.rb ADDED
@@ -0,0 +1,137 @@
1
+ require 'oauth2'
2
+ require 'money'
3
+ require 'bigdecimal'
4
+ require 'bigdecimal/util'
5
+
6
+ class Money
7
+ module Bank
8
+ class OANDA < VariableExchange
9
+
10
+ # Raised when there is an unexpected error in extracting exchange rates
11
+ # from OANDA
12
+ class FetchError < StandardError; end
13
+
14
+ INSTRUMENTS_URL = "https://api-fxtrade.oanda.com/v1/instruments"
15
+ RATES_URL = "https://api-fxtrade.oanda.com/v1/prices"
16
+
17
+ # @return [Integer] Returns the Time To Live (TTL) in seconds.
18
+ attr_reader :ttl_in_seconds
19
+
20
+ # @return [Time] Returns the time when the rates expire.
21
+ attr_reader :rates_expiration
22
+
23
+ def initialize(account_id, access_token, opts={})
24
+ @client = OAuth2::Client.new(nil, nil)
25
+ @access_token = OAuth2::AccessToken.new(@client, access_token)
26
+ @account_id = account_id
27
+ @updating_mutex = Mutex.new
28
+ setup
29
+ end
30
+
31
+ def update_rates!
32
+ begin
33
+ new_rates = fetch_rates
34
+ @mutex.synchronize do
35
+ @rates = new_rates
36
+ refresh_rates_expiration!
37
+ end
38
+ rescue
39
+ raise FetchError.new
40
+ end
41
+ end
42
+
43
+ def get_rate(from, to, opts = {})
44
+ expire_rates
45
+ fn = Proc.new do
46
+ straight_through = @rates[rate_key_for(from, to)]
47
+ if straight_through
48
+ straight_through
49
+ else
50
+ to_usd = @rates[rate_key_for(from, 'USD')]
51
+ from_usd = @rates[rate_key_for('USD', to)]
52
+ if (to_usd && from_usd)
53
+ to_usd * from_usd
54
+ else
55
+ nil
56
+ end
57
+ end
58
+ end
59
+ if opts[:without_mutex]
60
+ fn.call
61
+ else
62
+ @mutex.synchronize { fn.call }
63
+ end
64
+ end
65
+
66
+ def expire_rates
67
+ if @ttl_in_seconds && @rates_expiration <= Time.now
68
+ if @updating_mutex.try_lock
69
+ begin
70
+ update_rates!
71
+ true
72
+ ensure
73
+ @updating_mutex.unlock
74
+ end
75
+ end
76
+ else
77
+ false
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Set the Time To Live (TTL) in seconds.
83
+ #
84
+ # @param [Integer] the seconds between an expiration and another.
85
+ def ttl_in_seconds=(value)
86
+ @ttl_in_seconds = value
87
+ refresh_rates_expiration! if ttl_in_seconds
88
+ end
89
+
90
+ protected
91
+
92
+ def fetch_rates
93
+ instruments = @access_token.get("#{INSTRUMENTS_URL}?accountId=#{@account_id}")
94
+ instruments = JSON.parse(instruments.body)
95
+
96
+ instrument_codes = instruments['instruments'].map do |instrument|
97
+ instrument['instrument']
98
+ end
99
+
100
+ instruments_argument = instrument_codes.join('%2C')
101
+
102
+ rates = @access_token.get("#{RATES_URL}?instruments=#{instruments_argument}")
103
+ rates = JSON.parse(rates.body)
104
+ rates = rates['prices']
105
+
106
+ result = {}
107
+ rates.each do |rate|
108
+ match = /(?<base>[A-Z0-9]+)_(?<quote>[A-Z0-9]+)/.match(rate['instrument'])
109
+
110
+ if !match
111
+ next
112
+ end
113
+
114
+ from = match[:base].upcase
115
+ to = match[:quote].upcase
116
+
117
+ if !Money::Currency.find(from) || !Money::Currency.find(to)
118
+ next
119
+ end
120
+
121
+ result[rate_key_for(from, to)] = rate['bid'].to_d
122
+ result[rate_key_for(to, from)] = (1/rate['ask']).to_d
123
+ end
124
+ result
125
+ end
126
+
127
+ private
128
+
129
+ def refresh_rates_expiration!
130
+ if @ttl_in_seconds
131
+ @rates_expiration = Time.now + @ttl_in_seconds
132
+ end
133
+ end
134
+
135
+ end # /OANDA
136
+ end # /Bank
137
+ end # /Money
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'oanda_bank'
3
+ gem.version = '1.0.0'
4
+ gem.authors = ['Alex Ianus']
5
+ gem.email = ['hire@alexianus.com']
6
+ gem.description = ['Ruby Money::Bank interface for OANDA currency rate data']
7
+ gem.summary = ['Ruby Money::Bank interface for OANDA currency rate data']
8
+ gem.homepage = 'https://github.com/aianus/oanda_bank'
9
+ gem.licenses = ['MIT']
10
+
11
+ gem.files = `git ls-files`.split($/)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_development_dependency 'vcr', '~> 2.9'
17
+ gem.add_development_dependency 'rspec', '~> 3.0'
18
+ gem.add_development_dependency 'webmock', '~> 1.18'
19
+ gem.add_development_dependency 'simplecov', '~> 0.7'
20
+
21
+ gem.add_dependency 'oauth2', '~> 1.0'
22
+ gem.add_dependency 'money', '~> 6.0'
23
+ end