forex 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
* [![Code Climate](https://codeclimate.com/github/mcmorgan/forex.png)](https://codeclimate.com/github/mcmorgan/forex)
|
59
|
+
* [![Build Status](https://api.travis-ci.org/mcmorgan/forex.png)](https://travis-ci.org/mcmorgan/forex)
|
60
|
+
* [![Dependency Status](https://gemnasium.com/mcmorgan/forex.png)](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