signal_tools 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Matt White
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,39 @@
1
+ == Signal Tools
2
+
3
+ Signal tools allows you to create technical analysis data for a given stock (like MACD, stochastic, and exponential moving averages).
4
+
5
+ == Installation
6
+
7
+ gem install signal_tools
8
+ bundle install --without development test
9
+
10
+ == Usage
11
+
12
+ require 'signal_tools'
13
+
14
+ stock = SignalTools::Stock.new('GOOG', '2010-01-01', '2010-05-31')
15
+ # Leave the last parameter blank to get from a previous date up until today:
16
+ stock = SignalTools::Stock.new('GOOG', '2010-01-01')
17
+ # Leave both dates blank to get the past 90 days:
18
+ stock = SignalTools::Stock.new('GOOG')
19
+
20
+ # Generate MACD signal with an 8 day short period, 17 day long period, and 9 day EMA signal period:
21
+ stock.macd(8, 17, 9)
22
+
23
+ # Generate slow stochastic signal with a 14 day K period and a 5 day D period:
24
+ stock.slow_stochastic(14, 5)
25
+
26
+ == Note on Patches/Pull Requests
27
+
28
+ * Fork the project.
29
+ * Make your feature addition or bug fix.
30
+ * Add tests for it. This is important so I don't break it in a
31
+ future version unintentionally.
32
+ * Commit, do not mess with rakefile, version, or history.
33
+ (if you want to have your own version, that is fine but
34
+ bump version in a commit by itself I can ignore when I pull)
35
+ * Send me a pull request. Bonus points for topic branches.
36
+
37
+ == Copyright
38
+
39
+ Copyright (c) 2010 Matt White. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/**/test_*.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ begin
12
+ require 'rcov/rcovtask'
13
+ Rcov::RcovTask.new do |test|
14
+ test.libs << 'test'
15
+ test.pattern = 'test/**/*_test.rb'
16
+ test.verbose = true
17
+ end
18
+ rescue LoadError
19
+ task :rcov do
20
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
21
+ end
22
+ end
23
+
24
+ #task :test => :check_dependencies
25
+
26
+ task :default => :test
27
+
28
+ require 'rake/rdoctask'
29
+ Rake::RDocTask.new do |rdoc|
30
+ if File.exist?('VERSION')
31
+ version = File.read('VERSION')
32
+ else
33
+ version = ""
34
+ end
35
+
36
+ rdoc.rdoc_dir = 'rdoc'
37
+ rdoc.title = "signal_tools #{version}"
38
+ rdoc.rdoc_files.include('README*')
39
+ rdoc.rdoc_files.include('lib/**/*.rb')
40
+ end
@@ -0,0 +1,309 @@
1
+ module SignalTools
2
+ class Stock
3
+ Default_Period = 90
4
+ EMA_Seed_Days = 10
5
+ Slow_K_SMA = 3
6
+ ATR_Seed_Days = 14
7
+
8
+ attr_accessor :ticker
9
+ attr_reader :stock_data
10
+
11
+ def initialize(ticker, from_date=Date.today-Default_Period, to_date=Date.today)
12
+ from_date = Date.parse(from_date) unless from_date.is_a?(Date)
13
+ to_date = Date.parse(to_date) unless to_date.is_a?(Date)
14
+ @ticker = ticker
15
+ @stock_data = SignalTools::StockData.new(ticker, from_date, to_date)
16
+ end
17
+
18
+ def dates
19
+ @stock_data.dates
20
+ end
21
+
22
+ # Takes a period of days over which to average closing prices and returns the exponential moving average for each day.
23
+ def ema(period=10)
24
+ trim_data_to_range(ema_points(period, @stock_data.close_prices))
25
+ end
26
+
27
+ def macd(fast=8, slow=17, signal=9)
28
+ trim_data_to_range(macd_points(fast, slow, signal))
29
+ end
30
+
31
+ def fast_stochastic(k=14, d=5)
32
+ trim_data_to_range(fast_stochastic_points(k, d))
33
+ end
34
+
35
+ def slow_stochastic(k=14, d=5)
36
+ trim_data_to_range(slow_stochastic_points(k, d))
37
+ end
38
+
39
+ def atr(period=14)
40
+ trim_data_to_range(average_true_ranges(period))
41
+ end
42
+
43
+ def adx(period=14)
44
+ trim_data_to_range(average_directional_indexes(period))
45
+ end
46
+
47
+ private
48
+
49
+ #### EMA methods
50
+
51
+ def ema_points(period, data, type=:default)
52
+ emas = [default_simple_average(data, EMA_Seed_Days)]
53
+ if type == :wilder
54
+ data.slice(EMA_Seed_Days..-1).each { |current| emas << calculate_wilder_ema(emas.last, current, period) }
55
+ else
56
+ data.slice(EMA_Seed_Days..-1).each { |current| emas << calculate_ema(emas.last, current, period) }
57
+ end
58
+ emas
59
+ end
60
+
61
+ #Takes current value, previous day's EMA, and number of days. Returns EMA for that day.
62
+ def calculate_ema(previous, current, period)
63
+ (current - previous) * (2.0 / (period + 1)) + previous
64
+ end
65
+
66
+ #Uses Wilder's moving average formula.
67
+ def calculate_wilder_ema(previous, current, period)
68
+ (previous * (period - 1) + current) / period
69
+ end
70
+
71
+ #Takes a period and array of data and calculates the sum ema over the period specified.
72
+ def period_sum_ema(period, data)
73
+ raise if data.size <= period
74
+ sum_emas = [SignalTools.sum(data[0...period])]
75
+ data[(period..-1)].each do |today|
76
+ sum_emas << (sum_emas.last - (sum_emas.last / period) + today)
77
+ end
78
+ sum_emas
79
+ end
80
+
81
+ #### MACD Methods
82
+
83
+ # Takes a period of days for fast, slow, signal, and time period (eg 8,17,9).
84
+ def macd_points(fast, slow, signal)
85
+ fast_ema_points = ema_points(fast, @stock_data.close_prices)
86
+ slow_ema_points = ema_points(slow, @stock_data.close_prices)
87
+ macd_and_divergence_points(fast_ema_points, slow_ema_points, signal)
88
+ end
89
+
90
+ def macd_and_divergence_points(fast_ema_points, slow_ema_points, signal)
91
+ macds = differences_between_arrays(fast_ema_points, slow_ema_points)
92
+ signal_points = ema_points(signal, macds)
93
+ divergences = differences_between_arrays(macds, signal_points)
94
+ {:signal_points => signal_points, :divergences => divergences}
95
+ end
96
+
97
+ # Returns an array with the differences between the first_points and second_points
98
+ def differences_between_arrays(first_points, second_points)
99
+ SignalTools.truncate_to_shortest!(first_points, second_points)
100
+ differences = []
101
+ first_points.each_with_index { |fp, index| differences << fp - second_points[index] }
102
+ differences
103
+ end
104
+
105
+ #### Stochastic Methods
106
+
107
+ def fast_stochastic_points(k_period, d_period)
108
+ k_points = calculate_fast_stochastic_k_points(k_period)
109
+ d_points = calculate_d_points(k_points, d_period)
110
+ k_d_points(k_points, d_points)
111
+ end
112
+
113
+ def slow_stochastic_points(k_period, d_period)
114
+ fast_points = fast_stochastic_points(k_period, d_period)
115
+ k_points = slow_k_points(fast_points[:k])
116
+ slow_d_points = calculate_d_points(k_points, d_period)
117
+ k_d_points(k_points, slow_d_points)
118
+ end
119
+
120
+ def calculate_fast_stochastic_k_points(period)
121
+ index = 0
122
+ points = []
123
+ while((index + period) <= @stock_data.close_prices.size)
124
+ today_cp = @stock_data.close_prices[index + period - 1]
125
+ low_price = get_for_period(@stock_data.low_prices, index, index + period - 1, :min)
126
+ high_price = get_for_period(@stock_data.high_prices, index, index + period - 1, :max)
127
+ points << (today_cp - low_price) / (high_price - low_price)
128
+ index += 1
129
+ end
130
+ points
131
+ end
132
+
133
+ def calculate_d_points(k_points, period)
134
+ collection_for_array(k_points, period, :average)
135
+ end
136
+
137
+ def k_d_points(k_points, d_points)
138
+ raise unless k_points.size > d_points.size
139
+ SignalTools.truncate_to_shortest!(k_points, d_points)
140
+ {:k => k_points, :d => d_points}
141
+ end
142
+
143
+ def slow_k_points(fast_k_points)
144
+ collection_for_array(fast_k_points, Slow_K_SMA, :average)
145
+ end
146
+
147
+ #### True Range Methods
148
+
149
+ # Takes a smoothing period and historical data and calculates the average
150
+ # true ranges.
151
+ def average_true_ranges(period)
152
+ trs = true_ranges
153
+ atrs = [default_simple_average(trs.slice!(0...ATR_Seed_Days), ATR_Seed_Days)]
154
+ trs.each { |tr| atrs << calculate_average_true_range(atrs.last, tr, period) }
155
+ atrs
156
+ end
157
+
158
+ # Takes historical data and computes the true ranges.
159
+ def true_ranges
160
+ trs = [@stock_data.high_prices.first - @stock_data.low_prices.first]
161
+ index = 1
162
+ while index < (@stock_data.high_prices.size)
163
+ trs << true_range(@stock_data.raw_data[index], @stock_data.raw_data[index-1])
164
+ index += 1
165
+ end
166
+ trs
167
+ end
168
+
169
+ # Takes today's data and yesterday's data and computes the true range.
170
+ def true_range(today, yesterday)
171
+ [
172
+ today[SignalTools::StockData::Indexes[:high]] - today[SignalTools::StockData::Indexes[:low]],
173
+ (yesterday[SignalTools::StockData::Indexes[:close]] - today[SignalTools::StockData::Indexes[:high]]).abs,
174
+ (yesterday[SignalTools::StockData::Indexes[:close]] - today[SignalTools::StockData::Indexes[:low]]).abs
175
+ ].max
176
+ end
177
+
178
+ # Takes yesterday's average true range, today's true range, and the smoothing
179
+ # period and calculates the day's average true range.
180
+ def calculate_average_true_range(yesterday_atr, today_tr, period)
181
+ (yesterday_atr * (period - 1) + today_tr) / period
182
+ end
183
+
184
+ #### Average Directional Index Methods
185
+
186
+ def average_directional_indexes(period)
187
+ dxs = directional_indexes(plus_directional_index(period), minus_directional_index(period))
188
+ adxs = ema_points(period, dxs, :wilder)
189
+ adxs
190
+ end
191
+
192
+ def directional_indexes(plus_dis, minus_dis)
193
+ SignalTools.truncate_to_shortest!(plus_dis, minus_dis)
194
+ differences, sums = [], []
195
+ index = 0
196
+ while index < plus_dis.size
197
+ differences << (plus_dis[index] - minus_dis[index]).abs
198
+ sums << (plus_dis[index] + minus_dis[index])
199
+ index += 1
200
+ end
201
+ quotients(differences, sums)
202
+ end
203
+
204
+ def plus_directional_index(period)
205
+ plus_dms = plus_directional_movement(@stock_data.raw_data)
206
+ plus_dm_sums = period_sum_ema(period, plus_dms)
207
+ true_range_sums = period_sum_ema(period, true_ranges)
208
+ quotients(plus_dm_sums, true_range_sums)
209
+ end
210
+
211
+ def minus_directional_index(period)
212
+ minus_dms = minus_directional_movement(@stock_data.raw_data)
213
+ minus_dm_sums = period_sum_ema(period, minus_dms)
214
+ true_range_sums = period_sum_ema(period, true_ranges)
215
+ quotients(minus_dm_sums, true_range_sums)
216
+ end
217
+
218
+ def quotients(first, second)
219
+ SignalTools.truncate_to_shortest!(first, second)
220
+ index = 0
221
+ quots = []
222
+ while index < first.size
223
+ quots << first[index] / second[index]
224
+ index += 1
225
+ end
226
+ quots
227
+ end
228
+
229
+ def plus_directional_movement(data)
230
+ plus_dm = []
231
+ data.each_cons(2) do |two_days|
232
+ um = up_move(two_days.last, two_days.first)
233
+ dm = down_move(two_days.last, two_days.first)
234
+ plus_dm << ((um > dm) ? um : 0)
235
+ end
236
+ plus_dm
237
+ end
238
+
239
+ def minus_directional_movement(data)
240
+ minus_dm = []
241
+ data.each_cons(2) do |two_days|
242
+ um = up_move(two_days.last, two_days.first)
243
+ dm = down_move(two_days.last, two_days.first)
244
+ minus_dm << ((dm > um) ? dm : 0)
245
+ end
246
+ minus_dm
247
+ end
248
+
249
+ #TODO: Pass in only the high prices to this method
250
+ # Up move is today_high - yesterday_high
251
+ def up_move(today, yesterday)
252
+ diff = today[SignalTools::StockData::Indexes[:high]] - yesterday[SignalTools::StockData::Indexes[:high]]
253
+ diff > 0 ? diff : 0
254
+ end
255
+
256
+ #TODO: Pass in only the low prices to this method
257
+ # Down move is yesterday_low - today_low
258
+ def down_move(today, yesterday)
259
+ diff = yesterday[SignalTools::StockData::Indexes[:low]] - today[SignalTools::StockData::Indexes[:low]]
260
+ diff > 0 ? diff : 0
261
+ end
262
+
263
+ #### Misc Utility Methods
264
+
265
+ # Returns only the points specific to the date range given.
266
+ def trim_data_to_range(data)
267
+ if data.is_a? Array
268
+ data.slice!(0...(-dates.size))
269
+ elsif data.is_a? Hash
270
+ data.each { |k,v| v = v.slice!(0...(-dates.size)) }
271
+ end
272
+ data
273
+ end
274
+
275
+ # Gets the first 0...period of numbers from data and returns a simple average.
276
+ def default_simple_average(data, period)
277
+ SignalTools.average(data.slice(0...period))
278
+ end
279
+
280
+ #Runs method for the given slice of the array.
281
+ def get_for_period(points, start, finish, method)
282
+ case method
283
+ when :average
284
+ SignalTools.average(points.slice(start..finish))
285
+ else
286
+ (points.slice(start..finish)).send(method)
287
+ end
288
+ end
289
+
290
+ #Returns a collection of values by iterating over an array, slicing it period
291
+ # elements at a time and calling method for each slice.
292
+ def collection_for_array(points, period, method)
293
+ raise unless points.size >= period
294
+ collection = []
295
+ index = 0
296
+ while((index + period - 1) < points.size)
297
+ collection << get_for_period(points, index, (index + period - 1), method)
298
+ index += 1
299
+ end
300
+ collection
301
+ end
302
+
303
+ def matching_dates(array)
304
+ dates = @stock_data.dates.dup
305
+ SignalTools.truncate_to_shortest!(dates, array)
306
+ @dates = dates
307
+ end
308
+ end
309
+ end
@@ -0,0 +1,75 @@
1
+ require 'yahoofinance'
2
+
3
+ module SignalTools
4
+ class StockData
5
+ # Extra days needed to produce accurate data for the desired date.
6
+ Extra_Days = 365
7
+ Indexes = {
8
+ :date => 0,
9
+ :open => 1,
10
+ :high => 2,
11
+ :low => 3,
12
+ :close => 4,
13
+ #Presently unused
14
+ # :volume => 5,
15
+ # :adjusted_close => 6
16
+ }
17
+ attr_reader :raw_data, :dates
18
+
19
+ #Downloads historical prices using the YahooFinance gem.
20
+ def initialize(ticker, from_date, to_date)
21
+ @from_date = from_date
22
+ @raw_data = YahooFinance::get_historical_quotes(ticker, @from_date-Extra_Days, to_date).reverse
23
+ convert_raw_data_strings!
24
+ # We will never have need of the extraneous dates so we trim here
25
+ @dates = trim_dates
26
+ end
27
+
28
+ def open_prices
29
+ @open_prices ||= @raw_data.map { |d| d[Indexes[:open]] }
30
+ end
31
+
32
+ def high_prices
33
+ @high_prices ||= @raw_data.map { |d| d[Indexes[:high]] }
34
+ end
35
+
36
+ def low_prices
37
+ @low_prices ||= @raw_data.map { |d| d[Indexes[:low]] }
38
+ end
39
+
40
+ def close_prices
41
+ @close_prices ||= @raw_data.map { |d| d[Indexes[:close]] }
42
+ end
43
+
44
+ private
45
+
46
+ def convert_raw_data_strings!(*indexes)
47
+ @raw_data.each do |datum|
48
+ datum[Indexes[:date]] = Date.parse(datum[Indexes[:date]])
49
+ datum[Indexes[:open]] = datum[Indexes[:open]].to_f
50
+ datum[Indexes[:high]] = datum[Indexes[:high]].to_f
51
+ datum[Indexes[:low]] = datum[Indexes[:low]].to_f
52
+ datum[Indexes[:close]] = datum[Indexes[:close]].to_f
53
+ end
54
+ end
55
+
56
+ def trim_dates
57
+ dates = @raw_data.map { |d| d[Indexes[:date]] }
58
+ index = binary_search_for_date_index(dates)
59
+ dates[index..-1]
60
+ end
61
+
62
+ # Performs a binary search for @from_date on @dates. Returns the index of @from_date.
63
+ def binary_search_for_date_index(dates, low=0, high=dates.size-1)
64
+ return low if high <= low # closest match
65
+ mid = low + (high - low) / 2
66
+ if dates[mid] > @from_date
67
+ binary_search_for_date_index(dates, low, mid-1)
68
+ elsif dates[mid] < @from_date
69
+ binary_search_for_date_index(dates, mid+1, high)
70
+ else
71
+ mid
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path("../signal_tools/stock_data", __FILE__)
2
+ require File.expand_path("../signal_tools/stock", __FILE__)
3
+
4
+ module SignalTools
5
+ VERSION = '0.2.0'
6
+
7
+ def self.sum(array)
8
+ array.inject(0) {|accum, c| accum + c.to_f }
9
+ end
10
+
11
+ def self.average(array)
12
+ return nil if !array || array.size == 0
13
+ sum(array).to_f / array.size
14
+ end
15
+
16
+ # Truncates all arrays to the size of the shortest array by cutting off the front
17
+ # of the longer arrays.
18
+ def self.truncate_to_shortest!(*arrays)
19
+ shortest_size = arrays.inject(arrays.first.size) { |size, array| array.size < size ? array.size : size }
20
+ arrays.each { |array| array.slice!(0...(array.size - shortest_size)) }
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'flexmock/test_unit'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'signal_tools'
8
+
9
+ class Test::Unit::TestCase
10
+ def data_for_tests(period)
11
+ repeat = 5
12
+ historical_data = []
13
+ (0...SignalTools::StockData::Extra_Days+period).each do |i|
14
+ seed = i % repeat + 1
15
+ historical_data << [
16
+ (Date.today-i).to_s,
17
+ (seed * 0.8).to_s, #Open
18
+ (seed * 1.5).to_s, #High
19
+ (seed * 0.5).to_s, #Low
20
+ (seed * 0.9).to_s #Close
21
+ ]
22
+ end
23
+ historical_data
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ require 'test_helper'
2
+ require 'signal_tools'
3
+
4
+ class TestSignalTools < Test::Unit::TestCase
5
+ def setup
6
+ @array1 = [1,2,3,4,5,6,7,8,9,10]
7
+ end
8
+
9
+ def test_sum_returns_the_correct_sum_of_array_elements
10
+ assert_equal(55, SignalTools::sum(@array1))
11
+ end
12
+
13
+ def test_average_returns_the_correct_average_of_array_elements
14
+ assert_equal(5.5, SignalTools::average(@array1))
15
+ end
16
+
17
+ def test_truncate_to_shortest_returns_two_arrays_of_equal_size
18
+ array2 = [1,2,3]
19
+ SignalTools::truncate_to_shortest!(@array1, array2)
20
+ assert_equal(@array1.size, array2.size)
21
+ end
22
+ end
@@ -0,0 +1,76 @@
1
+ require 'test_helper'
2
+ require 'signal_tools'
3
+
4
+ class TestStock < Test::Unit::TestCase
5
+ def setup
6
+ ticker = "TESTING"
7
+ @days = 90
8
+ flexmock(YahooFinance).should_receive(:get_historical_quotes).with_any_args.and_return(data_for_tests(@days))
9
+ @stock = SignalTools::Stock.new(ticker)
10
+ end
11
+
12
+ def test_ema
13
+ assert_equal "2.344948", "%.6f" % @stock.ema[-1]
14
+ assert_equal "2.736776", "%.6f" % @stock.ema[-5]
15
+ assert_equal "2.556322", "%.6f" % @stock.ema(25)[-1]
16
+ assert_equal "2.705835", "%.6f" % @stock.ema(25)[-5]
17
+ end
18
+
19
+ def test_macd
20
+ assert_equal "-0.034645", "%.6f" % @stock.macd[:signal_points][-1]
21
+ assert_equal "-0.018762", "%.6f" % @stock.macd[:signal_points][-5]
22
+ assert_equal "-0.195043", "%.6f" % @stock.macd[:divergences][-1]
23
+ assert_equal "0.063532", "%.6f" % @stock.macd[:divergences][-5]
24
+
25
+ assert_equal "-0.022654", "%.6f" % @stock.macd(12, 26, 9)[:signal_points][-1]
26
+ assert_equal "-0.014099", "%.6f" % @stock.macd(12, 26, 9)[:signal_points][-5]
27
+ assert_equal "-0.136291", "%.6f" % @stock.macd(12, 26, 9)[:divergences][-1]
28
+ assert_equal "0.034219", "%.6f" % @stock.macd(12, 26, 9)[:divergences][-5]
29
+ end
30
+
31
+ def test_fast_stochastic
32
+ assert_equal "0.057143", "%.6f" % @stock.fast_stochastic[:k][-1]
33
+ assert_equal "0.571429", "%.6f" % @stock.fast_stochastic[:k][-5]
34
+ assert_equal "0.314286", "%.6f" % @stock.fast_stochastic[:d][-1]
35
+ assert_equal "0.314286", "%.6f" % @stock.fast_stochastic[:d][-5]
36
+
37
+ assert_equal "0.057143", "%.6f" % @stock.fast_stochastic(12, 3)[:k][-1]
38
+ assert_equal "0.571429", "%.6f" % @stock.fast_stochastic(12, 3)[:k][-5]
39
+ assert_equal "0.185714", "%.6f" % @stock.fast_stochastic(12, 3)[:d][-1]
40
+ assert_equal "0.271429", "%.6f" % @stock.fast_stochastic(12, 3)[:d][-5]
41
+ end
42
+
43
+ def test_slow_stochastic
44
+ assert_equal "0.185714", "%.6f" % @stock.slow_stochastic[:k][-1]
45
+ assert_equal "0.271429", "%.6f" % @stock.slow_stochastic[:k][-5]
46
+ assert_equal "0.314286", "%.6f" % @stock.slow_stochastic[:d][-1]
47
+ assert_equal "0.314286", "%.6f" % @stock.slow_stochastic[:d][-5]
48
+
49
+ assert_equal "0.185714", "%.6f" % @stock.slow_stochastic(12, 3)[:k][-1]
50
+ assert_equal "0.271429", "%.6f" % @stock.slow_stochastic(12, 3)[:k][-5]
51
+ assert_equal "0.314286", "%.6f" % @stock.slow_stochastic(12, 3)[:d][-1]
52
+ assert_equal "0.257143", "%.6f" % @stock.slow_stochastic(12, 3)[:d][-5]
53
+ end
54
+
55
+ def test_atr
56
+ assert_equal "3.195750", "%.6f" % @stock.atr[-1]
57
+ assert_equal "3.438910", "%.6f" % @stock.atr[-5]
58
+ assert_equal "3.208282", "%.6f" % @stock.atr(15)[-1]
59
+ assert_equal "3.434397", "%.6f" % @stock.atr(15)[-5]
60
+ end
61
+
62
+ def test_adx
63
+ assert_equal "0.491321", "%.6f" % @stock.adx[-1]
64
+ assert_equal "0.496588", "%.6f" % @stock.adx[-5]
65
+ end
66
+
67
+ def test_stock_should_have_correct_number_of_data_elements
68
+ assert_equal(@days+1, @stock.dates.size)
69
+ assert_equal(@days+1, @stock.ema.size)
70
+ assert_equal(@days+1, @stock.macd[:divergences].size)
71
+ assert_equal(@days+1, @stock.fast_stochastic[:k].size)
72
+ assert_equal(@days+1, @stock.slow_stochastic[:k].size)
73
+ assert_equal(@days+1, @stock.atr.size)
74
+ assert_equal(@days+1, @stock.adx.size)
75
+ end
76
+ end
@@ -0,0 +1,40 @@
1
+ require 'test_helper'
2
+ require 'signal_tools'
3
+
4
+ class TestStockData < Test::Unit::TestCase
5
+ def setup
6
+ ticker = "TESTING"
7
+ @days = 90
8
+ @total_days = @days + SignalTools::StockData::Extra_Days
9
+ @from_date = Date.today - @days
10
+ @to_date = Date.today
11
+ flexmock(YahooFinance).should_receive(:get_historical_quotes).with_any_args.and_return(data_for_tests(@days))
12
+ @stock_data = SignalTools::StockData.new(ticker, @from_date, @to_date)
13
+ end
14
+
15
+ def test_dates
16
+ new_dates = []
17
+ (0..@days).each { |i| new_dates.unshift((@to_date - i)) }
18
+ assert_equal new_dates, @stock_data.dates
19
+ end
20
+
21
+ def test_open_prices
22
+ result = @stock_data.open_prices.delete_if { |e| !e.is_a?(Float) }
23
+ assert_equal @total_days, result.size
24
+ end
25
+
26
+ def test_high_prices
27
+ result = @stock_data.high_prices.delete_if { |e| !e.is_a?(Float) }
28
+ assert_equal @total_days, result.size
29
+ end
30
+
31
+ def test_low_prices
32
+ result = @stock_data.low_prices.delete_if { |e| !e.is_a?(Float) }
33
+ assert_equal @total_days, result.size
34
+ end
35
+
36
+ def test_close_prices
37
+ result = @stock_data.close_prices.delete_if { |e| !e.is_a?(Float) }
38
+ assert_equal @total_days, result.size
39
+ end
40
+ end
data/test/test_tickers ADDED
@@ -0,0 +1 @@
1
+ JOSB DECK INFY NJ UA STEC VLCM CAAS QCOM CMG BLUD CAM CTSH AFAM DRQ SYNA STRA JCG ARO MIDD RMD ISRG WOOF CMTL MANT PCLN GME GPRO COH HS MCRS BOLT FLIR ABT
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signal_tools
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Matt White
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-11-05 00:00:00 -06:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rake
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: yahoofinance
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :runtime
45
+ version_requirements: *id002
46
+ description: Gem to create technical analysis data for a given security (like MACD, stochastic, and exponential moving averages).
47
+ email: mattw922@gmail.com
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files: []
53
+
54
+ files:
55
+ - Rakefile
56
+ - lib/signal_tools/stock.rb
57
+ - lib/signal_tools/stock_data.rb
58
+ - lib/signal_tools.rb
59
+ - test/test_signal_tools.rb
60
+ - test/test_tickers
61
+ - test/test_helper.rb
62
+ - test/test_stock_data.rb
63
+ - test/test_stock.rb
64
+ - README.rdoc
65
+ - LICENSE
66
+ has_rdoc: true
67
+ homepage: http://github.com/whitethunder/signal_tools
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ segments:
89
+ - 0
90
+ version: "0"
91
+ requirements: []
92
+
93
+ rubyforge_project:
94
+ rubygems_version: 1.3.7
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: Create technical analysis data for a given stock.
98
+ test_files: []
99
+