forex 0.0.1 → 0.1.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 +4 -4
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/README.md +41 -2
- data/features/step_definitions/tabular_rates_steps.rb +34 -0
- data/features/support/env.rb +1 -0
- data/features/tabular_rates.feature +37 -0
- data/forex.gemspec +10 -1
- data/lib/forex.rb +8 -3
- data/lib/forex/tabular_rates.rb +80 -0
- data/lib/forex/trader.rb +47 -0
- data/lib/forex/traders/jm/bns.rb +20 -0
- data/lib/forex/traders/jm/fxtraders.rb +26 -0
- data/lib/forex/traders/jm/jmmb.rb +24 -0
- data/lib/forex/traders/jm/jnbs.rb +20 -0
- data/lib/forex/traders/jm/ncb.rb +19 -0
- data/lib/forex/version.rb +1 -1
- data/spec/spec_helper.rb +28 -0
- data/spec/support/vcr.rb +14 -0
- data/spec/trader_spec.rb +54 -0
- data/spec/traders_spec.rb +10 -0
- data/spec/vcr/traders/bns.yml +75 -0
- data/spec/vcr/traders/fxtraders.yml +332 -0
- data/spec/vcr/traders/jmmb.yml +1106 -0
- data/spec/vcr/traders/jnbs.yml +487 -0
- data/spec/vcr/traders/ncb.yml +1090 -0
- metadata +136 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1abccfe13090f31cf101ec855aae312fdd2d9280
|
4
|
+
data.tar.gz: c81106b0304c9a62456eb34f9b5fdfc110daac3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da32e3bd6c127bd657f3fcfe2c24df6078a2b404b4e6b6e0df345c03660cc897727cc5156733b785e328710b20c9bfe56028d2251ea090201d4462164cfd5271
|
7
|
+
data.tar.gz: 9536d87fd001523776de9eb9c808237750f643c6e1cffba215c1ebc53470a386aa717ef83acf9182403759279f11295eb981448253a1db7afd1087616a85f359
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Forex
|
2
2
|
|
3
|
-
|
3
|
+
Provides a simple DSL for managing Foreign Exchange rates (forex) for various
|
4
|
+
traders.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
@@ -18,7 +19,45 @@ Or install it yourself as:
|
|
18
19
|
|
19
20
|
## Usage
|
20
21
|
|
21
|
-
|
22
|
+
To add a new trader, create a new file at
|
23
|
+
``lib/forex/traders/<country-code>/<trader-name>.rb`` with the following
|
24
|
+
contents:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
|
28
|
+
Forex::Trader.define "NCB" do |t|
|
29
|
+
t.base_currency = "JMD"
|
30
|
+
t.name = "National Commercial Bank"
|
31
|
+
t.endpoint = "http://www.jncb.com/rates/foreignexchangerates"
|
32
|
+
|
33
|
+
t.rates_parser = Proc.new do |doc| # doc is a nokogiri document
|
34
|
+
# process the doc and return rates hash in the following format
|
35
|
+
|
36
|
+
# {
|
37
|
+
# "USD" => {
|
38
|
+
# :buy_cash => 102.0,
|
39
|
+
# :buy_draft => 103.3,
|
40
|
+
# :sell_cash => 105.0
|
41
|
+
# },
|
42
|
+
# # ...
|
43
|
+
# }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Rates may be fetched for a specific trader
|
48
|
+
Forex::Trader.all['BNS'].fetch
|
49
|
+
|
50
|
+
# Or all traders and yielded to the block given
|
51
|
+
Forex::Trader.fetch_all do |trader|
|
52
|
+
# Save or do some other processing on the rates ``trader.rates`` hash.
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
## Code Status
|
57
|
+
|
58
|
+
* [](https://codeclimate.com/github/mcmorgan/forex)
|
59
|
+
* [](https://travis-ci.org/mcmorgan/forex)
|
60
|
+
* [](https://gemnasium.com/mcmorgan/forex)
|
22
61
|
|
23
62
|
## Contributing
|
24
63
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Given(/^the tabular rates table:$/) do |table|
|
2
|
+
@table = Nokogiri::HTML(table)
|
3
|
+
end
|
4
|
+
|
5
|
+
When(/^the column options exist for the tabular rates parser:$/) do |options_table|
|
6
|
+
@options = options_table.hashes.first
|
7
|
+
end
|
8
|
+
|
9
|
+
Then(/^parsing the table should return the following rates:$/) do |table|
|
10
|
+
ensure_rates_are_equal_to table
|
11
|
+
end
|
12
|
+
|
13
|
+
Then(/^parsing the table should raise an exception with the message:$/) do |table|
|
14
|
+
message = table.raw.first.first
|
15
|
+
expect { parse_rates }.to raise_error(message)
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse_rates
|
19
|
+
Forex::TabularRates.new(@table, @options).parse_rates
|
20
|
+
end
|
21
|
+
|
22
|
+
def ensure_rates_are_equal_to(table)
|
23
|
+
formatted_rates = table.hashes.each_with_object({}) do |table_hash, currencies|
|
24
|
+
table_hash.symbolize_keys!
|
25
|
+
|
26
|
+
currencies[table_hash.delete(:currency_code)] =
|
27
|
+
table_hash.each_with_object({}) do |currency_rate, rates|
|
28
|
+
currency, rate = *currency_rate
|
29
|
+
rates[currency] = rate.to_f
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
parse_rates.should == formatted_rates
|
34
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'forex'
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Feature: Tabular Rates
|
2
|
+
|
3
|
+
As a developer,
|
4
|
+
In order to easily retrieve tabular rates,
|
5
|
+
I want to be able to use the TabularRates to retrive rates formatted in a table
|
6
|
+
|
7
|
+
|
8
|
+
Background:
|
9
|
+
Given the tabular rates table:
|
10
|
+
"""
|
11
|
+
<table>
|
12
|
+
<tr><td> </td> <td>Cash </td> <td>Cheque</td> <td>Cash & Cheque</td></tr>
|
13
|
+
<tr><td> </td> <td>BUY </td> <td>BUY </td> <td>SELL </td> </tr>
|
14
|
+
<tr><td>USD</td> <td>101.30</td> <td>103.30</td> <td>105.00</td> </tr>
|
15
|
+
<tr><td>GBP</td> <td>166.20</td> <td>169.63</td> <td>173.30</td> </tr>
|
16
|
+
<tr><td>CAD</td> <td> 94.51</td> <td> 96.92</td> <td> 99.72</td> </tr>
|
17
|
+
<tr><td>EUR</td> <td> </td> <td>137.42</td> <td>143.16</td> </tr>
|
18
|
+
</table>
|
19
|
+
"""
|
20
|
+
|
21
|
+
Scenario: Parsing a table with the correct options
|
22
|
+
When the column options exist for the tabular rates parser:
|
23
|
+
| currency_code | buy_cash | buy_draft | sell_cash | sell_draft |
|
24
|
+
| 0 | 1 | 2 | 3 | 3 |
|
25
|
+
Then parsing the table should return the following rates:
|
26
|
+
| currency_code | buy_cash | buy_draft | sell_cash | sell_draft |
|
27
|
+
| USD | 101.30 | 103.30 | 105.00 | 105.00 |
|
28
|
+
| GBP | 166.20 | 169.63 | 173.30 | 173.30 |
|
29
|
+
| CAD | 94.51 | 96.92 | 99.72 | 99.72 |
|
30
|
+
| EUR | | 137.42 | 143.16 | 143.16 |
|
31
|
+
|
32
|
+
Scenario: Parsing a table with invalid options
|
33
|
+
When the column options exist for the tabular rates parser:
|
34
|
+
| currency_code | buy_cash | buy_draft | sell_cash | sell_draft |
|
35
|
+
| 0 | 1 | 2 | 3 | 4 |
|
36
|
+
Then parsing the table should raise an exception with the message:
|
37
|
+
| sell_draft (4) does not exist in table |
|
data/forex.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["marcel.morgan@codedry.com"]
|
11
11
|
spec.description = %q{Forex Rates for traders using a simple DSL for parsing}
|
12
12
|
spec.summary = %q{Forex Rates for traders}
|
13
|
-
spec.homepage = ""
|
13
|
+
spec.homepage = "https://github.com/mcmorgan/forex"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
@@ -18,6 +18,15 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.add_dependency "nokogiri"
|
22
|
+
spec.add_dependency "json"
|
23
|
+
spec.add_dependency "activesupport"
|
24
|
+
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "cucumber"
|
27
|
+
spec.add_development_dependency "vcr"
|
28
|
+
spec.add_development_dependency "webmock"
|
29
|
+
|
21
30
|
spec.add_development_dependency "bundler", "~> 1.3"
|
22
31
|
spec.add_development_dependency "rake"
|
23
32
|
end
|
data/lib/forex.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
|
1
5
|
require "forex/version"
|
6
|
+
require "forex/tabular_rates"
|
7
|
+
require "forex/trader"
|
2
8
|
|
3
|
-
|
4
|
-
|
5
|
-
end
|
9
|
+
# Traders are automatically loaded
|
10
|
+
Dir[File.dirname(__FILE__) + '/forex/traders/**/*.rb'].each { |t| require t }
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Forex
|
2
|
+
class TabularRates
|
3
|
+
NoSuchColumn = Class.new(StandardError)
|
4
|
+
|
5
|
+
attr_accessor :table, :options
|
6
|
+
|
7
|
+
def initialize(table, options)
|
8
|
+
@table = table
|
9
|
+
@options = options.symbolize_keys
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_rates
|
13
|
+
currency = options.delete(:currency_code) || 0
|
14
|
+
|
15
|
+
table.css('tr').each_with_object({}) do |tr, currencies|
|
16
|
+
cells = tr.css('td')
|
17
|
+
next if cells.empty?
|
18
|
+
|
19
|
+
currency_code = CurrencyCode.new(cells[currency.to_i].content)
|
20
|
+
|
21
|
+
next if currencies.has_key?(currency_code.to_s) || currency_code.invalid?
|
22
|
+
|
23
|
+
currencies[currency_code.to_s] = column_labels.each_with_object({}) do |column_label, rates|
|
24
|
+
next unless rate_column = options[column_label]
|
25
|
+
|
26
|
+
rate_node = cells[rate_column.to_i]
|
27
|
+
raise NoSuchColumn, "#{column_label} (#{rate_column}) does not exist in table" unless rate_node
|
28
|
+
|
29
|
+
rates[column_label.to_sym] = Currency.new(rate_node.content).value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def column_labels
|
35
|
+
[:buy_cash, :buy_draft, :sell_cash, :sell_draft]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class Currency
|
41
|
+
|
42
|
+
def initialize(string)
|
43
|
+
@string = string
|
44
|
+
end
|
45
|
+
|
46
|
+
# converts the currency to it's storage representation
|
47
|
+
def value
|
48
|
+
@currency_code ||= @string.strip.to_f
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
class CurrencyCode
|
54
|
+
def initialize(string)
|
55
|
+
@string = string
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO validate the currency codes via http://www.xe.com/iso4217.php
|
59
|
+
def valid?
|
60
|
+
@string = to_s # hack
|
61
|
+
!@string.blank? && @string.length == 3
|
62
|
+
end
|
63
|
+
|
64
|
+
def invalid?
|
65
|
+
!valid?
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
@string.strip.
|
70
|
+
# Replace currency symbols with letter equivalent
|
71
|
+
# TODO go crazy and add the rest http://www.xe.com/symbols.php
|
72
|
+
gsub('$', 'D').
|
73
|
+
|
74
|
+
# Remove all non word charactes ([^A-Za-z0-9_])
|
75
|
+
gsub(/\W/,'')
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
data/lib/forex/trader.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Forex
|
2
|
+
CannotRedefineTrader = Class.new(StandardError)
|
3
|
+
|
4
|
+
class Trader
|
5
|
+
attr_accessor :short_name,
|
6
|
+
:name,
|
7
|
+
:base_currency,
|
8
|
+
:endpoint,
|
9
|
+
:rates_parser,
|
10
|
+
:rates
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :all
|
14
|
+
|
15
|
+
def define(short_name)
|
16
|
+
raise CannotRedefineTrader, short_name if all[short_name]
|
17
|
+
|
18
|
+
t = Forex::Trader.new(short_name)
|
19
|
+
yield t if block_given?
|
20
|
+
@all[short_name] = t
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset
|
24
|
+
@all = Hash.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(short_name)
|
29
|
+
@short_name = short_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch
|
33
|
+
@rates = rates_parser.(doc)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# doc built from the endpoint
|
39
|
+
def doc
|
40
|
+
markup = Net::HTTP.get(URI(endpoint))
|
41
|
+
Nokogiri::HTML(markup)
|
42
|
+
end
|
43
|
+
|
44
|
+
reset
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Forex::Trader.define "BNS" do |t|
|
2
|
+
t.base_currency = "JMD"
|
3
|
+
t.name = "Bank of Nova Scotia"
|
4
|
+
t.endpoint = "http://www4.scotiabank.com/cgi-bin/ratesTool/depdisplay.cgi?pid=56"
|
5
|
+
|
6
|
+
t.rates_parser = Proc.new do |doc| # doc is a nokogiri document
|
7
|
+
|
8
|
+
options = {
|
9
|
+
currency_code: 0,
|
10
|
+
buy_cash: 1,
|
11
|
+
buy_draft: 2,
|
12
|
+
sell_cash: 3,
|
13
|
+
sell_draft: 3, # yes, it's the same rate
|
14
|
+
}
|
15
|
+
|
16
|
+
table = doc.css("table").first
|
17
|
+
|
18
|
+
Forex::TabularRates.new(table, options).parse_rates
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Forex::Trader.define "FXTRADERS" do |t|
|
2
|
+
t.base_currency = "JMD"
|
3
|
+
t.name = "FX Traders"
|
4
|
+
t.endpoint = "http://www.fxtrader.gkmsonline.com/rates"
|
5
|
+
|
6
|
+
t.rates_parser = Proc.new do |doc| # doc is a nokogiri document
|
7
|
+
|
8
|
+
content_for = ->(type, n, part = nil) do
|
9
|
+
doc.css(
|
10
|
+
['.views-field-field-fx-trader', type, n, part, 'value span'].compact.join('-')
|
11
|
+
).first.content
|
12
|
+
end
|
13
|
+
|
14
|
+
(1..5).each_with_object({}) do |n, currencies|
|
15
|
+
country_code = content_for.(:currency, n)
|
16
|
+
|
17
|
+
currencies[country_code] = {
|
18
|
+
buy_cash: content_for.(:buying, n).to_f,
|
19
|
+
buy_draft: content_for.(:buying, n, :b).to_f,
|
20
|
+
sell_cash: content_for.(:selling, n).to_f,
|
21
|
+
sell_draft: content_for.(:selling, n, :b).to_f,
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Forex::Trader.define "JMMB" do |t|
|
2
|
+
t.base_currency = "JMD"
|
3
|
+
t.name = "Jamaica Money Market Brokers"
|
4
|
+
t.endpoint = "http://www.jmmb.com/full_rates.php"
|
5
|
+
|
6
|
+
t.rates_parser = Proc.new do |doc| # doc is a nokogiri document
|
7
|
+
|
8
|
+
options = {
|
9
|
+
currency_code: 0,
|
10
|
+
buy_cash: 1,
|
11
|
+
buy_draft: 3,
|
12
|
+
sell_cash: 2,
|
13
|
+
sell_draft: 4
|
14
|
+
}
|
15
|
+
|
16
|
+
table =
|
17
|
+
doc.search("[text()*='FX Trading Rates']").first. # Section with rates
|
18
|
+
ancestors('table').first. # Root table for section
|
19
|
+
css("table").first # Rates table
|
20
|
+
|
21
|
+
Forex::TabularRates.new(table, options).parse_rates
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Forex::Trader.define "JNBS" do |t|
|
2
|
+
t.base_currency = "JMD"
|
3
|
+
t.name = "Jamaica National Building Society"
|
4
|
+
t.endpoint = "http://www.jnbs.com/fx-rates-2"
|
5
|
+
|
6
|
+
t.rates_parser = Proc.new do |doc| # doc is a nokogiri document
|
7
|
+
|
8
|
+
options = {
|
9
|
+
currency_code: 0,
|
10
|
+
sell_cash: 4,
|
11
|
+
buy_cash: 2,
|
12
|
+
buy_draft: 1,
|
13
|
+
}
|
14
|
+
|
15
|
+
table = doc.css(".fx-full").first
|
16
|
+
|
17
|
+
Forex::TabularRates.new(table, options).parse_rates
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Forex::Trader.define "NCB" do |t|
|
2
|
+
t.base_currency = "JMD"
|
3
|
+
t.name = "National Commercial Bank"
|
4
|
+
t.endpoint = "http://www.jncb.com/rates/foreignexchangerates"
|
5
|
+
|
6
|
+
t.rates_parser = Proc.new do |doc| # doc is a nokogiri document
|
7
|
+
|
8
|
+
options = {
|
9
|
+
currency_code: 1,
|
10
|
+
sell_cash: 2,
|
11
|
+
buy_cash: 4,
|
12
|
+
buy_draft: 3,
|
13
|
+
}
|
14
|
+
|
15
|
+
table = doc.css(".rates table").first
|
16
|
+
|
17
|
+
Forex::TabularRates.new(table, options).parse_rates
|
18
|
+
end
|
19
|
+
end
|
data/lib/forex/version.rb
CHANGED