acvwilson-currency 0.5.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.
- data/COPYING.txt +339 -0
- data/ChangeLog +8 -0
- data/LICENSE.txt +65 -0
- data/Manifest.txt +58 -0
- data/README.txt +51 -0
- data/Releases.txt +155 -0
- data/TODO.txt +9 -0
- data/currency.gemspec +18 -0
- data/examples/ex1.rb +13 -0
- data/examples/xe1.rb +20 -0
- data/lib/currency.rb +143 -0
- data/lib/currency/active_record.rb +265 -0
- data/lib/currency/config.rb +91 -0
- data/lib/currency/core_extensions.rb +83 -0
- data/lib/currency/currency.rb +175 -0
- data/lib/currency/currency/factory.rb +121 -0
- data/lib/currency/currency_version.rb +6 -0
- data/lib/currency/exception.rb +119 -0
- data/lib/currency/exchange.rb +48 -0
- data/lib/currency/exchange/rate.rb +214 -0
- data/lib/currency/exchange/rate/deriver.rb +157 -0
- data/lib/currency/exchange/rate/source.rb +89 -0
- data/lib/currency/exchange/rate/source/base.rb +166 -0
- data/lib/currency/exchange/rate/source/failover.rb +63 -0
- data/lib/currency/exchange/rate/source/federal_reserve.rb +160 -0
- data/lib/currency/exchange/rate/source/historical.rb +79 -0
- data/lib/currency/exchange/rate/source/historical/rate.rb +184 -0
- data/lib/currency/exchange/rate/source/historical/rate_loader.rb +186 -0
- data/lib/currency/exchange/rate/source/historical/writer.rb +220 -0
- data/lib/currency/exchange/rate/source/new_york_fed.rb +127 -0
- data/lib/currency/exchange/rate/source/provider.rb +120 -0
- data/lib/currency/exchange/rate/source/test.rb +50 -0
- data/lib/currency/exchange/rate/source/the_financials.rb +191 -0
- data/lib/currency/exchange/rate/source/timed_cache.rb +198 -0
- data/lib/currency/exchange/rate/source/xe.rb +165 -0
- data/lib/currency/exchange/time_quantitizer.rb +111 -0
- data/lib/currency/formatter.rb +310 -0
- data/lib/currency/macro.rb +321 -0
- data/lib/currency/money.rb +298 -0
- data/lib/currency/money_helper.rb +13 -0
- data/lib/currency/parser.rb +193 -0
- data/spec/ar_column_spec.rb +76 -0
- data/spec/ar_core_spec.rb +68 -0
- data/spec/ar_simple_spec.rb +23 -0
- data/spec/config_spec.rb +29 -0
- data/spec/federal_reserve_spec.rb +75 -0
- data/spec/formatter_spec.rb +72 -0
- data/spec/historical_writer_spec.rb +187 -0
- data/spec/macro_spec.rb +109 -0
- data/spec/money_spec.rb +355 -0
- data/spec/new_york_fed_spec.rb +73 -0
- data/spec/parser_spec.rb +105 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/time_quantitizer_spec.rb +115 -0
- data/spec/timed_cache_spec.rb +95 -0
- data/spec/xe_spec.rb +50 -0
- metadata +117 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
# Copyright (C) 2006-2007 Kurt Stephens <ruby-currency(at)umleta.com>
|
2
|
+
# See LICENSE.txt for details.
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source/base'
|
5
|
+
|
6
|
+
# Gets historical rates from database using Active::Record.
|
7
|
+
# Rates are retrieved using Currency::Exchange::Rate::Source::Historical::Rate as
|
8
|
+
# a database record proxy.
|
9
|
+
#
|
10
|
+
# See Currency::Exchange::Rate::Source::Historical::Writer for a rate archiver.
|
11
|
+
#
|
12
|
+
class Currency::Exchange::Rate::Source::Historical < Currency::Exchange::Rate::Source::Base
|
13
|
+
|
14
|
+
# Select specific rate source.
|
15
|
+
# Defaults to nil
|
16
|
+
attr_accessor :source
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@source = nil # any
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def source_key
|
25
|
+
@source ? @source.join(',') : ''
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# This Exchange's name is the same as its #uri.
|
30
|
+
def name
|
31
|
+
"historical #{source_key}"
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def initialize(*opt)
|
36
|
+
super
|
37
|
+
@rates_cache = { }
|
38
|
+
@raw_rates_cache = { }
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def clear_rates
|
43
|
+
@rates_cache.clear
|
44
|
+
@raw_rates_cache.clear
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Returns a Rate.
|
50
|
+
def get_rate(c1, c2, time)
|
51
|
+
# rate =
|
52
|
+
get_rates(time).select{ | r | r.c1 == c1 && r.c2 == c2 }[0]
|
53
|
+
# $stderr.puts "#{self}.get_rate(#{c1}, #{c2}, #{time.inspect}) => #{rate.inspect}"
|
54
|
+
# rate
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# Return a list of base Rates.
|
59
|
+
def get_rates(time = nil)
|
60
|
+
@rates_cache["#{source_key}:#{time}"] ||=
|
61
|
+
get_raw_rates(time).collect do | rr |
|
62
|
+
rr.to_rate
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Return a list of raw rates.
|
68
|
+
def get_raw_rates(time = nil)
|
69
|
+
@raw_rates_cache["#{source_key}:#{time}"] ||=
|
70
|
+
::Currency::Exchange::Rate::Source::Historical::Rate.new(:c1 => nil, :c2 => nil, :date => time, :source => source).
|
71
|
+
find_matching_this(:all)
|
72
|
+
end
|
73
|
+
|
74
|
+
end # class
|
75
|
+
|
76
|
+
|
77
|
+
require 'currency/exchange/rate/source/historical/rate'
|
78
|
+
|
79
|
+
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record/base'
|
3
|
+
|
4
|
+
require 'currency/exchange/rate/source/historical'
|
5
|
+
|
6
|
+
# This class represents a historical Rate in a database.
|
7
|
+
# It requires ActiveRecord.
|
8
|
+
#
|
9
|
+
class Currency::Exchange::Rate::Source::Historical::Rate < ::ActiveRecord::Base
|
10
|
+
@@_table_name ||= Currency::Config.current.historical_table_name
|
11
|
+
set_table_name @@_table_name
|
12
|
+
|
13
|
+
# Can create a table and indices for this class
|
14
|
+
# when passed a Migration.
|
15
|
+
def self.__create_table(m, table_name = @@_table_name)
|
16
|
+
table_name = table_name.intern
|
17
|
+
m.instance_eval do
|
18
|
+
create_table table_name do |t|
|
19
|
+
t.column :created_on, :datetime, :null => false
|
20
|
+
t.column :updated_on, :datetime
|
21
|
+
|
22
|
+
t.column :c1, :string, :limit => 3, :null => false
|
23
|
+
t.column :c2, :string, :limit => 3, :null => false
|
24
|
+
|
25
|
+
t.column :source, :string, :limit => 32, :null => false
|
26
|
+
|
27
|
+
t.column :rate, :float, :null => false
|
28
|
+
|
29
|
+
t.column :rate_avg, :float
|
30
|
+
t.column :rate_samples, :integer
|
31
|
+
t.column :rate_lo, :float
|
32
|
+
t.column :rate_hi, :float
|
33
|
+
t.column :rate_date_0, :float
|
34
|
+
t.column :rate_date_1, :float
|
35
|
+
|
36
|
+
t.column :date, :datetime, :null => false
|
37
|
+
t.column :date_0, :datetime
|
38
|
+
t.column :date_1, :datetime
|
39
|
+
|
40
|
+
t.column :derived, :string, :limit => 64
|
41
|
+
end
|
42
|
+
|
43
|
+
add_index table_name, :c1
|
44
|
+
add_index table_name, :c2
|
45
|
+
add_index table_name, :source
|
46
|
+
add_index table_name, :date
|
47
|
+
add_index table_name, :date_0
|
48
|
+
add_index table_name, :date_1
|
49
|
+
add_index table_name, [:c1, :c2, :source, :date_0, :date_1], :name => 'c1_c2_src_date_range', :unique => true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Initializes this object from a Currency::Exchange::Rate object.
|
55
|
+
def from_rate(rate)
|
56
|
+
self.c1 = rate.c1.code.to_s
|
57
|
+
self.c2 = rate.c2.code.to_s
|
58
|
+
self.rate = rate.rate
|
59
|
+
self.rate_avg = rate.rate_avg
|
60
|
+
self.rate_samples = rate.rate_samples
|
61
|
+
self.rate_lo = rate.rate_lo
|
62
|
+
self.rate_hi = rate.rate_hi
|
63
|
+
self.rate_date_0 = rate.rate_date_0
|
64
|
+
self.rate_date_1 = rate.rate_date_1
|
65
|
+
self.source = rate.source
|
66
|
+
self.derived = rate.derived
|
67
|
+
self.date = rate.date
|
68
|
+
self.date_0 = rate.date_0
|
69
|
+
self.date_1 = rate.date_1
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Convert all dates to localtime.
|
75
|
+
def dates_to_localtime!
|
76
|
+
self.date = self.date && self.date.clone.localtime
|
77
|
+
self.date_0 = self.date_0 && self.date_0.clone.localtime
|
78
|
+
self.date_1 = self.date_1 && self.date_1.clone.localtime
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Creates a new Currency::Exchange::Rate object.
|
83
|
+
def to_rate(cls = ::Currency::Exchange::Rate)
|
84
|
+
cls.
|
85
|
+
new(
|
86
|
+
::Currency::Currency.get(self.c1),
|
87
|
+
::Currency::Currency.get(self.c2),
|
88
|
+
self.rate,
|
89
|
+
"historical #{self.source}",
|
90
|
+
self.date,
|
91
|
+
self.derived,
|
92
|
+
{
|
93
|
+
:rate_avg => self.rate_avg,
|
94
|
+
:rate_samples => self.rate_samples,
|
95
|
+
:rate_lo => self.rate_lo,
|
96
|
+
:rate_hi => self.rate_hi,
|
97
|
+
:rate_date_0 => self.rate_date_0,
|
98
|
+
:rate_date_1 => self.rate_date_1,
|
99
|
+
:date_0 => self.date_0,
|
100
|
+
:date_1 => self.date_1
|
101
|
+
})
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# Various defaults.
|
106
|
+
def before_validation
|
107
|
+
self.rate_avg = self.rate unless self.rate_avg
|
108
|
+
self.rate_samples = 1 unless self.rate_samples
|
109
|
+
self.rate_lo = self.rate unless self.rate_lo
|
110
|
+
self.rate_hi = self.rate unless self.rate_hi
|
111
|
+
self.rate_date_0 = self.rate unless self.rate_date_0
|
112
|
+
self.rate_date_1 = self.rate unless self.rate_date_1
|
113
|
+
|
114
|
+
#self.date_0 = self.date unless self.date_0
|
115
|
+
#self.date_1 = self.date unless self.date_1
|
116
|
+
self.date = self.date_0 + (self.date_1 - self.date_0) * 0.5 if ! self.date && self.date_0 && self.date_1
|
117
|
+
self.date = self.date_0 unless self.date
|
118
|
+
self.date = self.date_1 unless self.date
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# Returns a ActiveRecord::Base#find :conditions value
|
123
|
+
# to locate any rates that will match this one.
|
124
|
+
#
|
125
|
+
# source may be a list of sources.
|
126
|
+
# date will match inside date_0 ... date_1 or exactly.
|
127
|
+
#
|
128
|
+
def find_matching_this_conditions
|
129
|
+
sql = [ ]
|
130
|
+
values = [ ]
|
131
|
+
|
132
|
+
if self.c1
|
133
|
+
sql << 'c1 = ?'
|
134
|
+
values.push(self.c1.to_s)
|
135
|
+
end
|
136
|
+
|
137
|
+
if self.c2
|
138
|
+
sql << 'c2 = ?'
|
139
|
+
values.push(self.c2.to_s)
|
140
|
+
end
|
141
|
+
|
142
|
+
if self.source
|
143
|
+
if self.source.kind_of?(Array)
|
144
|
+
sql << 'source IN ?'
|
145
|
+
else
|
146
|
+
sql << 'source = ?'
|
147
|
+
end
|
148
|
+
values.push(self.source)
|
149
|
+
end
|
150
|
+
|
151
|
+
if self.date
|
152
|
+
sql << '(((date_0 IS NULL) OR (date_0 <= ?)) AND ((date_1 IS NULL) OR (date_1 > ?))) OR date = ?'
|
153
|
+
values.push(self.date, self.date, self.date)
|
154
|
+
end
|
155
|
+
|
156
|
+
if self.date_0
|
157
|
+
sql << 'date_0 = ?'
|
158
|
+
values.push(self.date_0)
|
159
|
+
end
|
160
|
+
|
161
|
+
if self.date_1
|
162
|
+
sql << 'date_1 = ?'
|
163
|
+
values.push(self.date_1)
|
164
|
+
end
|
165
|
+
|
166
|
+
sql << '1 = 1' if sql.empty?
|
167
|
+
|
168
|
+
values.unshift(sql.collect{|x| "(#{x})"}.join(' AND '))
|
169
|
+
|
170
|
+
# $stderr.puts "values = #{values.inspect}"
|
171
|
+
|
172
|
+
values
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
# Shorthand.
|
177
|
+
def find_matching_this(opt1 = :all, *opts)
|
178
|
+
self.class.find(opt1, :conditions => find_matching_this_conditions, *opts)
|
179
|
+
end
|
180
|
+
|
181
|
+
end # class
|
182
|
+
|
183
|
+
|
184
|
+
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'currency/exchange/rate/source/historical'
|
2
|
+
require 'currency/exchange/rate/source/historical/rate'
|
3
|
+
require 'currency/exchange/rate/source/historical/writer'
|
4
|
+
|
5
|
+
|
6
|
+
# Currency::Config.current.historical_table_name = 'currency_rates'
|
7
|
+
# opts['uri_path'] ||= 'syndicated/cnusa/fxrates.xml'
|
8
|
+
|
9
|
+
# Loads rates from multiple sources and will store them
|
10
|
+
# as historical rates in a database.
|
11
|
+
|
12
|
+
class ::Currency::Exchange::Rate::Source::Historical::RateLoader
|
13
|
+
attr_accessor :options
|
14
|
+
attr_accessor :source_options
|
15
|
+
attr_accessor :required_currencies
|
16
|
+
attr_accessor :rate_sources
|
17
|
+
attr_accessor :rate_source_options
|
18
|
+
attr_accessor :verbose
|
19
|
+
attr_accessor :preferred_summary_source
|
20
|
+
attr_accessor :base_currencies
|
21
|
+
attr_accessor :summary_rate_src
|
22
|
+
attr_reader :writer
|
23
|
+
|
24
|
+
def initialize(opts = { })
|
25
|
+
self.summary_rate_src = 'summary'
|
26
|
+
self.source_options = { }
|
27
|
+
self.options = opts.dup.freeze
|
28
|
+
self.base_currencies = [ :USD ]
|
29
|
+
self.required_currencies =
|
30
|
+
[
|
31
|
+
:USD,
|
32
|
+
:GBP,
|
33
|
+
:CAD,
|
34
|
+
:EUR,
|
35
|
+
# :MXP,
|
36
|
+
]
|
37
|
+
self.verbose = ! ! ENV['CURRENCY_VERBOSE']
|
38
|
+
opts.each do | k, v |
|
39
|
+
setter = "#{k}="
|
40
|
+
send(setter, v) if respond_to?(setter)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def initialize_writer(writer = Currency::Exchange::Rate::Source::Historical::Writer.new)
|
46
|
+
@writer = writer
|
47
|
+
|
48
|
+
writer.time_quantitizer = :current
|
49
|
+
writer.required_currencies = required_currencies
|
50
|
+
writer.base_currencies = base_currencies
|
51
|
+
writer.preferred_currencies = writer.required_currencies
|
52
|
+
writer.reciprocal_rates = true
|
53
|
+
writer.all_rates = true
|
54
|
+
writer.identity_rates = false
|
55
|
+
|
56
|
+
options.each do | k, v |
|
57
|
+
setter = "#{k}="
|
58
|
+
writer.send(setter, v) if writer.respond_to?(setter)
|
59
|
+
end
|
60
|
+
|
61
|
+
writer
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def run
|
66
|
+
rate_sources.each do | src |
|
67
|
+
# Create a historical rate writer.
|
68
|
+
initialize_writer
|
69
|
+
|
70
|
+
# Handle creating a summary rates called 'summary'.
|
71
|
+
if src == summary_rate_src
|
72
|
+
summary_rates(src)
|
73
|
+
else
|
74
|
+
require "currency/exchange/rate/source/#{src}"
|
75
|
+
src_cls_name = src.gsub(/(^|_)([a-z])/) { | m | $2.upcase }
|
76
|
+
src_cls = Currency::Exchange::Rate::Source.const_get(src_cls_name)
|
77
|
+
src = src_cls.new(source_options)
|
78
|
+
|
79
|
+
writer.source = src
|
80
|
+
|
81
|
+
writer.write_rates
|
82
|
+
end
|
83
|
+
end
|
84
|
+
ensure
|
85
|
+
@writer = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def summary_rates(src)
|
90
|
+
# A list of summary rates.
|
91
|
+
summary_rates = [ ]
|
92
|
+
|
93
|
+
# Get a list of all rate time ranges before today,
|
94
|
+
# that do not have a 'cnu' summary rate.
|
95
|
+
h_rate_cls = Currency::Exchange::Rate::Source::Historical::Rate
|
96
|
+
conn = h_rate_cls.connection
|
97
|
+
|
98
|
+
# Select only rates from yesterday or before back till 30 days.
|
99
|
+
date_1 = Time.now - (0 * 24 * 60 * 60)
|
100
|
+
date_0 = date_1 - (30 * 24 * 60 * 60)
|
101
|
+
|
102
|
+
date_0 = conn.quote(date_0)
|
103
|
+
date_1 = conn.quote(date_1)
|
104
|
+
|
105
|
+
query =
|
106
|
+
"SELECT
|
107
|
+
DISTINCT a.date_0, a.date_1
|
108
|
+
FROM
|
109
|
+
#{h_rate_cls.table_name} AS a
|
110
|
+
WHERE
|
111
|
+
a.source <> '#{src}'
|
112
|
+
AND a.date_1 >= #{date_0} AND a.date_1 < #{date_1}
|
113
|
+
AND (SELECT COUNT(b.id) FROM #{h_rate_cls.table_name} AS b
|
114
|
+
WHERE
|
115
|
+
b.c1 = a.c1 AND b.c2 = a.c2
|
116
|
+
AND b.date_0 = a.date_0 AND b.date_1 = a.date_1
|
117
|
+
AND b.source = '#{src}') = 0
|
118
|
+
ORDER BY
|
119
|
+
date_0"
|
120
|
+
STDERR.puts "query = \n#{query.split("\n").join(' ')}" if verbose
|
121
|
+
|
122
|
+
dates = conn.query(query)
|
123
|
+
|
124
|
+
dates.each do | date_range |
|
125
|
+
STDERR.puts "\n=============================================\n" if verbose
|
126
|
+
STDERR.puts "date_range = #{date_range.inspect}" if verbose
|
127
|
+
|
128
|
+
# Query for all rates that have the same date range.
|
129
|
+
q_rate = h_rate_cls.new(:date_0 => date_range[0], :date_1 => date_range[1])
|
130
|
+
available_rates = q_rate.find_matching_this(:all)
|
131
|
+
|
132
|
+
# Collect all the currency pairs and rates.
|
133
|
+
currency_pair = { }
|
134
|
+
available_rates.each do | h_rate |
|
135
|
+
rate = h_rate.to_rate
|
136
|
+
(currency_pair[ [ rate.c1, rate.c2 ] ] ||= [ ]) << [ h_rate, rate ]
|
137
|
+
# STDERR.puts "rate = #{rate} #{h_rate.date_0} #{h_rate.date_1}" if verbose
|
138
|
+
end
|
139
|
+
|
140
|
+
currency_pair.each_pair do | currency_pair, rates |
|
141
|
+
STDERR.puts "\n =============================================\n" if verbose
|
142
|
+
STDERR.puts " currency_pair = #{currency_pair}" if verbose
|
143
|
+
|
144
|
+
# Create a summary rate for the currency pair.
|
145
|
+
selected_rates = [ ]
|
146
|
+
|
147
|
+
rates.each do | h_rates |
|
148
|
+
h_rate, rate = *h_rates
|
149
|
+
|
150
|
+
# Sanity check!
|
151
|
+
next if h_rate.source == src
|
152
|
+
|
153
|
+
# Found perferred source?
|
154
|
+
if h_rate.source == preferred_summary_source
|
155
|
+
selected_rates = [ h_rates ]
|
156
|
+
break
|
157
|
+
end
|
158
|
+
|
159
|
+
selected_rates << h_rates
|
160
|
+
end
|
161
|
+
|
162
|
+
unless selected_rates.empty?
|
163
|
+
summary_rate = Currency::Exchange::Rate::Writable.new(currency_pair[0], currency_pair[1], 0.0)
|
164
|
+
selected_rates.each do | h_rates |
|
165
|
+
h_rate, rate = *h_rates
|
166
|
+
STDERR.puts " rate = #{rate.inspect}" if verbose
|
167
|
+
summary_rate.collect_rate(rate)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Save the rate.
|
171
|
+
summary_rate.rate = summary_rate.rate_avg
|
172
|
+
summary_rate.source = src
|
173
|
+
summary_rate.derived = 'summary(' + selected_rates.collect{|r| r[0].id}.sort.join(',') + ')'
|
174
|
+
STDERR.puts " summary_rate = #{summary_rate} #{summary_rate.rate_samples}" if verbose
|
175
|
+
|
176
|
+
summary_rates << summary_rate
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
writer.write_rates(summary_rates)
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
|