indicators 0.1.0 → 1.0.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/README.md +18 -10
- data/lib/indicators.rb +12 -0
- data/lib/indicators/calculations/bb.rb +33 -0
- data/lib/indicators/calculations/ema.rb +31 -0
- data/lib/indicators/calculations/helper.rb +91 -0
- data/lib/indicators/calculations/macd.rb +45 -0
- data/lib/indicators/calculations/rsi.rb +81 -0
- data/lib/indicators/calculations/sma.rb +27 -0
- data/lib/indicators/calculations/sto.rb +54 -0
- data/lib/indicators/data.rb +2 -2
- data/lib/indicators/main.rb +45 -335
- data/lib/indicators/version.rb +1 -1
- metadata +11 -4
data/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Or install it yourself as:
|
|
|
28
28
|
|
|
29
29
|
Then it returns data as an array with indicator values in index places:
|
|
30
30
|
|
|
31
|
-
i.e. my_data.calc(:type => :sma, :
|
|
31
|
+
i.e. my_data.calc(:type => :sma, :params => 2).output => [nil, 1.5, 2.5, 3.5, 4.5]
|
|
32
32
|
|
|
33
33
|
# Securities gem hash
|
|
34
34
|
|
|
@@ -38,28 +38,38 @@ Then it returns a hash so it can be quite powerful at calculating multiple stock
|
|
|
38
38
|
|
|
39
39
|
{"aapl"=>[nil, 675.24, 674.0600000000001], "yhoo"=>[nil, 14.785, 14.821666666666667]}
|
|
40
40
|
|
|
41
|
+
## Output
|
|
42
|
+
|
|
43
|
+
.calc returns an object with such accessor methods
|
|
44
|
+
|
|
45
|
+
@abbr - indicator abbreviation (usually used when displaying information).
|
|
46
|
+
@params - given or defaulted parameters.
|
|
47
|
+
@output - indicator calculation result.
|
|
48
|
+
|
|
49
|
+
i.e. @abbr="SMA", @params=2, @output=[nil, 1.5, 2.5, 3.5, 4.5]
|
|
50
|
+
|
|
41
51
|
## Supported Indicators
|
|
42
52
|
|
|
43
53
|
# Simple Moving Average => :sma
|
|
44
54
|
|
|
45
|
-
my_data.calc(:type => :sma, :
|
|
55
|
+
my_data.calc(:type => :sma, :params => 5)
|
|
46
56
|
|
|
47
57
|
# Exponental Moving Average => :ema
|
|
48
58
|
|
|
49
|
-
my_data.calc(:type => :ema, :
|
|
59
|
+
my_data.calc(:type => :ema, :params => 5)
|
|
50
60
|
|
|
51
61
|
# Bollinger Bands => :bb
|
|
52
62
|
|
|
53
|
-
my_data.calc(:type => :bb, :
|
|
63
|
+
my_data.calc(:type => :bb, :params => [15, 3])
|
|
54
64
|
|
|
55
65
|
Variables have to be specified as an array [periods, multiplier]. If multiplier isn't specified, it defaults to 2.
|
|
56
66
|
|
|
57
67
|
It returns output as an array for each data point [middle band, upper band, lower band].
|
|
58
|
-
i.e. my_data.calc(:type => :bb, :
|
|
68
|
+
i.e. my_data.calc(:type => :bb, :params => 3) => {"aapl"=>[nil, nil, [674.65, 676.8752190903368, 672.4247809096631]]}
|
|
59
69
|
|
|
60
70
|
# Moving Average Convergence Divergence => :macd
|
|
61
71
|
|
|
62
|
-
my_data.calc(:type => :macd, :
|
|
72
|
+
my_data.calc(:type => :macd, :params => [12, 26, 9])
|
|
63
73
|
|
|
64
74
|
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
75
|
|
|
@@ -67,13 +77,13 @@ Variables have to be specified as an array [faster periods, slower periods, sign
|
|
|
67
77
|
|
|
68
78
|
# Relative Strength Index => :rsi
|
|
69
79
|
|
|
70
|
-
my_data.calc(:type => :rsi, :
|
|
80
|
+
my_data.calc(:type => :rsi, :params => 14)
|
|
71
81
|
|
|
72
82
|
The more data it has, the more accurate RSI is.
|
|
73
83
|
|
|
74
84
|
# Full Stochastic Oscillator => :sto
|
|
75
85
|
|
|
76
|
-
my_data.calc(:type => :sto, :
|
|
86
|
+
my_data.calc(:type => :sto, :params => [14, 3, 5])
|
|
77
87
|
|
|
78
88
|
Variables have to be specified as an array [lookback period, the number of periods to slow %K, the number of periods for the %D moving average] => [%K1, %K2, %D].
|
|
79
89
|
|
|
@@ -81,9 +91,7 @@ Variables have to be specified as an array [lookback period, the number of perio
|
|
|
81
91
|
|
|
82
92
|
## To Do
|
|
83
93
|
|
|
84
|
-
* Make defaults mechanism more versatile.
|
|
85
94
|
* Write specs.
|
|
86
|
-
* More validations.
|
|
87
95
|
* A strategy backtesting tool.
|
|
88
96
|
|
|
89
97
|
# Indicators:
|
data/lib/indicators.rb
CHANGED
|
@@ -4,5 +4,17 @@ require "indicators/data.rb"
|
|
|
4
4
|
require "indicators/parser.rb"
|
|
5
5
|
require "indicators/main.rb"
|
|
6
6
|
|
|
7
|
+
require "indicators/calculations/helper.rb"
|
|
8
|
+
|
|
9
|
+
# Lagging Indicators
|
|
10
|
+
require "indicators/calculations/sma.rb"
|
|
11
|
+
require "indicators/calculations/ema.rb"
|
|
12
|
+
require "indicators/calculations/bb.rb"
|
|
13
|
+
require "indicators/calculations/macd.rb"
|
|
14
|
+
|
|
15
|
+
# Leading Indicators
|
|
16
|
+
require "indicators/calculations/rsi.rb"
|
|
17
|
+
require "indicators/calculations/sto.rb"
|
|
18
|
+
|
|
7
19
|
module Indicators
|
|
8
20
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Bollinger Bands
|
|
4
|
+
class Bb
|
|
5
|
+
|
|
6
|
+
# Middle Band = 20-day simple moving average (SMA)
|
|
7
|
+
# Upper Band = 20-day SMA + (20-day standard deviation of price x 2)
|
|
8
|
+
# Lower Band = 20-day SMA - (20-day standard deviation of price x 2)
|
|
9
|
+
def self.calculate data, parameters
|
|
10
|
+
periods = parameters[0]
|
|
11
|
+
multiplier = parameters[1]
|
|
12
|
+
output = Array.new
|
|
13
|
+
adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods)
|
|
14
|
+
|
|
15
|
+
adj_closes.each_with_index do |adj_close, index|
|
|
16
|
+
start = index+1-periods
|
|
17
|
+
if index+1 >= periods
|
|
18
|
+
middle_band = Indicators::Sma.calculate(adj_closes[start..index], periods).last
|
|
19
|
+
upper_band = middle_band + (adj_closes[start..index].standard_deviation * multiplier)
|
|
20
|
+
lower_band = middle_band - (adj_closes[start..index].standard_deviation * multiplier)
|
|
21
|
+
# Output for each point is [middle, upper, lower].
|
|
22
|
+
output[index] = [middle_band, upper_band, lower_band]
|
|
23
|
+
else
|
|
24
|
+
output[index] = nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return output
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Exponential Moving Average
|
|
4
|
+
class Ema
|
|
5
|
+
|
|
6
|
+
# Multiplier: (2 / (Time periods + 1) ) = (2 / (10 + 1) ) = 0.1818 (18.18%)
|
|
7
|
+
# EMA: {Close - EMA(previous day)} x multiplier + EMA(previous day).
|
|
8
|
+
def self.calculate data, parameters
|
|
9
|
+
periods = parameters
|
|
10
|
+
output = Array.new
|
|
11
|
+
adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods)
|
|
12
|
+
|
|
13
|
+
k = 2.0/(periods+1)
|
|
14
|
+
adj_closes.each_with_index do |adj_close, index|
|
|
15
|
+
start = index+1-periods
|
|
16
|
+
if start == 0
|
|
17
|
+
output[index] = Indicators::Sma.calculate(adj_closes[start..index], periods).last
|
|
18
|
+
elsif start > 0
|
|
19
|
+
output[index] = ((adj_close - output[index-1]) * k + output[index-1])
|
|
20
|
+
else
|
|
21
|
+
output[index] = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return output
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
# Main helper methods
|
|
3
|
+
class Helper
|
|
4
|
+
|
|
5
|
+
# Error handling.
|
|
6
|
+
class HelperException < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.validate_data data, column, parameters
|
|
10
|
+
# If this is a hash, choose which column of values to use for calculations.
|
|
11
|
+
if data.is_a?(Hash)
|
|
12
|
+
valid_data = data[column]
|
|
13
|
+
else
|
|
14
|
+
valid_data = data
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Make output more friendly
|
|
18
|
+
# if parameters.is_a?(Array)
|
|
19
|
+
# parameters_array = parameters
|
|
20
|
+
# parameters = parameters.sum
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
if valid_data.length < parameters
|
|
24
|
+
raise HelperException, "Data point length (#{valid_data.length}) must be greater than or equal to the required indicator periods (#{parameters})."
|
|
25
|
+
end
|
|
26
|
+
return valid_data
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Indicators::Helper.get_parameters([12, 1, 1], 0, 15)
|
|
30
|
+
def self.get_parameters parameters, i=0, default=0
|
|
31
|
+
|
|
32
|
+
if parameters.is_a?(Integer) || parameters.is_a?(NilClass)
|
|
33
|
+
|
|
34
|
+
# Set all other to default if only one integer is given instead of an array.
|
|
35
|
+
return default if i != 0
|
|
36
|
+
|
|
37
|
+
# Check if no parameters are specified at all, if so => set to default.
|
|
38
|
+
# Parameters 15, 0, 0 are equal to 15 or 15, nil, nil.
|
|
39
|
+
if parameters == nil || parameters == 0
|
|
40
|
+
if default != 0
|
|
41
|
+
return default
|
|
42
|
+
else
|
|
43
|
+
raise HelperException, "There were no parameters specified and there is no default for it."
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
return parameters
|
|
47
|
+
end
|
|
48
|
+
elsif parameters.is_a?(Array)
|
|
49
|
+
# In case array is given not like [1, 2] but like this ["1", "2"]. This usually happens when getting data from input forms.
|
|
50
|
+
parameters = parameters.map(&:to_i)
|
|
51
|
+
if parameters[i] == nil || parameters[i] == 0
|
|
52
|
+
if default != 0
|
|
53
|
+
return default
|
|
54
|
+
else
|
|
55
|
+
raise HelperException, "There were no parameters specified and there is no default for it."
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
return parameters[i]
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
raise HelperException, "Parameters have to be a integer, an array or nil."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#
|
|
70
|
+
# Extra methods for mathematical calculations.
|
|
71
|
+
module Enumerable
|
|
72
|
+
|
|
73
|
+
def sum
|
|
74
|
+
return self.inject(0){|accum, i| accum + i }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def mean
|
|
78
|
+
return self.sum / self.length.to_f
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def sample_variance
|
|
82
|
+
m = self.mean
|
|
83
|
+
sum = self.inject(0){|accum, i| accum + (i - m) ** 2 }
|
|
84
|
+
return sum / (self.length - 1).to_f
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def standard_deviation
|
|
88
|
+
return Math.sqrt(self.sample_variance)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Moving Average Convergence Divergence
|
|
4
|
+
class Macd
|
|
5
|
+
|
|
6
|
+
# MACD Line: (12-day EMA - 26-day EMA)
|
|
7
|
+
# Signal Line: 9-day EMA of MACD Line
|
|
8
|
+
# MACD Histogram: MACD Line - Signal Line
|
|
9
|
+
# Default MACD(12, 26, 9)
|
|
10
|
+
def self.calculate data, parameters
|
|
11
|
+
faster_periods = parameters[0]
|
|
12
|
+
slower_periods = parameters[1]
|
|
13
|
+
signal_periods = parameters[2]
|
|
14
|
+
output = Array.new
|
|
15
|
+
adj_closes = Indicators::Helper.validate_data(data, :adj_close, slower_periods+signal_periods-1)
|
|
16
|
+
# puts "faster=#{faster_periods}, slower=#{slower_periods}, signal=#{signal_periods}"
|
|
17
|
+
|
|
18
|
+
macd_line = []
|
|
19
|
+
|
|
20
|
+
adj_closes.each_with_index do |adj_close, index|
|
|
21
|
+
if index+1 >= slower_periods
|
|
22
|
+
# Calibrate me! Not sure why it doesn't accept from or from_faster.
|
|
23
|
+
faster_ema = Indicators::Ema.calculate(adj_closes[0..index], faster_periods).last
|
|
24
|
+
slower_ema = Indicators::Ema.calculate(adj_closes[0..index], slower_periods).last
|
|
25
|
+
macd_line[index] = faster_ema - slower_ema
|
|
26
|
+
if index+1 >= slower_periods+signal_periods
|
|
27
|
+
signal_line = Indicators::Ema.calculate(macd_line[(-signal_periods)..index], signal_periods).last
|
|
28
|
+
# Output is [MACD, Signal, MACD Hist]
|
|
29
|
+
macd_histogram = macd_line[index] - signal_line
|
|
30
|
+
output[index] = [macd_line[index], signal_line, macd_histogram]
|
|
31
|
+
else
|
|
32
|
+
output[index] = [macd_line[index], nil, nil]
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
macd_line[index] = nil
|
|
36
|
+
output[index] = nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
return output
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Relative Strength Index
|
|
4
|
+
class Rsi
|
|
5
|
+
|
|
6
|
+
# 100
|
|
7
|
+
# RSI = 100 - --------
|
|
8
|
+
# 1 + RS
|
|
9
|
+
# RS = Average Gain / Average Loss
|
|
10
|
+
# First Average Gain = Sum of Gains over the past 14 periods / 14
|
|
11
|
+
# First Average Loss = Sum of Losses over the past 14 periods / 14
|
|
12
|
+
# Average Gain = [(previous Average Gain) x 13 + current Gain] / 14.
|
|
13
|
+
# Average Loss = [(previous Average Loss) x 13 + current Loss] / 14.
|
|
14
|
+
def self.calculate data, parameters
|
|
15
|
+
periods = parameters
|
|
16
|
+
output = Array.new
|
|
17
|
+
adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods)
|
|
18
|
+
|
|
19
|
+
rs = Array.new
|
|
20
|
+
average_gain = 0.0
|
|
21
|
+
average_loss = 0.0
|
|
22
|
+
adj_closes.each_with_index do |adj_close, index|
|
|
23
|
+
if index >= periods
|
|
24
|
+
if index == periods
|
|
25
|
+
average_gain = gain_or_loss(adj_closes[0..index], :gain) / periods
|
|
26
|
+
average_loss = gain_or_loss(adj_closes[0..index], :loss) / periods
|
|
27
|
+
else
|
|
28
|
+
difference = adj_close - adj_closes[index-1]
|
|
29
|
+
if difference >= 0
|
|
30
|
+
current_gain = difference
|
|
31
|
+
current_loss = 0
|
|
32
|
+
else
|
|
33
|
+
current_gain = 0
|
|
34
|
+
current_loss = difference.abs
|
|
35
|
+
end
|
|
36
|
+
average_gain = (average_gain * (periods-1) + current_gain) / periods
|
|
37
|
+
average_loss = (average_loss * (periods-1) + current_loss) / periods
|
|
38
|
+
end
|
|
39
|
+
rs[index] = average_gain / average_loss
|
|
40
|
+
output[index] = 100 - 100/(1+rs[index])
|
|
41
|
+
if average_gain == 0
|
|
42
|
+
output[index] = 0
|
|
43
|
+
elsif average_loss == 0
|
|
44
|
+
output[index] = 100
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
rs[index] = nil
|
|
48
|
+
output[index] = nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return output
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
# Helper methods for RSI
|
|
58
|
+
def self.gain_or_loss data, type
|
|
59
|
+
sum = 0.0
|
|
60
|
+
first_value = nil
|
|
61
|
+
data.each do |value|
|
|
62
|
+
if first_value == nil
|
|
63
|
+
first_value = value
|
|
64
|
+
else
|
|
65
|
+
if type == :gain
|
|
66
|
+
if value > first_value
|
|
67
|
+
sum += value - first_value
|
|
68
|
+
end
|
|
69
|
+
elsif type == :loss
|
|
70
|
+
if value < first_value
|
|
71
|
+
sum += first_value - value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
first_value = value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
return sum
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Simple Moving Average
|
|
4
|
+
class Sma
|
|
5
|
+
|
|
6
|
+
# SMA: (sum of closing prices for x period)/x
|
|
7
|
+
def self.calculate data, parameters
|
|
8
|
+
periods = parameters
|
|
9
|
+
output = Array.new
|
|
10
|
+
# Returns an array from the requested column and checks if there is enought data points.
|
|
11
|
+
adj_closes = Indicators::Helper.validate_data(data, :adj_close, periods)
|
|
12
|
+
|
|
13
|
+
adj_closes.each_with_index do |adj_close, index|
|
|
14
|
+
start = index+1-periods
|
|
15
|
+
if index+1 >= periods
|
|
16
|
+
adj_closes_sum = adj_closes[start..index].sum
|
|
17
|
+
output[index] = (adj_closes_sum/periods.to_f)
|
|
18
|
+
else
|
|
19
|
+
output[index] = nil
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
return output
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Indicators
|
|
2
|
+
#
|
|
3
|
+
# Full Stochastic Oscillator
|
|
4
|
+
class Sto
|
|
5
|
+
|
|
6
|
+
# %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100
|
|
7
|
+
# %D = 3-day SMA of %K
|
|
8
|
+
# Lowest Low = lowest low for the look-back period
|
|
9
|
+
# Highest High = highest high for the look-back period
|
|
10
|
+
# %K is multiplied by 100 to move the decimal point two places
|
|
11
|
+
#
|
|
12
|
+
# Full %K = Fast %K smoothed with X-period SMA
|
|
13
|
+
# Full %D = X-period SMA of Full %K
|
|
14
|
+
#
|
|
15
|
+
# Input 14, 3, 5
|
|
16
|
+
# Returns [full %K, full %D]
|
|
17
|
+
def self.calculate data, parameters
|
|
18
|
+
k1_periods = parameters[0]
|
|
19
|
+
k2_periods = parameters[1]
|
|
20
|
+
d_periods = parameters[2]
|
|
21
|
+
output = Array.new
|
|
22
|
+
adj_closes = Indicators::Helper.validate_data(data, :adj_close, k1_periods)
|
|
23
|
+
highs = Indicators::Helper.validate_data(data, :high, k1_periods)
|
|
24
|
+
lows = Indicators::Helper.validate_data(data, :low, k1_periods)
|
|
25
|
+
|
|
26
|
+
k1 = []
|
|
27
|
+
k2 = []
|
|
28
|
+
d = []
|
|
29
|
+
adj_closes.each_with_index do |adj_close, index|
|
|
30
|
+
start = index+1-k1_periods
|
|
31
|
+
if index+1 >= k1_periods
|
|
32
|
+
k1[index] = (adj_close - lows[start..index].min) / (highs[start..index].max - lows[start..index].min) * 100
|
|
33
|
+
if index+2 >= k1_periods + k2_periods
|
|
34
|
+
k2[index] = Indicators::Sma.calculate(k1[(k1_periods-1)..index], k2_periods).last
|
|
35
|
+
else
|
|
36
|
+
k2[index] = nil
|
|
37
|
+
end
|
|
38
|
+
if index+3 >= k1_periods + k2_periods + d_periods
|
|
39
|
+
d[index] = Indicators::Sma.calculate(k2[(k1_periods + k2_periods - 2)..index], d_periods).last
|
|
40
|
+
else
|
|
41
|
+
d[index] = nil
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
k1[index] = nil
|
|
45
|
+
end
|
|
46
|
+
output[index] = [k1[index], k2[index], d[index]]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
return output
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/indicators/data.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Indicators
|
|
|
27
27
|
def calc parameters
|
|
28
28
|
# Check is parameters are usable.
|
|
29
29
|
unless parameters.is_a?(Hash)
|
|
30
|
-
raise DataException, 'Given parameters have to be a hash. FORMAT: .calc(:type => :ema, :
|
|
30
|
+
raise DataException, 'Given parameters have to be a hash. FORMAT: .calc(:type => :ema, :params => 12)'
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# If not specified, set default :type to :sma.
|
|
@@ -35,7 +35,7 @@ module Indicators
|
|
|
35
35
|
|
|
36
36
|
# Check if there is such indicator type supported.
|
|
37
37
|
case
|
|
38
|
-
when INDICATORS.include?(parameters[:type]) then @results = Indicators::Main.
|
|
38
|
+
when INDICATORS.include?(parameters[:type]) then @results = Indicators::Main.new(@data, parameters)
|
|
39
39
|
else
|
|
40
40
|
raise DataException, "Invalid indicator type specified (#{parameters[:type]})."
|
|
41
41
|
end
|
data/lib/indicators/main.rb
CHANGED
|
@@ -1,358 +1,68 @@
|
|
|
1
1
|
module Indicators
|
|
2
|
-
|
|
3
|
-
# Moving averages
|
|
2
|
+
|
|
4
3
|
class Main
|
|
5
4
|
|
|
5
|
+
attr_reader :output, :abbr, :params
|
|
6
6
|
# Error handling.
|
|
7
7
|
class MainException < StandardError
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def initialize data, parameters
|
|
11
|
+
type = parameters[:type]
|
|
12
|
+
all_params = parameters[:params]
|
|
13
|
+
@abbr = type.to_s.upcase
|
|
14
|
+
if type == :sma
|
|
15
|
+
@params = Indicators::Helper.get_parameters(all_params, 0, 20)
|
|
16
|
+
elsif type == :ema
|
|
17
|
+
@params = Indicators::Helper.get_parameters(all_params, 0, 20)
|
|
18
|
+
elsif type == :bb
|
|
19
|
+
@params = Array.new
|
|
20
|
+
@params[0] = Indicators::Helper.get_parameters(all_params, 0, 20)
|
|
21
|
+
@params[1] = Indicators::Helper.get_parameters(all_params, 1, 2)
|
|
22
|
+
elsif type == :macd
|
|
23
|
+
@params = Array.new
|
|
24
|
+
@params[0] = Indicators::Helper.get_parameters(all_params, 0, 12)
|
|
25
|
+
@params[1] = Indicators::Helper.get_parameters(all_params, 1, 26)
|
|
26
|
+
@params[2] = Indicators::Helper.get_parameters(all_params, 2, 9)
|
|
27
|
+
elsif type == :rsi
|
|
28
|
+
@params = Indicators::Helper.get_parameters(all_params, 0, 14)
|
|
29
|
+
elsif type == :sto
|
|
30
|
+
@params = Array.new
|
|
31
|
+
@params[0] = Indicators::Helper.get_parameters(all_params, 0, 14)
|
|
32
|
+
@params[1] = Indicators::Helper.get_parameters(all_params, 1, 3)
|
|
33
|
+
@params[2] = Indicators::Helper.get_parameters(all_params, 2, 3)
|
|
34
|
+
end
|
|
35
|
+
|
|
11
36
|
if data.is_a?(Hash)
|
|
12
|
-
|
|
37
|
+
@output = Hash.new
|
|
13
38
|
data.each do |symbol, stock_data|
|
|
14
39
|
# Check if this symbol was empty and don't go further with it.
|
|
15
40
|
if stock_data.length == 0
|
|
16
|
-
|
|
41
|
+
@output[symbol] = []
|
|
17
42
|
else
|
|
18
|
-
|
|
19
|
-
when :sma then
|
|
20
|
-
when :ema then
|
|
21
|
-
when :bb then
|
|
22
|
-
when :macd then
|
|
23
|
-
when :rsi then
|
|
24
|
-
when :sto then
|
|
43
|
+
@output[symbol] = case type
|
|
44
|
+
when :sma then Indicators::Sma.calculate(Indicators::Parser.parse_data(stock_data), @params)
|
|
45
|
+
when :ema then Indicators::Ema.calculate(Indicators::Parser.parse_data(stock_data), @params)
|
|
46
|
+
when :bb then Indicators::Bb.calculate(Indicators::Parser.parse_data(stock_data), @params)
|
|
47
|
+
when :macd then Indicators::Macd.calculate(Indicators::Parser.parse_data(stock_data), @params)
|
|
48
|
+
when :rsi then Indicators::Rsi.calculate(Indicators::Parser.parse_data(stock_data), @params)
|
|
49
|
+
when :sto then Indicators::Sto.calculate(Indicators::Parser.parse_data(stock_data), @params)
|
|
25
50
|
end
|
|
26
51
|
# Parser returns in {:date=>[2012.0, 2012.0, 2012.0], :open=>[409.4, 410.0, 414.95],} format
|
|
27
52
|
end
|
|
28
53
|
end
|
|
29
54
|
else
|
|
30
|
-
|
|
31
|
-
when :sma then
|
|
32
|
-
when :ema then
|
|
33
|
-
when :bb then
|
|
34
|
-
when :macd then
|
|
35
|
-
when :rsi then
|
|
36
|
-
when :sto then raise MainException, "You cannot calculate Stochastic Oscillator on
|
|
55
|
+
@output = case type
|
|
56
|
+
when :sma then Indicators::Sma.calculate(data, @params)
|
|
57
|
+
when :ema then Indicators::Ema.calculate(data, @params)
|
|
58
|
+
when :bb then Indicators::Bb.calculate(data, @params)
|
|
59
|
+
when :macd then Indicators::Macd.calculate(data, @params)
|
|
60
|
+
when :rsi then Indicators::Rsi.calculate(data, @params)
|
|
61
|
+
when :sto then raise MainException, "You cannot calculate Stochastic Oscillator on array. Highs and lows are needed. Feel free Securities gem hash instead."
|
|
37
62
|
end
|
|
38
63
|
end
|
|
39
|
-
|
|
64
|
+
return @output
|
|
40
65
|
end
|
|
41
66
|
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def self.get_data data, variables, column
|
|
45
|
-
# If this is a hash, choose which column of values to use for calculations.
|
|
46
|
-
if data.is_a?(Hash)
|
|
47
|
-
usable_data = data[column]
|
|
48
|
-
else
|
|
49
|
-
usable_data = data
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
if usable_data.length < variables
|
|
53
|
-
raise MainException, "Data point length (#{usable_data.length}) must be greater or equal to the needed periods value (#{variables})."
|
|
54
|
-
end
|
|
55
|
-
return usable_data
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.get_variables variables, i=0, default=0
|
|
59
|
-
if variables.is_a?(Array)
|
|
60
|
-
# In case array is given not like [1, 2] but like this ["1", "2"]. This usually happens when getting data from input forms.
|
|
61
|
-
variables = variables.map(&:to_i)
|
|
62
|
-
if variables.length < 2
|
|
63
|
-
return default if i != 0
|
|
64
|
-
return variables[0]
|
|
65
|
-
else
|
|
66
|
-
if variables[i].nil? then return default else return variables[i] end
|
|
67
|
-
end
|
|
68
|
-
else
|
|
69
|
-
return default if i != 0
|
|
70
|
-
return variables
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
# Lagging indicators
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
# Simple Moving Average
|
|
79
|
-
def self.sma data, variables
|
|
80
|
-
periods = get_variables(variables)
|
|
81
|
-
usable_data = Array.new
|
|
82
|
-
usable_data = get_data(data, periods, :adj_close)
|
|
83
|
-
|
|
84
|
-
# Just the calculation of SMA by the formula.
|
|
85
|
-
sma = []
|
|
86
|
-
usable_data.each_with_index do |value, index|
|
|
87
|
-
from = index-periods+1
|
|
88
|
-
if from >= 0
|
|
89
|
-
sum = usable_data[from..index].sum
|
|
90
|
-
sma[index] = (sum/periods.to_f)
|
|
91
|
-
else
|
|
92
|
-
sma[index] = nil
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
return sma
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
# Exponential Moving Average
|
|
100
|
-
|
|
101
|
-
# Multiplier: (2 / (Time periods + 1) ) = (2 / (10 + 1) ) = 0.1818 (18.18%)
|
|
102
|
-
# EMA: {Close - EMA(previous day)} x multiplier + EMA(previous day).
|
|
103
|
-
def self.ema data, variables
|
|
104
|
-
periods = get_variables(variables)
|
|
105
|
-
usable_data = Array.new
|
|
106
|
-
usable_data = get_data(data, periods, :adj_close)
|
|
107
|
-
|
|
108
|
-
ema = []
|
|
109
|
-
k = 2/(periods+1).to_f
|
|
110
|
-
usable_data.each_with_index do |value, index|
|
|
111
|
-
from = index+1-periods
|
|
112
|
-
if from == 0
|
|
113
|
-
ema[index] = sma(usable_data[from..index], periods).last
|
|
114
|
-
# puts ma
|
|
115
|
-
elsif from > 0
|
|
116
|
-
ema[index] = ((usable_data[index] - ema[index-1]) * k + ema[index-1])
|
|
117
|
-
else
|
|
118
|
-
ema[index] = nil
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
return ema
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
# Bollinger Bands :type => :bb, :variables => 20, 2)
|
|
126
|
-
|
|
127
|
-
# Middle Band = 20-day simple moving average (SMA)
|
|
128
|
-
# Upper Band = 20-day SMA + (20-day standard deviation of price x 2)
|
|
129
|
-
# Lower Band = 20-day SMA - (20-day standard deviation of price x 2)
|
|
130
|
-
def self.bb data, variables
|
|
131
|
-
periods = get_variables(variables)
|
|
132
|
-
default_multiplier = 2
|
|
133
|
-
multiplier = get_variables(variables, 1, default_multiplier)
|
|
134
|
-
|
|
135
|
-
usable_data = Array.new
|
|
136
|
-
usable_data = get_data(data, periods, :adj_close)
|
|
137
|
-
bb = []
|
|
138
|
-
usable_data.each_with_index do |value, index|
|
|
139
|
-
from = index-periods+1
|
|
140
|
-
if from >= 0
|
|
141
|
-
middle_band = sma(usable_data[from..index], periods).last
|
|
142
|
-
upper_band = middle_band + (usable_data[from..index].standard_deviation * multiplier)
|
|
143
|
-
lower_band = middle_band - (usable_data[from..index].standard_deviation * multiplier)
|
|
144
|
-
# output is [middle, upper, lower]
|
|
145
|
-
bb[index] = [middle_band, upper_band, lower_band]
|
|
146
|
-
else
|
|
147
|
-
bb[index] = nil
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
return bb
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
#
|
|
154
|
-
# Moving Average Convergence Divergence
|
|
155
|
-
|
|
156
|
-
# MACD Line: (12-day EMA - 26-day EMA)
|
|
157
|
-
# Signal Line: 9-day EMA of MACD Line
|
|
158
|
-
# MACD Histogram: MACD Line - Signal Line
|
|
159
|
-
# Default MACD(12, 26, 9)
|
|
160
|
-
def self.macd data, variables
|
|
161
|
-
faster_periods = get_variables(variables, 0, 12)
|
|
162
|
-
slower_periods = get_variables(variables, 1, 26)
|
|
163
|
-
signal_periods = get_variables(variables, 2, 9)
|
|
164
|
-
@variables = [faster_periods, slower_periods, signal_periods]
|
|
165
|
-
# puts "faster=#{faster_periods}, slower=#{slower_periods}, signal=#{signal_periods}"
|
|
166
|
-
|
|
167
|
-
usable_data = Array.new
|
|
168
|
-
usable_data = get_data(data, slower_periods+signal_periods-1, :adj_close)
|
|
169
|
-
macd = []
|
|
170
|
-
macd_line = []
|
|
171
|
-
|
|
172
|
-
usable_data.each_with_index do |value, index|
|
|
173
|
-
if index+1 >= slower_periods
|
|
174
|
-
# Calibrate me! Not sure why it doesn't accept from or from_faster.
|
|
175
|
-
faster_ema = ema(usable_data[0..index], faster_periods).last
|
|
176
|
-
slower_ema = ema(usable_data[0..index], slower_periods).last
|
|
177
|
-
macd_line[index] = faster_ema - slower_ema
|
|
178
|
-
if index+1 >= slower_periods + signal_periods
|
|
179
|
-
# I'm pretty sure this is right.
|
|
180
|
-
signal_line = ema(macd_line[(-signal_periods)..index], signal_periods).last
|
|
181
|
-
# Output is [MACD, Signal, MACD Hist]
|
|
182
|
-
macd_histogram = macd_line[index] - signal_line
|
|
183
|
-
macd[index] = [macd_line[index], signal_line, macd_histogram]
|
|
184
|
-
end
|
|
185
|
-
else
|
|
186
|
-
macd_line[index] = nil
|
|
187
|
-
macd[index] = nil
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
return macd
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
# Relative Strength Index
|
|
195
|
-
|
|
196
|
-
# 100
|
|
197
|
-
# RSI = 100 - --------
|
|
198
|
-
# 1 + RS
|
|
199
|
-
# RS = Average Gain / Average Loss
|
|
200
|
-
# First Average Gain = Sum of Gains over the past 14 periods / 14
|
|
201
|
-
# First Average Loss = Sum of Losses over the past 14 periods / 14
|
|
202
|
-
# Average Gain = [(previous Average Gain) x 13 + current Gain] / 14.
|
|
203
|
-
# Average Loss = [(previous Average Loss) x 13 + current Loss] / 14.
|
|
204
|
-
def self.rsi data, variables
|
|
205
|
-
periods = get_variables(variables)
|
|
206
|
-
usable_data = Array.new
|
|
207
|
-
usable_data = get_data(data, periods, :adj_close)
|
|
208
|
-
|
|
209
|
-
values = []
|
|
210
|
-
rsi = []
|
|
211
|
-
rs = []
|
|
212
|
-
average_gain = 0.0
|
|
213
|
-
average_loss = 0.0
|
|
214
|
-
usable_data.each_with_index do |value, index|
|
|
215
|
-
values[index] = value
|
|
216
|
-
if index >= periods
|
|
217
|
-
if index == periods
|
|
218
|
-
average_gain = gain(values) / periods
|
|
219
|
-
average_loss = loss(values) / periods
|
|
220
|
-
else
|
|
221
|
-
difference = value - values[index-1]
|
|
222
|
-
if difference >= 0
|
|
223
|
-
current_gain = difference
|
|
224
|
-
current_loss = 0
|
|
225
|
-
else
|
|
226
|
-
current_gain = 0
|
|
227
|
-
current_loss = difference.abs
|
|
228
|
-
end
|
|
229
|
-
average_gain = (average_gain * (periods-1) + current_gain) / periods
|
|
230
|
-
average_loss = (average_loss * (periods-1) + current_loss) / periods
|
|
231
|
-
end
|
|
232
|
-
rs[index] = average_gain / average_loss
|
|
233
|
-
rsi[index] = 100 - 100/(1+rs[index])
|
|
234
|
-
if average_gain == 0
|
|
235
|
-
rsi[index] = 0
|
|
236
|
-
elsif average_loss == 0
|
|
237
|
-
rsi[index] = 100
|
|
238
|
-
end
|
|
239
|
-
else
|
|
240
|
-
rs[index] = nil
|
|
241
|
-
rsi[index] = nil
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
return rsi
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
#
|
|
248
|
-
# Full Stochastic Oscillator
|
|
249
|
-
|
|
250
|
-
# %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100
|
|
251
|
-
# %D = 3-day SMA of %K
|
|
252
|
-
# Lowest Low = lowest low for the look-back period
|
|
253
|
-
# Highest High = highest high for the look-back period
|
|
254
|
-
# %K is multiplied by 100 to move the decimal point two places
|
|
255
|
-
#
|
|
256
|
-
# Full %K = Fast %K smoothed with X-period SMA
|
|
257
|
-
# Full %D = X-period SMA of Full %K
|
|
258
|
-
#
|
|
259
|
-
# Input 14, 3, 5
|
|
260
|
-
# Returns [full %K, full %D]
|
|
261
|
-
def self.sto data, variables
|
|
262
|
-
k1_periods = get_variables(variables, 0, 14)
|
|
263
|
-
k2_periods = get_variables(variables, 1, 3)
|
|
264
|
-
d_periods = get_variables(variables, 2, 1)
|
|
265
|
-
high_data = Array.new
|
|
266
|
-
low_data = Array.new
|
|
267
|
-
adj_close_data = Array.new
|
|
268
|
-
high_data = get_data(data, k1_periods, :high)
|
|
269
|
-
low_data = get_data(data, k1_periods, :low)
|
|
270
|
-
adj_close_data = get_data(data, k1_periods, :adj_close)
|
|
271
|
-
|
|
272
|
-
k1 = []
|
|
273
|
-
k2 = []
|
|
274
|
-
d = []
|
|
275
|
-
sto = []
|
|
276
|
-
adj_close_data.each_with_index do |adj_close, index|
|
|
277
|
-
from = index-k1_periods+1
|
|
278
|
-
if index+1 >= k1_periods
|
|
279
|
-
k1[index] = (adj_close - low_data[from..index].min) / (high_data[from..index].max - low_data[from..index].min) * 100
|
|
280
|
-
if index+2 >= k1_periods + k2_periods
|
|
281
|
-
k2[index] = sma(k1[(k1_periods-1)..index], k2_periods).last
|
|
282
|
-
else
|
|
283
|
-
k2[index] = nil
|
|
284
|
-
end
|
|
285
|
-
if index+3 >= k1_periods + k2_periods + d_periods
|
|
286
|
-
d[index] = sma(k2[(k1_periods + k2_periods - 2)..index], d_periods).last
|
|
287
|
-
else
|
|
288
|
-
d[index] = nil
|
|
289
|
-
end
|
|
290
|
-
else
|
|
291
|
-
k1[index] = nil
|
|
292
|
-
end
|
|
293
|
-
sto[index] = [k1[index], k2[index], d[index]]
|
|
294
|
-
end
|
|
295
|
-
return sto
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
#
|
|
300
|
-
# Helper methods for RSI
|
|
301
|
-
def self.gain data
|
|
302
|
-
sum = 0.0
|
|
303
|
-
first_value = nil
|
|
304
|
-
data.each do |value|
|
|
305
|
-
if first_value == nil
|
|
306
|
-
first_value = value
|
|
307
|
-
else
|
|
308
|
-
if value > first_value
|
|
309
|
-
sum += value - first_value
|
|
310
|
-
end
|
|
311
|
-
first_value = value
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
return sum
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def self.loss data
|
|
318
|
-
sum = 0.0
|
|
319
|
-
first_value = nil
|
|
320
|
-
data.each do |value|
|
|
321
|
-
if first_value == nil
|
|
322
|
-
first_value = value
|
|
323
|
-
else
|
|
324
|
-
if value < first_value
|
|
325
|
-
sum += first_value - value
|
|
326
|
-
end
|
|
327
|
-
first_value = value
|
|
328
|
-
end
|
|
329
|
-
end
|
|
330
|
-
return sum
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
#
|
|
337
|
-
# Extra methods for mathematical calculations.
|
|
338
|
-
module Enumerable
|
|
339
|
-
|
|
340
|
-
def sum
|
|
341
|
-
return self.inject(0){|accum, i| accum + i }
|
|
342
67
|
end
|
|
343
|
-
|
|
344
|
-
def mean
|
|
345
|
-
return self.sum / self.length.to_f
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
def sample_variance
|
|
349
|
-
m = self.mean
|
|
350
|
-
sum = self.inject(0){|accum, i| accum + (i - m) ** 2 }
|
|
351
|
-
return sum / (self.length - 1).to_f
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def standard_deviation
|
|
355
|
-
return Math.sqrt(self.sample_variance)
|
|
356
|
-
end
|
|
357
|
-
|
|
358
68
|
end
|
data/lib/indicators/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: indicators
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2012-09-
|
|
12
|
+
date: 2012-09-03 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rails
|
|
@@ -75,6 +75,13 @@ files:
|
|
|
75
75
|
- Rakefile
|
|
76
76
|
- indicators.gemspec
|
|
77
77
|
- lib/indicators.rb
|
|
78
|
+
- lib/indicators/calculations/bb.rb
|
|
79
|
+
- lib/indicators/calculations/ema.rb
|
|
80
|
+
- lib/indicators/calculations/helper.rb
|
|
81
|
+
- lib/indicators/calculations/macd.rb
|
|
82
|
+
- lib/indicators/calculations/rsi.rb
|
|
83
|
+
- lib/indicators/calculations/sma.rb
|
|
84
|
+
- lib/indicators/calculations/sto.rb
|
|
78
85
|
- lib/indicators/data.rb
|
|
79
86
|
- lib/indicators/main.rb
|
|
80
87
|
- lib/indicators/parser.rb
|
|
@@ -95,7 +102,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
95
102
|
version: '0'
|
|
96
103
|
segments:
|
|
97
104
|
- 0
|
|
98
|
-
hash:
|
|
105
|
+
hash: 856502728936260822
|
|
99
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
107
|
none: false
|
|
101
108
|
requirements:
|
|
@@ -104,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
111
|
version: '0'
|
|
105
112
|
segments:
|
|
106
113
|
- 0
|
|
107
|
-
hash:
|
|
114
|
+
hash: 856502728936260822
|
|
108
115
|
requirements: []
|
|
109
116
|
rubyforge_project:
|
|
110
117
|
rubygems_version: 1.8.24
|