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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7113e7419c77329d14770c81a3aee7ee3b3b792
4
- data.tar.gz: d4999403674df0b8248ec33dd88d809ce446af18
3
+ metadata.gz: 1abccfe13090f31cf101ec855aae312fdd2d9280
4
+ data.tar.gz: c81106b0304c9a62456eb34f9b5fdfc110daac3b
5
5
  SHA512:
6
- metadata.gz: d55666dd2afd131d112134362d072dcdd0f296d7505f759390d927e14528f95940d46be25edfead59b15a9ac3d7fd6cd24b3a0b0fa9d53a15f6da212c4dce05b
7
- data.tar.gz: bb0d817c5d8f98528297470338f1b26179bfd4e97d942e3c0f95e5a540b1246ce6b387cc93477bd915c627f3c384d7308d5982e0575964ca53f9dcc8b3f6cb4c
6
+ metadata.gz: da32e3bd6c127bd657f3fcfe2c24df6078a2b404b4e6b6e0df345c03660cc897727cc5156733b785e328710b20c9bfe56028d2251ea090201d4462164cfd5271
7
+ data.tar.gz: 9536d87fd001523776de9eb9c808237750f643c6e1cffba215c1ebc53470a386aa717ef83acf9182403759279f11295eb981448253a1db7afd1087616a85f359
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ script: bundle exec cucumber && bundle exec rspec
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Forex
2
2
 
3
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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 |
@@ -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
@@ -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
- module Forex
4
- # Your code goes here...
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
+
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Forex
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end