technical-analysis 0.1.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.
- 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
|