technical-analysis 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/technical-analysis.rb +3 -0
- data/lib/technical_analysis.rb +41 -0
- data/lib/technical_analysis/helpers/array_helper.rb +27 -0
- data/lib/technical_analysis/helpers/stock_calculation.rb +25 -0
- data/lib/technical_analysis/helpers/validation.rb +33 -0
- data/lib/technical_analysis/indicators/adi.rb +101 -0
- data/lib/technical_analysis/indicators/adtv.rb +98 -0
- data/lib/technical_analysis/indicators/adx.rb +168 -0
- data/lib/technical_analysis/indicators/ao.rb +105 -0
- data/lib/technical_analysis/indicators/atr.rb +109 -0
- data/lib/technical_analysis/indicators/bb.rb +126 -0
- data/lib/technical_analysis/indicators/cci.rb +105 -0
- data/lib/technical_analysis/indicators/cmf.rb +105 -0
- data/lib/technical_analysis/indicators/cr.rb +95 -0
- data/lib/technical_analysis/indicators/dc.rb +108 -0
- data/lib/technical_analysis/indicators/dlr.rb +97 -0
- data/lib/technical_analysis/indicators/dpo.rb +106 -0
- data/lib/technical_analysis/indicators/dr.rb +96 -0
- data/lib/technical_analysis/indicators/eom.rb +104 -0
- data/lib/technical_analysis/indicators/fi.rb +95 -0
- data/lib/technical_analysis/indicators/ichimoku.rb +179 -0
- data/lib/technical_analysis/indicators/indicator.rb +138 -0
- data/lib/technical_analysis/indicators/kc.rb +124 -0
- data/lib/technical_analysis/indicators/kst.rb +132 -0
- data/lib/technical_analysis/indicators/macd.rb +144 -0
- data/lib/technical_analysis/indicators/mfi.rb +119 -0
- data/lib/technical_analysis/indicators/mi.rb +121 -0
- data/lib/technical_analysis/indicators/nvi.rb +102 -0
- data/lib/technical_analysis/indicators/obv.rb +104 -0
- data/lib/technical_analysis/indicators/obv_mean.rb +110 -0
- data/lib/technical_analysis/indicators/rsi.rb +133 -0
- data/lib/technical_analysis/indicators/sma.rb +98 -0
- data/lib/technical_analysis/indicators/sr.rb +122 -0
- data/lib/technical_analysis/indicators/trix.rb +127 -0
- data/lib/technical_analysis/indicators/tsi.rb +139 -0
- data/lib/technical_analysis/indicators/uo.rb +130 -0
- data/lib/technical_analysis/indicators/vi.rb +117 -0
- data/lib/technical_analysis/indicators/vpt.rb +95 -0
- data/lib/technical_analysis/indicators/wr.rb +103 -0
- data/spec/helpers/array_helper_spec.rb +31 -0
- data/spec/helpers/validaton_spec.rb +22 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/ta_test_data.csv +64 -0
- data/spec/technical_analysis/indicators/adi_spec.rb +116 -0
- data/spec/technical_analysis/indicators/adtv_spec.rb +98 -0
- data/spec/technical_analysis/indicators/adx_spec.rb +92 -0
- data/spec/technical_analysis/indicators/ao_spec.rb +86 -0
- data/spec/technical_analysis/indicators/atr_spec.rb +105 -0
- data/spec/technical_analysis/indicators/bb_spec.rb +100 -0
- data/spec/technical_analysis/indicators/cci_spec.rb +100 -0
- data/spec/technical_analysis/indicators/cmf_spec.rb +100 -0
- data/spec/technical_analysis/indicators/cr_spec.rb +119 -0
- data/spec/technical_analysis/indicators/dc_spec.rb +100 -0
- data/spec/technical_analysis/indicators/dlr_spec.rb +119 -0
- data/spec/technical_analysis/indicators/dpo_spec.rb +90 -0
- data/spec/technical_analysis/indicators/dr_spec.rb +119 -0
- data/spec/technical_analysis/indicators/eom_spec.rb +105 -0
- data/spec/technical_analysis/indicators/fi_spec.rb +118 -0
- data/spec/technical_analysis/indicators/ichimoku_spec.rb +95 -0
- data/spec/technical_analysis/indicators/indicator_spec.rb +120 -0
- data/spec/technical_analysis/indicators/kc_spec.rb +110 -0
- data/spec/technical_analysis/indicators/kst_spec.rb +78 -0
- data/spec/technical_analysis/indicators/macd_spec.rb +86 -0
- data/spec/technical_analysis/indicators/mfi_spec.rb +105 -0
- data/spec/technical_analysis/indicators/mi_spec.rb +79 -0
- data/spec/technical_analysis/indicators/nvi_spec.rb +119 -0
- data/spec/technical_analysis/indicators/obv_mean_spec.rb +109 -0
- data/spec/technical_analysis/indicators/obv_spec.rb +119 -0
- data/spec/technical_analysis/indicators/rsi_spec.rb +105 -0
- data/spec/technical_analysis/indicators/sma_spec.rb +115 -0
- data/spec/technical_analysis/indicators/sr_spec.rb +104 -0
- data/spec/technical_analysis/indicators/trix_spec.rb +76 -0
- data/spec/technical_analysis/indicators/tsi_spec.rb +87 -0
- data/spec/technical_analysis/indicators/uo_spec.rb +91 -0
- data/spec/technical_analysis/indicators/vi_spec.rb +105 -0
- data/spec/technical_analysis/indicators/vpt_spec.rb +118 -0
- data/spec/technical_analysis/indicators/wr_spec.rb +106 -0
- metadata +177 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
class Indicator
|
3
|
+
|
4
|
+
CALCULATIONS = [
|
5
|
+
:indicator_name,
|
6
|
+
:indicator_symbol,
|
7
|
+
:min_data_size,
|
8
|
+
:technicals,
|
9
|
+
:valid_options,
|
10
|
+
:validate_options,
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
private_constant :CALCULATIONS
|
14
|
+
|
15
|
+
# Returns an array of TechnicalAnalysis modules
|
16
|
+
#
|
17
|
+
# @return [Array] A list of TechnicalAnalysis::Class
|
18
|
+
def self.roster
|
19
|
+
[
|
20
|
+
TechnicalAnalysis::Adi,
|
21
|
+
TechnicalAnalysis::Adtv,
|
22
|
+
TechnicalAnalysis::Adx,
|
23
|
+
TechnicalAnalysis::Ao,
|
24
|
+
TechnicalAnalysis::Atr,
|
25
|
+
TechnicalAnalysis::Bb,
|
26
|
+
TechnicalAnalysis::Cci,
|
27
|
+
TechnicalAnalysis::Cmf,
|
28
|
+
TechnicalAnalysis::Cr,
|
29
|
+
TechnicalAnalysis::Dc,
|
30
|
+
TechnicalAnalysis::Dlr,
|
31
|
+
TechnicalAnalysis::Dpo,
|
32
|
+
TechnicalAnalysis::Dr,
|
33
|
+
TechnicalAnalysis::Eom,
|
34
|
+
TechnicalAnalysis::Fi,
|
35
|
+
TechnicalAnalysis::Ichimoku,
|
36
|
+
TechnicalAnalysis::Kc,
|
37
|
+
TechnicalAnalysis::Kst,
|
38
|
+
TechnicalAnalysis::Macd,
|
39
|
+
TechnicalAnalysis::Mfi,
|
40
|
+
TechnicalAnalysis::Mi,
|
41
|
+
TechnicalAnalysis::Nvi,
|
42
|
+
TechnicalAnalysis::Obv,
|
43
|
+
TechnicalAnalysis::ObvMean,
|
44
|
+
TechnicalAnalysis::Rsi,
|
45
|
+
TechnicalAnalysis::Sma,
|
46
|
+
TechnicalAnalysis::Sr,
|
47
|
+
TechnicalAnalysis::Trix,
|
48
|
+
TechnicalAnalysis::Tsi,
|
49
|
+
TechnicalAnalysis::Uo,
|
50
|
+
TechnicalAnalysis::Vi,
|
51
|
+
TechnicalAnalysis::Vpt,
|
52
|
+
TechnicalAnalysis::Wr,
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Finds the applicable indicator and returns an instance
|
57
|
+
#
|
58
|
+
# @param indicator_symbol [String] Downcased string of the indicator symbol
|
59
|
+
#
|
60
|
+
# @return TechnicalAnalysis::ClassName
|
61
|
+
def self.find(indicator_symbol)
|
62
|
+
roster.each do |indicator|
|
63
|
+
return indicator if indicator.indicator_symbol == indicator_symbol
|
64
|
+
end
|
65
|
+
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Find the applicable indicator and looks up the value
|
70
|
+
#
|
71
|
+
# @param indicator_symbol [String] Downcased string of the indicator symbol
|
72
|
+
# @param data [Array] Array of hashes of price data to perform calcualtion on
|
73
|
+
# @param calculation [Symbol] The calculation to be performed on the requested indicator and params
|
74
|
+
# @param options [Hash] A hash containing options for the requested calculation
|
75
|
+
#
|
76
|
+
# @return Returns the requested calculation
|
77
|
+
def self.calculate(indicator_symbol, data, calculation, options={})
|
78
|
+
return nil unless CALCULATIONS.include? calculation
|
79
|
+
|
80
|
+
indicator = find(indicator_symbol)
|
81
|
+
raise "Indicator not found!" if indicator.nil?
|
82
|
+
|
83
|
+
case calculation
|
84
|
+
when :indicator_name; indicator.indicator_name
|
85
|
+
when :indicator_symbol; indicator.indicator_symbol
|
86
|
+
when :technicals; indicator.calculate(data, options)
|
87
|
+
when :min_data_size; indicator.min_data_size(options)
|
88
|
+
when :valid_options; indicator.valid_options
|
89
|
+
when :validate_options; indicator.validate_options(options)
|
90
|
+
else nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Calculates the minimum number of observations needed to calculate the technical indicator
|
95
|
+
#
|
96
|
+
# @param options [Hash] The options for the technical indicator
|
97
|
+
#
|
98
|
+
# @return [Integer] Returns the minimum number of observations needed to calculate the technical
|
99
|
+
# indicator based on the options provided
|
100
|
+
def self.min_data_size(indicator_symbol, options)
|
101
|
+
raise "#{self.name} did not implement min_data_size"
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# Validates the provided options for this technical indicator
|
106
|
+
#
|
107
|
+
# @param options [Hash] The options for the technical indicator to be validated
|
108
|
+
#
|
109
|
+
# @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not
|
110
|
+
def self.validate_options(options)
|
111
|
+
raise "#{self.name} did not implement validate_options"
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns an array of valid keys for options for this technical indicator
|
116
|
+
#
|
117
|
+
# @return [Array] An array of keys as symbols for valid options for this technical indicator
|
118
|
+
def self.valid_options
|
119
|
+
raise "#{self.name} did not implement valid_options"
|
120
|
+
[]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the symbol of the technical indicator
|
124
|
+
#
|
125
|
+
# @return [String] A string of the symbol of the technical indicator
|
126
|
+
def self.indicator_symbol
|
127
|
+
raise "#{self.name} did not implement indicator_symbol"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the name of the technical indicator
|
131
|
+
#
|
132
|
+
# @return [String] A string of the name of the technical indicator
|
133
|
+
def self.indicator_name
|
134
|
+
raise "#{self.name} did not implement indicator_name"
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# Keltner Channel
|
3
|
+
class Kc < Indicator
|
4
|
+
|
5
|
+
# Returns the symbol of the technical indicator
|
6
|
+
#
|
7
|
+
# @return [String] A string of the symbol of the technical indicator
|
8
|
+
def self.indicator_symbol
|
9
|
+
"kc"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the name of the technical indicator
|
13
|
+
#
|
14
|
+
# @return [String] A string of the name of the technical indicator
|
15
|
+
def self.indicator_name
|
16
|
+
"Keltner Channel"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an array of valid keys for options for this technical indicator
|
20
|
+
#
|
21
|
+
# @return [Array] An array of keys as symbols for valid options for this technical indicator
|
22
|
+
def self.valid_options
|
23
|
+
%i(period)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validates the provided options for this technical indicator
|
27
|
+
#
|
28
|
+
# @param options [Hash] The options for the technical indicator to be validated
|
29
|
+
#
|
30
|
+
# @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not
|
31
|
+
def self.validate_options(options)
|
32
|
+
Validation.validate_options(options, valid_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calculates the minimum number of observations needed to calculate the technical indicator
|
36
|
+
#
|
37
|
+
# @param options [Hash] The options for the technical indicator
|
38
|
+
#
|
39
|
+
# @return [Integer] Returns the minimum number of observations needed to calculate the technical
|
40
|
+
# indicator based on the options provided
|
41
|
+
def self.min_data_size(period: 10)
|
42
|
+
period.to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the keltner channel (KC) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/Keltner_channel
|
47
|
+
#
|
48
|
+
# @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close)
|
49
|
+
# @param period [Integer] The given period to calculate the KC
|
50
|
+
#
|
51
|
+
# @return [Array<KcValue>] An array of KcValue instances
|
52
|
+
def self.calculate(data, period: 10)
|
53
|
+
period = period.to_i
|
54
|
+
Validation.validate_numeric_data(data, :high, :low, :close)
|
55
|
+
Validation.validate_length(data, min_data_size(period: period))
|
56
|
+
Validation.validate_date_time_key(data)
|
57
|
+
|
58
|
+
data = data.sort_by { |row| row[:date_time] }
|
59
|
+
|
60
|
+
output = []
|
61
|
+
period_values = []
|
62
|
+
|
63
|
+
data.each do |v|
|
64
|
+
tp = StockCalculation.typical_price(v)
|
65
|
+
tr = v[:high] - v[:low]
|
66
|
+
period_values << { typical_price: tp, trading_range: tr }
|
67
|
+
|
68
|
+
if period_values.size == period
|
69
|
+
mb = ArrayHelper.average(period_values.map { |pv| pv[:typical_price] })
|
70
|
+
|
71
|
+
trading_range_average = ArrayHelper.average(period_values.map { |pv| pv[:trading_range] })
|
72
|
+
ub = mb + trading_range_average
|
73
|
+
lb = mb - trading_range_average
|
74
|
+
|
75
|
+
output << KcValue.new(
|
76
|
+
date_time: v[:date_time],
|
77
|
+
lower_band: lb,
|
78
|
+
middle_band: mb,
|
79
|
+
upper_band: ub
|
80
|
+
)
|
81
|
+
|
82
|
+
period_values.shift
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
output.sort_by(&:date_time).reverse
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# The value class to be returned by calculations
|
92
|
+
class KcValue
|
93
|
+
|
94
|
+
# @return [String] the date_time of the obversation as it was provided
|
95
|
+
attr_accessor :date_time
|
96
|
+
|
97
|
+
# @return [Float] the lower_band calculation value
|
98
|
+
attr_accessor :lower_band
|
99
|
+
|
100
|
+
# @return [Float] the middle_band calculation value
|
101
|
+
attr_accessor :middle_band
|
102
|
+
|
103
|
+
# @return [Float] the upper_band calculation value
|
104
|
+
attr_accessor :upper_band
|
105
|
+
|
106
|
+
def initialize(date_time: nil, lower_band: nil, middle_band: nil, upper_band: nil)
|
107
|
+
@date_time = date_time
|
108
|
+
@lower_band = lower_band
|
109
|
+
@middle_band = middle_band
|
110
|
+
@upper_band = upper_band
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [Hash] the attributes as a hash
|
114
|
+
def to_hash
|
115
|
+
{
|
116
|
+
date_time: @date_time,
|
117
|
+
lower_band: @lower_band,
|
118
|
+
middle_band: @middle_band,
|
119
|
+
upper_band: @upper_band
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# Know Sure Thing
|
3
|
+
class Kst < Indicator
|
4
|
+
|
5
|
+
# Returns the symbol of the technical indicator
|
6
|
+
#
|
7
|
+
# @return [String] A string of the symbol of the technical indicator
|
8
|
+
def self.indicator_symbol
|
9
|
+
"kst"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the name of the technical indicator
|
13
|
+
#
|
14
|
+
# @return [String] A string of the name of the technical indicator
|
15
|
+
def self.indicator_name
|
16
|
+
"Know Sure Thing"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an array of valid keys for options for this technical indicator
|
20
|
+
#
|
21
|
+
# @return [Array] An array of keys as symbols for valid options for this technical indicator
|
22
|
+
def self.valid_options
|
23
|
+
%i(period roc1 roc2 roc3 roc4 sma1 sma2 sma3 sma4 price_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validates the provided options for this technical indicator
|
27
|
+
#
|
28
|
+
# @param options [Hash] The options for the technical indicator to be validated
|
29
|
+
#
|
30
|
+
# @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not
|
31
|
+
def self.validate_options(options)
|
32
|
+
Validation.validate_options(options, valid_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calculates the minimum number of observations needed to calculate the technical indicator
|
36
|
+
#
|
37
|
+
# @param options [Hash] The options for the technical indicator
|
38
|
+
#
|
39
|
+
# @return [Integer] Returns the minimum number of observations needed to calculate the technical
|
40
|
+
# indicator based on the options provided
|
41
|
+
def self.min_data_size(roc4: 30, sma4: 15, **params)
|
42
|
+
roc4.to_i + sma4.to_i - 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the know sure thing (KST) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/KST_oscillator
|
47
|
+
#
|
48
|
+
# @param data [Array] Array of hashes with keys (:date_time, :value)
|
49
|
+
# @param roc1 [Integer] The given period to calculate the rate-of-change for RCMA1
|
50
|
+
# @param roc2 [Integer] The given period to calculate the rate-of-change for RCMA2
|
51
|
+
# @param roc3 [Integer] The given period to calculate the rate-of-change for RCMA3
|
52
|
+
# @param roc4 [Integer] The given period to calculate the rate-of-change for RCMA4
|
53
|
+
# @param sma1 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA1
|
54
|
+
# @param sma2 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA2
|
55
|
+
# @param sma3 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA3
|
56
|
+
# @param sma4 [Integer] The given period to calculate the SMA of the rate-of-change for RCMA4
|
57
|
+
# @param price_key [Symbol] The hash key for the price data. Default :value
|
58
|
+
#
|
59
|
+
# @return [Array<KstValue>] An array of KstValue instances
|
60
|
+
def self.calculate(data, roc1: 10, roc2: 15, roc3: 20, roc4: 30, sma1: 10, sma2: 10, sma3: 10, sma4: 15, price_key: :value)
|
61
|
+
roc1 = roc1.to_i
|
62
|
+
roc2 = roc2.to_i
|
63
|
+
roc3 = roc3.to_i
|
64
|
+
roc4 = roc4.to_i
|
65
|
+
sma1 = sma1.to_i
|
66
|
+
sma2 = sma2.to_i
|
67
|
+
sma3 = sma3.to_i
|
68
|
+
sma4 = sma4.to_i
|
69
|
+
price_key = price_key.to_sym
|
70
|
+
Validation.validate_numeric_data(data, price_key)
|
71
|
+
Validation.validate_length(data, min_data_size(roc4: roc4, sma4: sma4))
|
72
|
+
Validation.validate_date_time_key(data)
|
73
|
+
|
74
|
+
data = data.sort_by { |row| row[:date_time] }
|
75
|
+
|
76
|
+
index = roc4 + sma4 - 2
|
77
|
+
output = []
|
78
|
+
|
79
|
+
while index < data.size
|
80
|
+
date_time = data[index][:date_time]
|
81
|
+
rcma1 = calculate_rcma(data, index, price_key, roc1, sma1)
|
82
|
+
rcma2 = calculate_rcma(data, index, price_key, roc2, sma2)
|
83
|
+
rcma3 = calculate_rcma(data, index, price_key, roc3, sma3)
|
84
|
+
rcma4 = calculate_rcma(data, index, price_key, roc4, sma4)
|
85
|
+
|
86
|
+
kst = (1 * rcma1) + (2 * rcma2) + (3 * rcma3) + (4 * rcma4)
|
87
|
+
|
88
|
+
output << KstValue.new(date_time: date_time, kst: kst)
|
89
|
+
|
90
|
+
index += 1
|
91
|
+
end
|
92
|
+
|
93
|
+
output.sort_by(&:date_time).reverse
|
94
|
+
end
|
95
|
+
|
96
|
+
private_class_method def self.calculate_rcma(data, index, price_key, roc, sma)
|
97
|
+
roc_data = []
|
98
|
+
index_range = (index - sma + 1)..index
|
99
|
+
|
100
|
+
index_range.each do |i|
|
101
|
+
last_price = data[i][price_key]
|
102
|
+
starting_price = data[i - roc + 1][price_key]
|
103
|
+
|
104
|
+
roc_data << (last_price - starting_price) / starting_price * 100
|
105
|
+
end
|
106
|
+
|
107
|
+
ArrayHelper.sum(roc_data) / sma.to_f
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# The value class to be returned by calculations
|
113
|
+
class KstValue
|
114
|
+
|
115
|
+
# @return [String] the date_time of the obversation as it was provided
|
116
|
+
attr_accessor :date_time
|
117
|
+
|
118
|
+
# @return [Float] the kst calculation value
|
119
|
+
attr_accessor :kst
|
120
|
+
|
121
|
+
def initialize(date_time: nil, kst: nil)
|
122
|
+
@date_time = date_time
|
123
|
+
@kst = kst
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Hash] the attributes as a hash
|
127
|
+
def to_hash
|
128
|
+
{ date_time: @date_time, kst: @kst }
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# Moving Average Convergence Divergence
|
3
|
+
class Macd < Indicator
|
4
|
+
|
5
|
+
# Returns the symbol of the technical indicator
|
6
|
+
#
|
7
|
+
# @return [String] A string of the symbol of the technical indicator
|
8
|
+
def self.indicator_symbol
|
9
|
+
"macd"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the name of the technical indicator
|
13
|
+
#
|
14
|
+
# @return [String] A string of the name of the technical indicator
|
15
|
+
def self.indicator_name
|
16
|
+
"Moving Average Convergence Divergence"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns an array of valid keys for options for this technical indicator
|
20
|
+
#
|
21
|
+
# @return [Array] An array of keys as symbols for valid options for this technical indicator
|
22
|
+
def self.valid_options
|
23
|
+
%i(fast_period slow_period signal_period price_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Validates the provided options for this technical indicator
|
27
|
+
#
|
28
|
+
# @param options [Hash] The options for the technical indicator to be validated
|
29
|
+
#
|
30
|
+
# @return [Boolean] Returns true if options are valid or raises a ValidationError if they're not
|
31
|
+
def self.validate_options(options)
|
32
|
+
Validation.validate_options(options, valid_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calculates the minimum number of observations needed to calculate the technical indicator
|
36
|
+
#
|
37
|
+
# @param options [Hash] The options for the technical indicator
|
38
|
+
#
|
39
|
+
# @return [Integer] Returns the minimum number of observations needed to calculate the technical
|
40
|
+
# indicator based on the options provided
|
41
|
+
def self.min_data_size(slow_period: 26, signal_period: 9, **params)
|
42
|
+
slow_period.to_i + signal_period.to_i - 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the moving average convergence divergence (MACD) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/MACD
|
47
|
+
#
|
48
|
+
# @param data [Array] Array of hashes with keys (:date_time, :value)
|
49
|
+
# @param fast_period [Integer] The given period to calculate the fast moving EMA for MACD
|
50
|
+
# @param slow_period [Integer] The given period to calculate the slow moving EMA for MACD
|
51
|
+
# @param signal_period [Integer] The given period to calculate the signal line for MACD
|
52
|
+
# @param price_key [Symbol] The hash key for the price data. Default :value
|
53
|
+
#
|
54
|
+
# @return [Array<MacdValue>] An array of MacdValue instances
|
55
|
+
def self.calculate(data, fast_period: 12, slow_period: 26, signal_period: 9, price_key: :value)
|
56
|
+
fast_period = fast_period.to_i
|
57
|
+
slow_period = slow_period.to_i
|
58
|
+
signal_period = signal_period.to_i
|
59
|
+
price_key = price_key.to_sym
|
60
|
+
Validation.validate_numeric_data(data, price_key)
|
61
|
+
Validation.validate_length(data, min_data_size(slow_period: slow_period, signal_period: signal_period))
|
62
|
+
Validation.validate_date_time_key(data)
|
63
|
+
|
64
|
+
data = data.sort_by { |row| row[:date_time] }
|
65
|
+
|
66
|
+
macd_values = []
|
67
|
+
output = []
|
68
|
+
period_values = []
|
69
|
+
prev_fast_ema = nil
|
70
|
+
prev_signal = nil
|
71
|
+
prev_slow_ema = nil
|
72
|
+
|
73
|
+
data.each do |v|
|
74
|
+
period_values << v[price_key]
|
75
|
+
|
76
|
+
if period_values.size >= fast_period
|
77
|
+
fast_ema = StockCalculation.ema(v[price_key], period_values, fast_period, prev_fast_ema)
|
78
|
+
prev_fast_ema = fast_ema
|
79
|
+
|
80
|
+
if period_values.size == slow_period
|
81
|
+
slow_ema = StockCalculation.ema(v[price_key], period_values, slow_period, prev_slow_ema)
|
82
|
+
prev_slow_ema = slow_ema
|
83
|
+
|
84
|
+
macd = fast_ema - slow_ema
|
85
|
+
macd_values << macd
|
86
|
+
|
87
|
+
if macd_values.size == signal_period
|
88
|
+
signal = StockCalculation.ema(macd, macd_values, signal_period, prev_signal)
|
89
|
+
prev_signal = signal
|
90
|
+
|
91
|
+
output << MacdValue.new(
|
92
|
+
date_time: v[:date_time],
|
93
|
+
macd_line: macd,
|
94
|
+
signal_line: signal,
|
95
|
+
macd_histogram: macd - signal,
|
96
|
+
)
|
97
|
+
|
98
|
+
macd_values.shift
|
99
|
+
end
|
100
|
+
|
101
|
+
period_values.shift
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
output.sort_by(&:date_time).reverse
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
# The value class to be returned by calculations
|
112
|
+
class MacdValue
|
113
|
+
|
114
|
+
# @return [String] the date_time of the obversation as it was provided
|
115
|
+
attr_accessor :date_time
|
116
|
+
|
117
|
+
# @return [Float] the macd_line calculation value
|
118
|
+
attr_accessor :macd_line
|
119
|
+
|
120
|
+
# @return [Float] the macd_histogram calculation value
|
121
|
+
attr_accessor :macd_histogram
|
122
|
+
|
123
|
+
# @return [Float] the signal_line calculation value
|
124
|
+
attr_accessor :signal_line
|
125
|
+
|
126
|
+
def initialize(date_time: nil, macd_line: nil, macd_histogram: nil, signal_line: nil)
|
127
|
+
@date_time = date_time
|
128
|
+
@macd_line = macd_line
|
129
|
+
@macd_histogram = macd_histogram
|
130
|
+
@signal_line = signal_line
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Hash] the attributes as a hash
|
134
|
+
def to_hash
|
135
|
+
{
|
136
|
+
date_time: @date_time,
|
137
|
+
macd_line: @macd_line,
|
138
|
+
macd_histogram: @macd_histogram,
|
139
|
+
signal_line: @signal_line
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|