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 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