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,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
|