signal_tools 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +40 -0
- data/lib/signal_tools/stock.rb +309 -0
- data/lib/signal_tools/stock_data.rb +75 -0
- data/lib/signal_tools.rb +22 -0
- data/test/test_helper.rb +25 -0
- data/test/test_signal_tools.rb +22 -0
- data/test/test_stock.rb +76 -0
- data/test/test_stock_data.rb +40 -0
- data/test/test_tickers +1 -0
- metadata +99 -0
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
|
data/lib/signal_tools.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|
data/test/test_stock.rb
ADDED
@@ -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
|
+
|