indicators 0.0.1
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/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +93 -0
- data/Rakefile +6 -0
- data/indicators.gemspec +21 -0
- data/lib/indicators/data.rb +45 -0
- data/lib/indicators/main.rb +305 -0
- data/lib/indicators/parser.rb +27 -0
- data/lib/indicators/version.rb +3 -0
- data/lib/indicators.rb +8 -0
- data/spec/data_spec.rb +28 -0
- data/spec/spec_helper.rb +20 -0
- metadata +116 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2012 Nedomas
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Indicators
|
|
2
|
+
|
|
3
|
+
A gem for calculating technical analysis indicators.
|
|
4
|
+
|
|
5
|
+
[](http://travis-ci.org/Nedomas/indicators)
|
|
6
|
+
|
|
7
|
+
Current functionality demo of Indicators and Securities gems synergy can be tested at http://strangemuseum.heroku.com.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add this line to your application's Gemfile:
|
|
12
|
+
|
|
13
|
+
gem 'indicators'
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install indicators
|
|
22
|
+
|
|
23
|
+
## Accepted Input
|
|
24
|
+
|
|
25
|
+
# Array
|
|
26
|
+
|
|
27
|
+
my_data = Indicators::Data.new([1, 2, 3, 4, 5])
|
|
28
|
+
|
|
29
|
+
Then it returns data as an array with indicator values in index places:
|
|
30
|
+
|
|
31
|
+
i.e. my_data.calc(:type => :sma, :variables => 2) => [nil, 1.5, 2.5, 3.5, 4.5]
|
|
32
|
+
|
|
33
|
+
# Securities gem hash
|
|
34
|
+
|
|
35
|
+
my_data = Indicators::Data.new(Securities::Stock.new(["aapl", "yhoo"]).history(:start_date => '2012-08-25', :end_date => '2012-08-30').results)
|
|
36
|
+
|
|
37
|
+
Then it returns a hash so it can be quite powerful at calculating multiple stock indicators at once:
|
|
38
|
+
|
|
39
|
+
{"aapl"=>[nil, 675.24, 674.0600000000001], "yhoo"=>[nil, 14.785, 14.821666666666667]}
|
|
40
|
+
|
|
41
|
+
## Supported Indicators
|
|
42
|
+
|
|
43
|
+
# Simple Moving Average => :sma
|
|
44
|
+
|
|
45
|
+
my_data.calc(:type => :sma, :variables => 5)
|
|
46
|
+
|
|
47
|
+
# Exponental Moving Average => :ema
|
|
48
|
+
|
|
49
|
+
my_data.calc(:type => :ema, :variables => 5)
|
|
50
|
+
|
|
51
|
+
# Bollinger Bands => :bb
|
|
52
|
+
|
|
53
|
+
my_data.calc(:type => :bb, :variables => [15, 3])
|
|
54
|
+
|
|
55
|
+
Variables have to be specified as an array [periods, multiplier]. If multiplier isn't specified, it defaults to 2.
|
|
56
|
+
|
|
57
|
+
It returns output as an array for each data point [middle band, upper band, lower band].
|
|
58
|
+
i.e. my_data.calc(:type => :bb, :variables => 3) => {"aapl"=>[nil, nil, [674.65, 676.8752190903368, 672.4247809096631]]}
|
|
59
|
+
|
|
60
|
+
# Moving Average Convergence Divergence => :macd
|
|
61
|
+
|
|
62
|
+
my_data.calc(:type => :macd, :variables => [12, 26, 9])
|
|
63
|
+
|
|
64
|
+
Variables have to be specified as an array [faster periods, slower periods, signal line]. If slower periods isn't specified, it defaults to 26 and signal line to 9.
|
|
65
|
+
|
|
66
|
+
MACD output is [MACD line, signal line, MACD histogram].
|
|
67
|
+
|
|
68
|
+
# Relative Strength Index => :rsi
|
|
69
|
+
|
|
70
|
+
my_data.calc(:type => :rsi, :variables => 14)
|
|
71
|
+
|
|
72
|
+
The more data it has, the more accurate RSI is.
|
|
73
|
+
|
|
74
|
+
## To Do
|
|
75
|
+
|
|
76
|
+
* Make defaults mechanism more versatile.
|
|
77
|
+
* Write specs.
|
|
78
|
+
* More validations.
|
|
79
|
+
* A strategy backtesting tool.
|
|
80
|
+
|
|
81
|
+
# Indicators:
|
|
82
|
+
* More moving averages (CMA, WMA, MMA).
|
|
83
|
+
* ROC.
|
|
84
|
+
* CCI.
|
|
85
|
+
* Stochastics.
|
|
86
|
+
|
|
87
|
+
## Contributing
|
|
88
|
+
|
|
89
|
+
1. Fork it
|
|
90
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
91
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
|
92
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
93
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/indicators.gemspec
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/indicators/version', __FILE__)
|
|
3
|
+
|
|
4
|
+
Gem::Specification.new do |gem|
|
|
5
|
+
gem.authors = ["Nedomas"]
|
|
6
|
+
gem.email = ["domas.bitvinskas@me.com"]
|
|
7
|
+
gem.description = %q{A gem for calculating technical analysis indicators.}
|
|
8
|
+
gem.summary = %q{A gem for calculating technical analysis indicators.}
|
|
9
|
+
gem.homepage = "http://github.com/Nedomas/indicators"
|
|
10
|
+
|
|
11
|
+
gem.files = `git ls-files`.split($\)
|
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
14
|
+
gem.name = "indicators"
|
|
15
|
+
gem.require_paths = ["lib"]
|
|
16
|
+
gem.version = Indicators::VERSION
|
|
17
|
+
|
|
18
|
+
gem.add_dependency 'rails'
|
|
19
|
+
gem.add_development_dependency 'securities'
|
|
20
|
+
gem.add_development_dependency 'rspec'
|
|
21
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
class Data
|
|
3
|
+
|
|
4
|
+
attr_reader :data, :results
|
|
5
|
+
INDICATORS = [:sma, :ema, :bb, :macd, :rsi]
|
|
6
|
+
# Error handling
|
|
7
|
+
class DataException < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize parameters
|
|
11
|
+
@data = parameters
|
|
12
|
+
|
|
13
|
+
# Check if data usable.
|
|
14
|
+
if @data.nil? || @data.empty?
|
|
15
|
+
raise DataException, "There is no data to work on."
|
|
16
|
+
end
|
|
17
|
+
unless @data.is_a?(Array) or @data.is_a?(Hash)
|
|
18
|
+
raise DataException, "Alien data. Given data must be an array or a hash (got #{@data.class})."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if @data.is_a?(Hash)
|
|
22
|
+
# Hacky, but a fast way to check if this is a dividends hash without the extra hassle.
|
|
23
|
+
raise DataException, "Cannot use dividends values for technical analysis." if @data.to_s.include?(':dividends')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def calc parameters
|
|
28
|
+
# Check is parameters are usable.
|
|
29
|
+
unless parameters.is_a?(Hash)
|
|
30
|
+
raise DataException, 'Given parameters have to be a hash. FORMAT: .calc(:type => :ema, :variables => 12)'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# If not specified, set default :type to :sma.
|
|
34
|
+
parameters[:type] = :sma if parameters[:type].nil? or parameters[:type].empty?
|
|
35
|
+
|
|
36
|
+
# Check if there is such indicator type supported.
|
|
37
|
+
case
|
|
38
|
+
when INDICATORS.include?(parameters[:type]) then @results = Indicators::Main.calculate(@data, parameters)
|
|
39
|
+
else
|
|
40
|
+
raise DataException, "Invalid indicator type specified (#{parameters[:type]})."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Moving averages
|
|
4
|
+
class Main
|
|
5
|
+
|
|
6
|
+
# Error handling.
|
|
7
|
+
class MainException < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.calculate data, parameters
|
|
11
|
+
if data.is_a?(Hash)
|
|
12
|
+
results = Hash.new
|
|
13
|
+
data.each do |symbol, stock_data|
|
|
14
|
+
# Check if this symbol was empty and don't go further with it.
|
|
15
|
+
if stock_data.length == 0
|
|
16
|
+
results[symbol] = []
|
|
17
|
+
else
|
|
18
|
+
results[symbol] = case parameters[:type]
|
|
19
|
+
when :sma then sma(Indicators::Parser.parse_data(stock_data), parameters[:variables])
|
|
20
|
+
when :ema then ema(Indicators::Parser.parse_data(stock_data), parameters[:variables])
|
|
21
|
+
when :bb then bb(Indicators::Parser.parse_data(stock_data), parameters[:variables])
|
|
22
|
+
when :macd then macd(Indicators::Parser.parse_data(stock_data), parameters[:variables])
|
|
23
|
+
when :rsi then rsi(Indicators::Parser.parse_data(stock_data), parameters[:variables])
|
|
24
|
+
end
|
|
25
|
+
# Parser returns in {:date=>[2012.0, 2012.0, 2012.0], :open=>[409.4, 410.0, 414.95],} format
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
results = case parameters[:type]
|
|
30
|
+
when :sma then sma(data, parameters[:variables])
|
|
31
|
+
when :ema then ema(data, parameters[:variables])
|
|
32
|
+
when :bb then bb(data, parameters[:variables])
|
|
33
|
+
when :macd then macd(data, parameters[:variables])
|
|
34
|
+
when :rsi then rsi(data, parameters[:variables])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
return results
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def self.get_data data, variables, column
|
|
43
|
+
# If this is a hash, choose which column of values to use for calculations.
|
|
44
|
+
if data.is_a?(Hash)
|
|
45
|
+
usable_data = data[column]
|
|
46
|
+
else
|
|
47
|
+
usable_data = data
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if usable_data.length < variables
|
|
51
|
+
raise MainException, "Data point length (#{usable_data.length}) must be greater or equal to the needed periods value (#{variables})."
|
|
52
|
+
end
|
|
53
|
+
return usable_data
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.get_variables variables, i=0, default=0
|
|
57
|
+
if variables.is_a?(Array)
|
|
58
|
+
# In case array is given not like [1, 2] but like this ["1", "2"]. This usually happens when getting data from input forms.
|
|
59
|
+
variables = variables.map(&:to_i)
|
|
60
|
+
if variables.length < 2
|
|
61
|
+
return default if i != 0
|
|
62
|
+
return variables[0]
|
|
63
|
+
else
|
|
64
|
+
if variables[i].nil? then return default else return variables[i] end
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
return default if i != 0
|
|
68
|
+
return variables
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
# Lagging indicators
|
|
74
|
+
#
|
|
75
|
+
|
|
76
|
+
# Simple Moving Average
|
|
77
|
+
def self.sma data, variables
|
|
78
|
+
periods = get_variables(variables)
|
|
79
|
+
usable_data = Array.new
|
|
80
|
+
usable_data = get_data(data, periods, :adj_close)
|
|
81
|
+
|
|
82
|
+
# Just the calculation of SMA by the formula.
|
|
83
|
+
sma = []
|
|
84
|
+
usable_data.each_with_index do |value, index|
|
|
85
|
+
from = index-periods+1
|
|
86
|
+
if from >= 0
|
|
87
|
+
sum = usable_data[from..index].sum
|
|
88
|
+
sma[index] = (sum/periods.to_f)
|
|
89
|
+
else
|
|
90
|
+
sma[index] = nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
return sma
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# Exponential Moving Average
|
|
98
|
+
|
|
99
|
+
# Multiplier: (2 / (Time periods + 1) ) = (2 / (10 + 1) ) = 0.1818 (18.18%)
|
|
100
|
+
# EMA: {Close - EMA(previous day)} x multiplier + EMA(previous day).
|
|
101
|
+
def self.ema data, variables
|
|
102
|
+
periods = get_variables(variables)
|
|
103
|
+
usable_data = Array.new
|
|
104
|
+
usable_data = get_data(data, periods, :adj_close)
|
|
105
|
+
|
|
106
|
+
ema = []
|
|
107
|
+
k = 2/(periods+1).to_f
|
|
108
|
+
usable_data.each_with_index do |value, index|
|
|
109
|
+
from = index+1-periods
|
|
110
|
+
if from == 0
|
|
111
|
+
ema[index] = sma(usable_data[from..index], periods).last
|
|
112
|
+
# puts ma
|
|
113
|
+
elsif from > 0
|
|
114
|
+
ema[index] = ((usable_data[index] - ema[index-1]) * k + ema[index-1])
|
|
115
|
+
else
|
|
116
|
+
ema[index] = nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
return ema
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
#
|
|
123
|
+
# Bollinger Bands :type => :bb, :variables => 20, 2)
|
|
124
|
+
|
|
125
|
+
# Middle Band = 20-day simple moving average (SMA)
|
|
126
|
+
# Upper Band = 20-day SMA + (20-day standard deviation of price x 2)
|
|
127
|
+
# Lower Band = 20-day SMA - (20-day standard deviation of price x 2)
|
|
128
|
+
def self.bb data, variables
|
|
129
|
+
periods = get_variables(variables)
|
|
130
|
+
default_multiplier = 2
|
|
131
|
+
multiplier = get_variables(variables, 1, default_multiplier)
|
|
132
|
+
|
|
133
|
+
usable_data = Array.new
|
|
134
|
+
usable_data = get_data(data, periods, :adj_close)
|
|
135
|
+
bb = []
|
|
136
|
+
usable_data.each_with_index do |value, index|
|
|
137
|
+
from = index-periods+1
|
|
138
|
+
if from >= 0
|
|
139
|
+
middle_band = sma(usable_data[from..index], periods).last
|
|
140
|
+
upper_band = middle_band + (usable_data[from..index].standard_deviation * multiplier)
|
|
141
|
+
lower_band = middle_band - (usable_data[from..index].standard_deviation * multiplier)
|
|
142
|
+
# output is [middle, upper, lower]
|
|
143
|
+
bb[index] = [middle_band, upper_band, lower_band]
|
|
144
|
+
else
|
|
145
|
+
bb[index] = nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
return bb
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
#
|
|
152
|
+
# Moving Average Convergence Divergence
|
|
153
|
+
|
|
154
|
+
# MACD Line: (12-day EMA - 26-day EMA)
|
|
155
|
+
# Signal Line: 9-day EMA of MACD Line
|
|
156
|
+
# MACD Histogram: MACD Line - Signal Line
|
|
157
|
+
# Default MACD(12, 26, 9)
|
|
158
|
+
def self.macd data, variables
|
|
159
|
+
faster_periods = get_variables(variables, 0, 12)
|
|
160
|
+
slower_periods = get_variables(variables, 1, 26)
|
|
161
|
+
signal_periods = get_variables(variables, 2, 9)
|
|
162
|
+
@variables = [faster_periods, slower_periods, signal_periods]
|
|
163
|
+
# puts "faster=#{faster_periods}, slower=#{slower_periods}, signal=#{signal_periods}"
|
|
164
|
+
|
|
165
|
+
usable_data = Array.new
|
|
166
|
+
usable_data = get_data(data, slower_periods+signal_periods-1, :adj_close)
|
|
167
|
+
macd = []
|
|
168
|
+
macd_line = []
|
|
169
|
+
|
|
170
|
+
usable_data.each_with_index do |value, index|
|
|
171
|
+
if index+1 >= slower_periods
|
|
172
|
+
# Calibrate me! Not sure why it doesn't accept from or from_faster.
|
|
173
|
+
faster_ema = ema(usable_data[0..index], faster_periods).last
|
|
174
|
+
slower_ema = ema(usable_data[0..index], slower_periods).last
|
|
175
|
+
macd_line[index] = faster_ema - slower_ema
|
|
176
|
+
if index+1 >= slower_periods + signal_periods
|
|
177
|
+
# I'm pretty sure this is right.
|
|
178
|
+
signal_line = ema(macd_line[(-signal_periods)..index], signal_periods).last
|
|
179
|
+
# Output is [MACD, Signal, MACD Hist]
|
|
180
|
+
macd_histogram = macd_line[index] - signal_line
|
|
181
|
+
macd[index] = [macd_line[index], signal_line, macd_histogram]
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
macd_line[index] = nil
|
|
185
|
+
macd[index] = nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
return macd
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
#
|
|
192
|
+
# Relative Strength Index
|
|
193
|
+
|
|
194
|
+
# 100
|
|
195
|
+
# RSI = 100 - --------
|
|
196
|
+
# 1 + RS
|
|
197
|
+
# RS = Average Gain / Average Loss
|
|
198
|
+
# First Average Gain = Sum of Gains over the past 14 periods / 14
|
|
199
|
+
# First Average Loss = Sum of Losses over the past 14 periods / 14
|
|
200
|
+
# Average Gain = [(previous Average Gain) x 13 + current Gain] / 14.
|
|
201
|
+
# Average Loss = [(previous Average Loss) x 13 + current Loss] / 14.
|
|
202
|
+
def self.rsi data, variables
|
|
203
|
+
periods = get_variables(variables)
|
|
204
|
+
usable_data = Array.new
|
|
205
|
+
usable_data = get_data(data, periods, :adj_close)
|
|
206
|
+
|
|
207
|
+
values = []
|
|
208
|
+
rsi = []
|
|
209
|
+
rs = []
|
|
210
|
+
average_gain = 0.0
|
|
211
|
+
average_loss = 0.0
|
|
212
|
+
usable_data.each_with_index do |value, index|
|
|
213
|
+
values[index] = value
|
|
214
|
+
if index >= periods
|
|
215
|
+
if index == periods
|
|
216
|
+
average_gain = gain(values) / periods
|
|
217
|
+
average_loss = loss(values) / periods
|
|
218
|
+
else
|
|
219
|
+
difference = value - values[index-1]
|
|
220
|
+
if difference >= 0
|
|
221
|
+
current_gain = difference
|
|
222
|
+
current_loss = 0
|
|
223
|
+
else
|
|
224
|
+
current_gain = 0
|
|
225
|
+
current_loss = difference.abs
|
|
226
|
+
end
|
|
227
|
+
average_gain = (average_gain * (periods-1) + current_gain) / periods
|
|
228
|
+
average_loss = (average_loss * (periods-1) + current_loss) / periods
|
|
229
|
+
end
|
|
230
|
+
rs[index] = average_gain / average_loss
|
|
231
|
+
rsi[index] = 100 - 100/(1+rs[index])
|
|
232
|
+
if average_gain == 0
|
|
233
|
+
rsi[index] = 0
|
|
234
|
+
elsif average_loss == 0
|
|
235
|
+
rsi[index] = 100
|
|
236
|
+
end
|
|
237
|
+
else
|
|
238
|
+
rs[index] = nil
|
|
239
|
+
rsi[index] = nil
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
return rsi
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
#
|
|
247
|
+
# Helper methods for RSI
|
|
248
|
+
def self.gain data
|
|
249
|
+
sum = 0.0
|
|
250
|
+
first_value = nil
|
|
251
|
+
data.each do |value|
|
|
252
|
+
if first_value == nil
|
|
253
|
+
first_value = value
|
|
254
|
+
else
|
|
255
|
+
if value > first_value
|
|
256
|
+
sum += value - first_value
|
|
257
|
+
end
|
|
258
|
+
first_value = value
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
return sum
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def self.loss data
|
|
265
|
+
sum = 0.0
|
|
266
|
+
first_value = nil
|
|
267
|
+
data.each do |value|
|
|
268
|
+
if first_value == nil
|
|
269
|
+
first_value = value
|
|
270
|
+
else
|
|
271
|
+
if value < first_value
|
|
272
|
+
sum += first_value - value
|
|
273
|
+
end
|
|
274
|
+
first_value = value
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
return sum
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
#
|
|
284
|
+
# Extra methods for mathematical calculations.
|
|
285
|
+
module Enumerable
|
|
286
|
+
|
|
287
|
+
def sum
|
|
288
|
+
return self.inject(0){|accum, i| accum + i }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def mean
|
|
292
|
+
return self.sum / self.length.to_f
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def sample_variance
|
|
296
|
+
m = self.mean
|
|
297
|
+
sum = self.inject(0){|accum, i| accum + (i - m) ** 2 }
|
|
298
|
+
return sum / (self.length - 1).to_f
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def standard_deviation
|
|
302
|
+
return Math.sqrt(self.sample_variance)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
|
|
3
|
+
# Class to parse a securities gem return hash.
|
|
4
|
+
class Parser
|
|
5
|
+
|
|
6
|
+
# Error handling.
|
|
7
|
+
class ParserException < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.parse_data parameters
|
|
11
|
+
|
|
12
|
+
usable_data = Hash.new
|
|
13
|
+
transposed_hash = Hash.new
|
|
14
|
+
# Such a hacky way to transpose an array.
|
|
15
|
+
# FIXME: Now v.to_f converts date to float, it shouldn't.
|
|
16
|
+
parameters.reverse.inject({}){|a, h|
|
|
17
|
+
h.each_pair{|k,v| (a[k] ||= []) << v.to_f}
|
|
18
|
+
transposed_hash = a
|
|
19
|
+
}
|
|
20
|
+
usable_data = transposed_hash
|
|
21
|
+
# usable data is are {:close => [1, 2, 3], :open => []}
|
|
22
|
+
|
|
23
|
+
return usable_data
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/indicators.rb
ADDED
data/spec/data_spec.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Data do
|
|
4
|
+
|
|
5
|
+
context "should raise an exception if" do
|
|
6
|
+
it "nil or empty" do
|
|
7
|
+
expect { Indicators::Data.new(nil) }.to raise_error
|
|
8
|
+
expect { Indicators::Data.new('') }.to raise_error
|
|
9
|
+
end
|
|
10
|
+
it "not an array or hash" do
|
|
11
|
+
expect { Indicators::Data.new('some string') }.to raise_error
|
|
12
|
+
end
|
|
13
|
+
it "contains dividends hash from securities gem" do
|
|
14
|
+
expect { Indicators::Data.new(Securities::Stock.new(["aapl"]).history(:start_date => '2012-08-01', :end_date => '2012-08-10', :periods => :dividends).results) }.to raise_error
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context "should not raise an exception if" do
|
|
19
|
+
it "is an array" do
|
|
20
|
+
expect { Indicators::Data.new([1, 2, 3]) }.not_to raise_error
|
|
21
|
+
end
|
|
22
|
+
it "is a hash" do
|
|
23
|
+
expect { Indicators::Data.new(Securities::Stock.new(["aapl"]).history(:start_date => '2012-08-01', :end_date => '2012-08-03', :periods => :daily).results) }.not_to raise_error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
|
4
|
+
# loaded once.
|
|
5
|
+
#
|
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
|
7
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
|
8
|
+
require 'indicators'
|
|
9
|
+
|
|
10
|
+
RSpec.configure do |config|
|
|
11
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
|
12
|
+
config.run_all_when_everything_filtered = true
|
|
13
|
+
config.filter_run :focus
|
|
14
|
+
|
|
15
|
+
# Run specs in random order to surface order dependencies. If you find an
|
|
16
|
+
# order dependency and want to debug it, you can fix the order by providing
|
|
17
|
+
# the seed, which is printed after each run.
|
|
18
|
+
# --seed 1234
|
|
19
|
+
config.order = 'random'
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: indicators
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Nedomas
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-09-01 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: rails
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ! '>='
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '0'
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: securities
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
none: false
|
|
34
|
+
requirements:
|
|
35
|
+
- - ! '>='
|
|
36
|
+
- !ruby/object:Gem::Version
|
|
37
|
+
version: '0'
|
|
38
|
+
type: :development
|
|
39
|
+
prerelease: false
|
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ! '>='
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rspec
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
none: false
|
|
50
|
+
requirements:
|
|
51
|
+
- - ! '>='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
none: false
|
|
58
|
+
requirements:
|
|
59
|
+
- - ! '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
description: A gem for calculating technical analysis indicators.
|
|
63
|
+
email:
|
|
64
|
+
- domas.bitvinskas@me.com
|
|
65
|
+
executables: []
|
|
66
|
+
extensions: []
|
|
67
|
+
extra_rdoc_files: []
|
|
68
|
+
files:
|
|
69
|
+
- .gitignore
|
|
70
|
+
- .rspec
|
|
71
|
+
- .travis.yml
|
|
72
|
+
- Gemfile
|
|
73
|
+
- LICENSE
|
|
74
|
+
- README.md
|
|
75
|
+
- Rakefile
|
|
76
|
+
- indicators.gemspec
|
|
77
|
+
- lib/indicators.rb
|
|
78
|
+
- lib/indicators/data.rb
|
|
79
|
+
- lib/indicators/main.rb
|
|
80
|
+
- lib/indicators/parser.rb
|
|
81
|
+
- lib/indicators/version.rb
|
|
82
|
+
- spec/data_spec.rb
|
|
83
|
+
- spec/spec_helper.rb
|
|
84
|
+
homepage: http://github.com/Nedomas/indicators
|
|
85
|
+
licenses: []
|
|
86
|
+
post_install_message:
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
none: false
|
|
92
|
+
requirements:
|
|
93
|
+
- - ! '>='
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
segments:
|
|
97
|
+
- 0
|
|
98
|
+
hash: 3875007528278561270
|
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
|
+
none: false
|
|
101
|
+
requirements:
|
|
102
|
+
- - ! '>='
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
segments:
|
|
106
|
+
- 0
|
|
107
|
+
hash: 3875007528278561270
|
|
108
|
+
requirements: []
|
|
109
|
+
rubyforge_project:
|
|
110
|
+
rubygems_version: 1.8.24
|
|
111
|
+
signing_key:
|
|
112
|
+
specification_version: 3
|
|
113
|
+
summary: A gem for calculating technical analysis indicators.
|
|
114
|
+
test_files:
|
|
115
|
+
- spec/data_spec.rb
|
|
116
|
+
- spec/spec_helper.rb
|