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,127 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# Triple Exponential Average
|
3
|
+
class Trix < 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
|
+
"trix"
|
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
|
+
"Triple Exponential Average"
|
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 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(period: 15, **params)
|
42
|
+
(period.to_i * 3) - 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the triple exponential average (Trix) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/Trix_(technical_analysis)
|
47
|
+
#
|
48
|
+
# @param data [Array] Array of hashes with keys (:date_time, :value)
|
49
|
+
# @param period [Integer] The given period to calculate the EMA for Trix
|
50
|
+
# @param price_key [Symbol] The hash key for the price data. Default :value
|
51
|
+
#
|
52
|
+
# @return [Array<TrixValue>] An array of TrixValue instances
|
53
|
+
def self.calculate(data, period: 15, price_key: :value)
|
54
|
+
period = period.to_i
|
55
|
+
price_key = price_key.to_sym
|
56
|
+
Validation.validate_numeric_data(data, price_key)
|
57
|
+
Validation.validate_length(data, min_data_size(period: period))
|
58
|
+
Validation.validate_date_time_key(data)
|
59
|
+
|
60
|
+
data = data.sort_by { |row| row[:date_time] }
|
61
|
+
|
62
|
+
ema1 = []
|
63
|
+
ema2 = []
|
64
|
+
ema3 = []
|
65
|
+
output = []
|
66
|
+
period_values = []
|
67
|
+
|
68
|
+
data.each do |v|
|
69
|
+
price = v[price_key]
|
70
|
+
period_values << price
|
71
|
+
|
72
|
+
if period_values.size == period
|
73
|
+
ema1_value = StockCalculation.ema(price, period_values, period, ema1.last)
|
74
|
+
ema1 << ema1_value
|
75
|
+
|
76
|
+
if ema1.size == period
|
77
|
+
ema2_value = StockCalculation.ema(ema1_value, ema1, period, ema2.last)
|
78
|
+
ema2 << ema2_value
|
79
|
+
|
80
|
+
if ema2.size == period
|
81
|
+
ema3_value = StockCalculation.ema(ema2_value, ema2, period, ema3.last)
|
82
|
+
ema3 << ema3_value
|
83
|
+
|
84
|
+
if ema3.size == 2
|
85
|
+
prev_ema3, current_ema3 = ema3
|
86
|
+
trix = ((current_ema3 - prev_ema3) / prev_ema3)
|
87
|
+
output << TrixValue.new(date_time: v[:date_time], trix: trix)
|
88
|
+
|
89
|
+
ema3.shift
|
90
|
+
end
|
91
|
+
|
92
|
+
ema2.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
ema1.shift
|
96
|
+
end
|
97
|
+
|
98
|
+
period_values.shift
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
output.sort_by(&:date_time).reverse
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
# The value class to be returned by calculations
|
108
|
+
class TrixValue
|
109
|
+
|
110
|
+
# @return [String] the date_time of the obversation as it was provided
|
111
|
+
attr_accessor :date_time
|
112
|
+
|
113
|
+
# @return [Float] the trix calculation value
|
114
|
+
attr_accessor :trix
|
115
|
+
|
116
|
+
def initialize(date_time: nil, trix: nil)
|
117
|
+
@date_time = date_time
|
118
|
+
@trix = trix
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Hash] the attributes as a hash
|
122
|
+
def to_hash
|
123
|
+
{ date_time: @date_time, trix: @trix }
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# True Strength Index
|
3
|
+
class Tsi < 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
|
+
"tsi"
|
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
|
+
"True Strength Index"
|
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(low_period high_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(low_period: 13, high_period: 25, **params)
|
42
|
+
low_period.to_i + high_period.to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the true strength index (TSI) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/True_strength_index
|
47
|
+
#
|
48
|
+
# @param data [Array] Array of hashes with keys (:date_time, :value)
|
49
|
+
# @param high_period [Integer] The given high period to calculate the EMA
|
50
|
+
# @param low_period [Integer] The given low period to calculate the EMA
|
51
|
+
# @param price_key [Symbol] The hash key for the price data. Default :value
|
52
|
+
#
|
53
|
+
# @return [Array<TsiValue>] An array of TsiValue instances
|
54
|
+
def self.calculate(data, low_period: 13, high_period: 25, price_key: :value)
|
55
|
+
low_period = low_period.to_i
|
56
|
+
high_period = high_period.to_i
|
57
|
+
price_key = price_key.to_sym
|
58
|
+
Validation.validate_numeric_data(data, price_key)
|
59
|
+
Validation.validate_length(data, min_data_size(low_period: low_period, high_period: high_period))
|
60
|
+
Validation.validate_date_time_key(data)
|
61
|
+
|
62
|
+
data = data.sort_by { |row| row[:date_time] }
|
63
|
+
|
64
|
+
high_emas = []
|
65
|
+
high_multiplier = (2.0 / (high_period + 1.0))
|
66
|
+
low_emas = []
|
67
|
+
low_multiplier = (2.0 / (low_period + 1.0))
|
68
|
+
momentum_values = []
|
69
|
+
output = []
|
70
|
+
prev_price = data.shift[price_key]
|
71
|
+
|
72
|
+
data.each do |v|
|
73
|
+
current_price = v[price_key]
|
74
|
+
momentum = current_price - prev_price
|
75
|
+
momentum_hash = { value: momentum, abs_value: momentum.abs }
|
76
|
+
|
77
|
+
momentum_values << momentum_hash
|
78
|
+
|
79
|
+
if momentum_values.size == high_period
|
80
|
+
high_emas << process_ema(momentum_hash, momentum_values, high_multiplier, high_period, high_emas)
|
81
|
+
|
82
|
+
if high_emas.size == low_period
|
83
|
+
low_ema = process_ema(high_emas.last, high_emas, low_multiplier, low_period, low_emas)
|
84
|
+
low_emas << low_ema
|
85
|
+
|
86
|
+
output << TsiValue.new(
|
87
|
+
date_time: v[:date_time],
|
88
|
+
tsi: ((100 * (low_ema[:value] / low_ema[:abs_value])))
|
89
|
+
)
|
90
|
+
|
91
|
+
low_emas.shift if low_emas.size > 1 # Only need to retain the last low_ema
|
92
|
+
high_emas.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
momentum_values.shift
|
96
|
+
end
|
97
|
+
|
98
|
+
prev_price = current_price
|
99
|
+
end
|
100
|
+
|
101
|
+
output.sort_by(&:date_time).reverse
|
102
|
+
end
|
103
|
+
|
104
|
+
private_class_method def self.process_ema(current_value, data, multiplier, period, store)
|
105
|
+
if store.empty?
|
106
|
+
value = ArrayHelper.average(data.map { |d| d[:value] })
|
107
|
+
abs_value = ArrayHelper.average(data.map { |d| d[:abs_value] })
|
108
|
+
else
|
109
|
+
prev_value = store.last
|
110
|
+
value = ((multiplier * (current_value[:value] - prev_value[:value])) + prev_value[:value])
|
111
|
+
abs_value = ((multiplier * (current_value[:abs_value] - prev_value[:abs_value])) + prev_value[:abs_value])
|
112
|
+
end
|
113
|
+
|
114
|
+
{ value: value, abs_value: abs_value }
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
# The value class to be returned by calculations
|
120
|
+
class TsiValue
|
121
|
+
|
122
|
+
# @return [String] the date_time of the obversation as it was provided
|
123
|
+
attr_accessor :date_time
|
124
|
+
|
125
|
+
# @return [Float] the tsi calculation value
|
126
|
+
attr_accessor :tsi
|
127
|
+
|
128
|
+
def initialize(date_time: nil, tsi: nil)
|
129
|
+
@date_time = date_time
|
130
|
+
@tsi = tsi
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Hash] the attributes as a hash
|
134
|
+
def to_hash
|
135
|
+
{ date_time: @date_time, tsi: @tsi }
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# Ultimate Oscillator
|
3
|
+
class Uo < 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
|
+
"uo"
|
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
|
+
"Ultimate Oscillator"
|
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(short_period medium_period long_period short_weight medium_weight long_weight)
|
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(long_period: 28, **params)
|
42
|
+
long_period.to_i + 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the ultimate oscillator (UO) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/Ultimate_oscillator
|
47
|
+
#
|
48
|
+
# @param data [Array] Array of hashes with keys (:date_time, :high, :low, :close)
|
49
|
+
# @param short_period [Integer] The given short period
|
50
|
+
# @param medium_period [Integer] The given medium period
|
51
|
+
# @param long_period [Integer] The given long period
|
52
|
+
# @param short_weight [Float] Weight of short Buying Pressure average for UO
|
53
|
+
# @param medium_weight [Float] Weight of medium Buying Pressure average for UO
|
54
|
+
# @param long_weight [Float] Weight of long Buying Pressure average for UO
|
55
|
+
#
|
56
|
+
# @return [Array<UoValue>] An array of UoValue instances
|
57
|
+
def self.calculate(data, short_period: 7, medium_period: 14, long_period: 28, short_weight: 4, medium_weight: 2, long_weight: 1)
|
58
|
+
short_period = short_period.to_i
|
59
|
+
medium_period = medium_period.to_i
|
60
|
+
long_period = long_period.to_i
|
61
|
+
short_weight = short_weight.to_f
|
62
|
+
medium_weight = medium_weight.to_f
|
63
|
+
long_weight = long_weight.to_f
|
64
|
+
Validation.validate_numeric_data(data, :high, :low, :close)
|
65
|
+
Validation.validate_length(data, min_data_size(long_period: long_period))
|
66
|
+
Validation.validate_date_time_key(data)
|
67
|
+
|
68
|
+
data = data.sort_by { |row| row[:date_time] }
|
69
|
+
|
70
|
+
output = []
|
71
|
+
period_values = []
|
72
|
+
prior_close = data.shift[:close]
|
73
|
+
sum_of_weights = ArrayHelper.sum([short_weight, medium_weight, long_weight])
|
74
|
+
|
75
|
+
data.each do |v|
|
76
|
+
min_low_p_close = [v[:low], prior_close].min
|
77
|
+
max_high_p_close = [v[:high], prior_close].max
|
78
|
+
|
79
|
+
buying_pressure = v[:close] - min_low_p_close
|
80
|
+
true_range = max_high_p_close - min_low_p_close
|
81
|
+
|
82
|
+
period_values << { buying_pressure: buying_pressure, true_range: true_range }
|
83
|
+
|
84
|
+
if period_values.size == long_period
|
85
|
+
short_average = calculate_average(short_period, period_values)
|
86
|
+
medium_average = calculate_average(medium_period, period_values)
|
87
|
+
long_average = calculate_average(long_period, period_values)
|
88
|
+
uo = 100 * (((short_weight * short_average) + (medium_weight * medium_average) + (long_weight * long_average)) / (sum_of_weights))
|
89
|
+
|
90
|
+
output << UoValue.new(date_time: v[:date_time], uo: uo)
|
91
|
+
|
92
|
+
period_values.shift
|
93
|
+
end
|
94
|
+
|
95
|
+
prior_close = v[:close]
|
96
|
+
end
|
97
|
+
|
98
|
+
output.sort_by(&:date_time).reverse
|
99
|
+
end
|
100
|
+
|
101
|
+
private_class_method def self.calculate_average(period, data)
|
102
|
+
buying_pressures_sum = ArrayHelper.sum(data.last(period).map { |d| d[:buying_pressure] })
|
103
|
+
true_ranges_sum = ArrayHelper.sum(data.last(period).map { |d| d[:true_range] })
|
104
|
+
|
105
|
+
buying_pressures_sum / true_ranges_sum
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
# The value class to be returned by calculations
|
111
|
+
class UoValue
|
112
|
+
|
113
|
+
# @return [String] the date_time of the obversation as it was provided
|
114
|
+
attr_accessor :date_time
|
115
|
+
|
116
|
+
# @return [Float] the uo calculation value
|
117
|
+
attr_accessor :uo
|
118
|
+
|
119
|
+
def initialize(date_time: nil, uo: nil)
|
120
|
+
@date_time = date_time
|
121
|
+
@uo = uo
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return [Hash] the attributes as a hash
|
125
|
+
def to_hash
|
126
|
+
{ date_time: @date_time, uo: @uo }
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module TechnicalAnalysis
|
2
|
+
# Vortex Indicator
|
3
|
+
class Vi < 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
|
+
"vi"
|
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
|
+
"Vortex Indicator"
|
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: 14)
|
42
|
+
period.to_i + 1
|
43
|
+
end
|
44
|
+
|
45
|
+
# Calculates the vortex indicator (VI) for the data over the given period
|
46
|
+
# https://en.wikipedia.org/wiki/Vortex_indicator
|
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 VI
|
50
|
+
#
|
51
|
+
# @return [Array<Hash>] An array of ViValue instances
|
52
|
+
def self.calculate(data, period: 14)
|
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
|
+
prev_price = data.shift
|
63
|
+
|
64
|
+
data.each do |v|
|
65
|
+
positive_vm = (v[:high] - prev_price[:low]).abs
|
66
|
+
negative_vm = (v[:low] - prev_price[:high]).abs
|
67
|
+
tr = [(v[:high] - v[:low]), (v[:high] - prev_price[:close]).abs, (v[:low] - prev_price[:close]).abs].max
|
68
|
+
|
69
|
+
period_values << { pos_vm: positive_vm, neg_vm: negative_vm, tr: tr }
|
70
|
+
|
71
|
+
if period_values.size == period
|
72
|
+
pos_vm_period = ArrayHelper.sum(period_values.map { |pv| pv[:pos_vm] })
|
73
|
+
neg_vm_period = ArrayHelper.sum(period_values.map { |pv| pv[:neg_vm] })
|
74
|
+
tr_period = ArrayHelper.sum(period_values.map { |pv| pv[:tr] })
|
75
|
+
|
76
|
+
output << ViValue.new(
|
77
|
+
date_time: v[:date_time],
|
78
|
+
positive_vi: (pos_vm_period / tr_period),
|
79
|
+
negative_vi: (neg_vm_period / tr_period),
|
80
|
+
)
|
81
|
+
|
82
|
+
period_values.shift
|
83
|
+
end
|
84
|
+
|
85
|
+
prev_price = v
|
86
|
+
end
|
87
|
+
|
88
|
+
output.sort_by(&:date_time).reverse
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
# The value class to be returned by calculations
|
94
|
+
class ViValue
|
95
|
+
|
96
|
+
# @return [String] the date_time of the obversation as it was provided
|
97
|
+
attr_accessor :date_time
|
98
|
+
|
99
|
+
# @return [Float] the positive Vortex Indicator value
|
100
|
+
attr_accessor :positive_vi
|
101
|
+
|
102
|
+
# @return [Float] the negative Vortex Indicator value
|
103
|
+
attr_accessor :negative_vi
|
104
|
+
|
105
|
+
def initialize(date_time: nil, positive_vi: nil, negative_vi: nil)
|
106
|
+
@date_time = date_time
|
107
|
+
@positive_vi = positive_vi
|
108
|
+
@negative_vi = negative_vi
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return [Hash] the attributes as a hash
|
112
|
+
def to_hash
|
113
|
+
{ date_time: @date_time, positive_vi: @positive_vi, negative_vi: @negative_vi }
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|