indicator_hub 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/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +551 -0
- data/Rakefile +12 -0
- data/exe/indicator_hub +4 -0
- data/lib/indicator_hub/calculation_helpers.rb +196 -0
- data/lib/indicator_hub/indicators/adi.rb +37 -0
- data/lib/indicator_hub/indicators/adtv.rb +27 -0
- data/lib/indicator_hub/indicators/adx.rb +102 -0
- data/lib/indicator_hub/indicators/ao.rb +39 -0
- data/lib/indicator_hub/indicators/atr.rb +45 -0
- data/lib/indicator_hub/indicators/base_indicator.rb +68 -0
- data/lib/indicator_hub/indicators/bb.rb +44 -0
- data/lib/indicator_hub/indicators/cci.rb +41 -0
- data/lib/indicator_hub/indicators/cmf.rb +54 -0
- data/lib/indicator_hub/indicators/cmo.rb +49 -0
- data/lib/indicator_hub/indicators/cr.rb +26 -0
- data/lib/indicator_hub/indicators/dc.rb +40 -0
- data/lib/indicator_hub/indicators/dlr.rb +27 -0
- data/lib/indicator_hub/indicators/dpo.rb +32 -0
- data/lib/indicator_hub/indicators/dr.rb +27 -0
- data/lib/indicator_hub/indicators/ema.rb +40 -0
- data/lib/indicator_hub/indicators/envelopes_ema.rb +36 -0
- data/lib/indicator_hub/indicators/eom.rb +48 -0
- data/lib/indicator_hub/indicators/fi.rb +45 -0
- data/lib/indicator_hub/indicators/ichimoku.rb +76 -0
- data/lib/indicator_hub/indicators/imi.rb +48 -0
- data/lib/indicator_hub/indicators/kc.rb +46 -0
- data/lib/indicator_hub/indicators/kst.rb +82 -0
- data/lib/indicator_hub/indicators/macd.rb +46 -0
- data/lib/indicator_hub/indicators/mfi.rb +62 -0
- data/lib/indicator_hub/indicators/mi.rb +81 -0
- data/lib/indicator_hub/indicators/nvi.rb +42 -0
- data/lib/indicator_hub/indicators/obv.rb +41 -0
- data/lib/indicator_hub/indicators/obv_mean.rb +42 -0
- data/lib/indicator_hub/indicators/pivot_points.rb +44 -0
- data/lib/indicator_hub/indicators/price_channel.rb +38 -0
- data/lib/indicator_hub/indicators/qstick.rb +36 -0
- data/lib/indicator_hub/indicators/rmi.rb +48 -0
- data/lib/indicator_hub/indicators/roc.rb +37 -0
- data/lib/indicator_hub/indicators/rsi.rb +67 -0
- data/lib/indicator_hub/indicators/sma.rb +32 -0
- data/lib/indicator_hub/indicators/so.rb +76 -0
- data/lib/indicator_hub/indicators/trix.rb +53 -0
- data/lib/indicator_hub/indicators/tsi.rb +67 -0
- data/lib/indicator_hub/indicators/uo.rb +67 -0
- data/lib/indicator_hub/indicators/vi.rb +54 -0
- data/lib/indicator_hub/indicators/volume_oscillator.rb +55 -0
- data/lib/indicator_hub/indicators/vpt.rb +35 -0
- data/lib/indicator_hub/indicators/vwap.rb +33 -0
- data/lib/indicator_hub/indicators/wilders_smoothing.rb +36 -0
- data/lib/indicator_hub/indicators/wma.rb +36 -0
- data/lib/indicator_hub/indicators/wr.rb +36 -0
- data/lib/indicator_hub/series.rb +79 -0
- data/lib/indicator_hub/talib_adapter.rb +23 -0
- data/lib/indicator_hub/validation.rb +49 -0
- data/lib/indicator_hub/version.rb +6 -0
- data/lib/indicator_hub.rb +485 -0
- data/sig/indicator_hub.rbs +4 -0
- metadata +112 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module IndicatorHub
|
|
7
|
+
# Calculation helpers for technical analysis math.
|
|
8
|
+
# Provides common mathematical functions used in various indicator calculations.
|
|
9
|
+
module CalculationHelpers
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
# Sums up the given numerical data.
|
|
13
|
+
# @param data [Array<Numeric>] The input data array.
|
|
14
|
+
# @return [Float] The sum of all elements.
|
|
15
|
+
sig { params(data: T::Array[Numeric]).returns(Float) }
|
|
16
|
+
def self.sum(data)
|
|
17
|
+
data.sum(0.0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Calculates the arithmetic mean (average) of the data.
|
|
21
|
+
# @param data [Array<Numeric>] The input data array.
|
|
22
|
+
# @return [Float] The average of the elements.
|
|
23
|
+
sig { params(data: T::Array[Numeric]).returns(Float) }
|
|
24
|
+
def self.average(data)
|
|
25
|
+
return 0.0 if data.empty?
|
|
26
|
+
|
|
27
|
+
sum(data) / data.size.to_f
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @see average
|
|
31
|
+
sig { params(data: T::Array[Numeric]).returns(Float) }
|
|
32
|
+
def self.mean(data)
|
|
33
|
+
average(data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Calculates the sample variance of the data.
|
|
37
|
+
# @param data [Array<Numeric>] The input data array.
|
|
38
|
+
# @return [Float] The sample variance.
|
|
39
|
+
sig { params(data: T::Array[Numeric]).returns(Float) }
|
|
40
|
+
def self.sample_variance(data)
|
|
41
|
+
return 0.0 if data.size <= 1
|
|
42
|
+
|
|
43
|
+
m = mean(data)
|
|
44
|
+
sum_sq_diff = data.inject(0.0) { |accum, i| accum + ((i - m)**2) }
|
|
45
|
+
sum_sq_diff / (data.size - 1).to_f
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Calculates the standard deviation of the data.
|
|
49
|
+
# @param data [Array<Numeric>] The input data array.
|
|
50
|
+
# @return [Float] The standard deviation.
|
|
51
|
+
sig { params(data: T::Array[Numeric]).returns(Float) }
|
|
52
|
+
def self.standard_deviation(data)
|
|
53
|
+
Math.sqrt(sample_variance(data))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Calculates the mean absolute deviation (MAD) of the data.
|
|
57
|
+
# @param data [Array<Numeric>] The input data array.
|
|
58
|
+
# @param mean [Float] The mean of the data.
|
|
59
|
+
# @return [Float] The MAD of the elements.
|
|
60
|
+
sig { params(data: T::Array[Numeric], mean: Float).returns(Float) }
|
|
61
|
+
def self.mean_absolute_deviation(data, mean)
|
|
62
|
+
return 0.0 if data.empty?
|
|
63
|
+
|
|
64
|
+
sum_abs_diff = data.inject(0.0) { |accum, i| accum + (i - mean).abs }
|
|
65
|
+
sum_abs_diff / data.size.to_f
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Wilder's Smoothing for RSI.
|
|
69
|
+
# @param prev_avg [Numeric] The previous average.
|
|
70
|
+
# @param current [Numeric] The current value.
|
|
71
|
+
# @param period [Integer] The smoothing period.
|
|
72
|
+
# @return [Float] The smoothed value.
|
|
73
|
+
sig { params(prev_avg: Numeric, current: Numeric, period: Integer).returns(Float) }
|
|
74
|
+
def self.wilder_smoothing(prev_avg, current, period)
|
|
75
|
+
((prev_avg.to_f * (period - 1)) + current.to_f) / period.to_f
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Calculates the True Range (TR).
|
|
79
|
+
# @param current_high [Numeric] The high price of the current period.
|
|
80
|
+
# @param current_low [Numeric] The low price of the current period.
|
|
81
|
+
# @param previous_close [Numeric] The close price of the previous period.
|
|
82
|
+
# @return [Float] The calculated True Range.
|
|
83
|
+
sig { params(current_high: Numeric, current_low: Numeric, previous_close: Numeric).returns(Float) }
|
|
84
|
+
def self.true_range(current_high, current_low, previous_close)
|
|
85
|
+
[
|
|
86
|
+
(current_high.to_f - current_low.to_f),
|
|
87
|
+
(current_high.to_f - previous_close.to_f).abs,
|
|
88
|
+
(current_low.to_f - previous_close.to_f).abs
|
|
89
|
+
].max.to_f
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Calculates the Typical Price.
|
|
93
|
+
# @param high [Numeric] The high price.
|
|
94
|
+
# @param low [Numeric] The low price.
|
|
95
|
+
# @param close [Numeric] The close price.
|
|
96
|
+
# @return [Float] (high + low + close) / 3.0
|
|
97
|
+
sig { params(high: Numeric, low: Numeric, close: Numeric).returns(Float) }
|
|
98
|
+
def self.typical_price(high, low, close)
|
|
99
|
+
(high.to_f + low.to_f + close.to_f) / 3.0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Calculates the Exponential Moving Average (EMA).
|
|
103
|
+
# @param current_value [Numeric] The current data point.
|
|
104
|
+
# @param data [Array<Numeric>] Historical data (used for seed calculation).
|
|
105
|
+
# @param period [Integer] The EMA period.
|
|
106
|
+
# @param prev_value [Numeric, nil] The previous EMA value.
|
|
107
|
+
# @return [Float] The calculated EMA value.
|
|
108
|
+
sig { params(current_value: Numeric, data: T::Array[Numeric], period: Integer, prev_value: T.nilable(Numeric)).returns(Float) }
|
|
109
|
+
def self.ema(current_value, data, period, prev_value)
|
|
110
|
+
if prev_value.nil?
|
|
111
|
+
average(data)
|
|
112
|
+
else
|
|
113
|
+
((current_value.to_f - prev_value.to_f) * (2.0 / (period + 1.0))) + prev_value.to_f
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Calculates the Weighted Moving Average (WMA) of the data.
|
|
118
|
+
# @param data [Array<Numeric>] The input data array.
|
|
119
|
+
# @return [Float] The WMA of the data.
|
|
120
|
+
sig { params(data: T::Array[Numeric]).returns(Float) }
|
|
121
|
+
def self.wma(data)
|
|
122
|
+
return 0.0 if data.empty?
|
|
123
|
+
|
|
124
|
+
divisor = (data.size * (data.size + 1) / 2.0)
|
|
125
|
+
sum = 0.0
|
|
126
|
+
data.each_with_index do |v, i|
|
|
127
|
+
sum += v.to_f * (i + 1) / divisor
|
|
128
|
+
end
|
|
129
|
+
sum
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Calculates the maximum value in a sliding window.
|
|
133
|
+
# @param data [Array<Numeric>] The input data array.
|
|
134
|
+
# @param period [Integer] The window period.
|
|
135
|
+
# @return [Array<Float, nil>]
|
|
136
|
+
sig { params(data: T::Array[Numeric], period: Integer).returns(T::Array[T.nilable(Float)]) }
|
|
137
|
+
def self.max(data, period)
|
|
138
|
+
output = []
|
|
139
|
+
window = []
|
|
140
|
+
data.each do |v|
|
|
141
|
+
window << v.to_f
|
|
142
|
+
if window.size == period
|
|
143
|
+
output << window.max
|
|
144
|
+
window.shift
|
|
145
|
+
else
|
|
146
|
+
output << nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
output
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Calculates the minimum value in a sliding window.
|
|
153
|
+
# @param data [Array<Numeric>] The input data array.
|
|
154
|
+
# @param period [Integer] The window period.
|
|
155
|
+
# @return [Array<Float, nil>]
|
|
156
|
+
sig { params(data: T::Array[Numeric], period: Integer).returns(T::Array[T.nilable(Float)]) }
|
|
157
|
+
def self.min(data, period)
|
|
158
|
+
output = []
|
|
159
|
+
window = []
|
|
160
|
+
data.each do |v|
|
|
161
|
+
window << v.to_f
|
|
162
|
+
if window.size == period
|
|
163
|
+
output << window.min
|
|
164
|
+
window.shift
|
|
165
|
+
else
|
|
166
|
+
output << nil
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
output
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Simple Moving Average for numeric arrays.
|
|
173
|
+
# @param data [Array<Numeric, nil>] The input data array.
|
|
174
|
+
# @param period [Integer] The window period.
|
|
175
|
+
# @return [Array<Float, nil>]
|
|
176
|
+
sig { params(data: T::Array[T.nilable(Numeric)], period: Integer).returns(T::Array[T.nilable(Float)]) }
|
|
177
|
+
def self.sma(data, period)
|
|
178
|
+
output = []
|
|
179
|
+
window = []
|
|
180
|
+
data.each do |v|
|
|
181
|
+
if v.nil?
|
|
182
|
+
output << nil
|
|
183
|
+
next
|
|
184
|
+
end
|
|
185
|
+
window << v.to_f
|
|
186
|
+
if window.size == period
|
|
187
|
+
output << average(window)
|
|
188
|
+
window.shift
|
|
189
|
+
else
|
|
190
|
+
output << nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
output
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Accumulation/Distribution Index (ADI).
|
|
8
|
+
# ADI uses volume and price to assess whether a stock is being accumulated or distributed.
|
|
9
|
+
class ADI
|
|
10
|
+
# Calculates the Accumulation/Distribution Index.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @return [Array<Float, nil>] The calculated ADI values.
|
|
13
|
+
def self.calculate(data)
|
|
14
|
+
ad = 0.0
|
|
15
|
+
output = []
|
|
16
|
+
|
|
17
|
+
data.each do |values|
|
|
18
|
+
high = values[:high]
|
|
19
|
+
low = values[:low]
|
|
20
|
+
close = values[:close]
|
|
21
|
+
volume = values[:volume]
|
|
22
|
+
|
|
23
|
+
clv = if high == low
|
|
24
|
+
0.0
|
|
25
|
+
else
|
|
26
|
+
((close - low) - (high - close)) / (high - low).to_f
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
ad += (clv * volume)
|
|
30
|
+
output << ad
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
output
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Average Daily Trading Volume (ADTV).
|
|
8
|
+
# ADTV is the average number of shares traded within a day in a given stock.
|
|
9
|
+
class ADTV
|
|
10
|
+
# Calculates the Average Daily Trading Volume.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @param period [Integer] The period for calculating the average (default: 20).
|
|
13
|
+
# @return [Array<Float, nil>] The calculated ADTV values.
|
|
14
|
+
def self.calculate(data, period: 20)
|
|
15
|
+
output = []
|
|
16
|
+
volumes = data.map { |v| v[:volume] }
|
|
17
|
+
|
|
18
|
+
(period - 1).upto(volumes.size - 1) do |i|
|
|
19
|
+
window = volumes[(i - period + 1)..i]
|
|
20
|
+
output << CalculationHelpers.average(window)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Array.new(data.size - output.size, nil) + output
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Average Directional Index (ADX).
|
|
8
|
+
# ADX is used to quantify trend strength. It is calculated based on the
|
|
9
|
+
# moving average of price range expansion over a given period of time.
|
|
10
|
+
class ADX
|
|
11
|
+
# Calculates the Average Directional Index.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The period for ADX calculation (default: 14).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated ADX values.
|
|
15
|
+
def self.calculate(data, period: 14)
|
|
16
|
+
output = []
|
|
17
|
+
plus_dm = []
|
|
18
|
+
minus_dm = []
|
|
19
|
+
tr = []
|
|
20
|
+
|
|
21
|
+
data.each_with_index do |val, i|
|
|
22
|
+
if i.zero?
|
|
23
|
+
plus_dm << 0.0
|
|
24
|
+
minus_dm << 0.0
|
|
25
|
+
tr << (val[:high] - val[:low])
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
prev = data[i - 1]
|
|
30
|
+
high_diff = val[:high] - prev[:high]
|
|
31
|
+
low_diff = prev[:low] - val[:low]
|
|
32
|
+
|
|
33
|
+
plus_dm << if high_diff > low_diff && high_diff.positive?
|
|
34
|
+
high_diff
|
|
35
|
+
else
|
|
36
|
+
0.0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
minus_dm << if low_diff > high_diff && low_diff.positive?
|
|
40
|
+
low_diff
|
|
41
|
+
else
|
|
42
|
+
0.0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
tr << CalculationHelpers.true_range(val[:high], val[:low], prev[:close])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
smoothed_plus_dm = smooth(plus_dm, period)
|
|
49
|
+
smoothed_minus_dm = smooth(minus_dm, period)
|
|
50
|
+
smoothed_tr = smooth(tr, period)
|
|
51
|
+
|
|
52
|
+
dx_values = []
|
|
53
|
+
smoothed_plus_dm.each_with_index do |s_plus, i|
|
|
54
|
+
s_minus = smoothed_minus_dm[i]
|
|
55
|
+
s_tr = smoothed_tr[i]
|
|
56
|
+
|
|
57
|
+
plus_di = 100.0 * (s_plus / s_tr)
|
|
58
|
+
minus_di = 100.0 * (s_minus / s_tr)
|
|
59
|
+
|
|
60
|
+
dx = 100.0 * (plus_di - minus_di).abs / (plus_di + minus_di)
|
|
61
|
+
dx_values << dx
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ADX is SMA of DX for the first period, then smoothed
|
|
65
|
+
adx = 0.0
|
|
66
|
+
dx_values.each_with_index do |dx, i|
|
|
67
|
+
if i < period - 1
|
|
68
|
+
output << nil
|
|
69
|
+
elsif i == period - 1
|
|
70
|
+
adx = CalculationHelpers.average(dx_values[0..i])
|
|
71
|
+
output << adx
|
|
72
|
+
else
|
|
73
|
+
adx = ((adx * (period - 1)) + dx) / period.to_f
|
|
74
|
+
output << adx
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Prepend nil for the initial period where TR/DM are being calculated
|
|
79
|
+
Array.new(period - 1, nil) + output
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.smooth(data, period)
|
|
83
|
+
smoothed = []
|
|
84
|
+
current_sum = 0.0
|
|
85
|
+
|
|
86
|
+
data.each_with_index do |val, i|
|
|
87
|
+
if i < period - 1
|
|
88
|
+
current_sum += val
|
|
89
|
+
elsif i == period - 1
|
|
90
|
+
current_sum += val
|
|
91
|
+
smoothed << current_sum
|
|
92
|
+
else
|
|
93
|
+
prev_smoothed = smoothed.last
|
|
94
|
+
new_smoothed = prev_smoothed - (prev_smoothed / period.to_f) + val
|
|
95
|
+
smoothed << new_smoothed
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
smoothed
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Awesome Oscillator (AO).
|
|
8
|
+
# AO is used to measure market momentum. It calculates the difference
|
|
9
|
+
# between a 34-period and 5-period Simple Moving Average of the bar's midpoints.
|
|
10
|
+
class AO
|
|
11
|
+
# Calculates the Awesome Oscillator.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param short_period [Integer] The short SMA period (default: 5).
|
|
14
|
+
# @param long_period [Integer] The long SMA period (default: 34).
|
|
15
|
+
# @return [Array<Float, nil>] The calculated AO values.
|
|
16
|
+
def self.calculate(data, short_period: 5, long_period: 34)
|
|
17
|
+
midpoints = data.map { |v| (v[:high] + v[:low]) / 2.0 }
|
|
18
|
+
output = []
|
|
19
|
+
|
|
20
|
+
midpoints.each_with_index do |_, i|
|
|
21
|
+
if i < long_period - 1
|
|
22
|
+
output << nil
|
|
23
|
+
next
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
short_window = midpoints[(i - short_period + 1)..i]
|
|
27
|
+
long_window = midpoints[(i - long_period + 1)..i]
|
|
28
|
+
|
|
29
|
+
short_sma = CalculationHelpers.average(short_window)
|
|
30
|
+
long_sma = CalculationHelpers.average(long_window)
|
|
31
|
+
|
|
32
|
+
output << (short_sma - long_sma)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
output
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Average True Range (ATR).
|
|
8
|
+
# ATR is a volatility indicator that shows how much an asset moves, on average, during a given time frame.
|
|
9
|
+
class ATR
|
|
10
|
+
# Calculates the Average True Range.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @param period [Integer] The period for ATR (default: 14).
|
|
13
|
+
# @return [Array<Float, nil>] The calculated ATR values.
|
|
14
|
+
def self.calculate(data, period: 14)
|
|
15
|
+
output = []
|
|
16
|
+
tr_values = []
|
|
17
|
+
prev_close = nil
|
|
18
|
+
current_atr = nil
|
|
19
|
+
|
|
20
|
+
data.each_with_index do |v, i|
|
|
21
|
+
tr = if i.zero?
|
|
22
|
+
v[:high] - v[:low]
|
|
23
|
+
else
|
|
24
|
+
CalculationHelpers.true_range(v[:high], v[:low], prev_close)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
tr_values << tr
|
|
28
|
+
prev_close = v[:close]
|
|
29
|
+
|
|
30
|
+
if tr_values.size < period
|
|
31
|
+
output << nil
|
|
32
|
+
elsif tr_values.size == period
|
|
33
|
+
current_atr = CalculationHelpers.average(tr_values)
|
|
34
|
+
output << current_atr
|
|
35
|
+
else
|
|
36
|
+
current_atr = CalculationHelpers.wilder_smoothing(current_atr, tr, period)
|
|
37
|
+
output << current_atr
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
output
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../validation"
|
|
4
|
+
require_relative "../series"
|
|
5
|
+
require_relative "../calculation_helpers"
|
|
6
|
+
|
|
7
|
+
module IndicatorHub
|
|
8
|
+
module Indicators
|
|
9
|
+
# Abstract base class for all technical analysis indicators.
|
|
10
|
+
# Implements common initialization, data normalization, and validation logic.
|
|
11
|
+
class BaseIndicator
|
|
12
|
+
# @return [Series] The normalized series data.
|
|
13
|
+
attr_reader :data
|
|
14
|
+
# @return [Hash] The configuration options for the indicator.
|
|
15
|
+
attr_reader :options
|
|
16
|
+
|
|
17
|
+
# Initializes a new indicator instance.
|
|
18
|
+
# @param data [Array, Series] The input data series.
|
|
19
|
+
# @param options [Hash] Configuration options (e.g., period, field).
|
|
20
|
+
def initialize(data, options = {})
|
|
21
|
+
@data = data.is_a?(Series) ? data : Series.new(data)
|
|
22
|
+
@options = options
|
|
23
|
+
validate!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Abstract method to be implemented by concrete indicator classes.
|
|
27
|
+
# @raise [NotImplementedError] if not overridden.
|
|
28
|
+
def calculate
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #calculate"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the data normalized as a simple numeric array.
|
|
33
|
+
# @return [Array<Float>]
|
|
34
|
+
def numeric_data
|
|
35
|
+
data.to_a(field: options[:field] || :close)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the data normalized as an OHLCV hash array.
|
|
39
|
+
# @return [Array<Hash>]
|
|
40
|
+
def ohlc_data
|
|
41
|
+
data.to_ohlc
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Defines the list of valid option keys for this indicator.
|
|
45
|
+
# @return [Array<Symbol>]
|
|
46
|
+
def self.valid_options
|
|
47
|
+
[:field]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Defines the minimum number of data points required for calculation.
|
|
51
|
+
# @param options [Hash] The current indicator options.
|
|
52
|
+
# @return [Integer]
|
|
53
|
+
def self.min_data_size(_options)
|
|
54
|
+
1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Validates options, numeric integrity, and minimum data length.
|
|
60
|
+
# @raise [Validation::Error] if validation fails.
|
|
61
|
+
def validate!
|
|
62
|
+
Validation.validate_options(options, self.class.valid_options)
|
|
63
|
+
Validation.validate_numeric_data(numeric_data)
|
|
64
|
+
Validation.validate_length(numeric_data, self.class.min_data_size(options))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Bollinger Bands (BB).
|
|
8
|
+
# Bollinger Bands are a type of statistical chart characterizing the prices
|
|
9
|
+
# and volatility over time of a financial instrument or commodity.
|
|
10
|
+
class BB
|
|
11
|
+
# Calculates the Bollinger Bands.
|
|
12
|
+
# @param data [Array<Numeric>] The input data points.
|
|
13
|
+
# @param period [Integer] The SMA period for the middle band (default: 20).
|
|
14
|
+
# @param standard_deviations [Numeric] The number of standard deviations for the bands (default: 2).
|
|
15
|
+
# @return [Array<Hash, nil>] The calculated Bollinger Bands (upper, middle, lower).
|
|
16
|
+
def self.calculate(data, period: 20, standard_deviations: 2)
|
|
17
|
+
return [] if data.size < period
|
|
18
|
+
|
|
19
|
+
period_values = []
|
|
20
|
+
output = []
|
|
21
|
+
|
|
22
|
+
data.each do |v|
|
|
23
|
+
period_values << v
|
|
24
|
+
if period_values.size == period
|
|
25
|
+
mb = CalculationHelpers.average(period_values)
|
|
26
|
+
sd = CalculationHelpers.standard_deviation(period_values)
|
|
27
|
+
ub = mb + (standard_deviations * sd)
|
|
28
|
+
lb = mb - (standard_deviations * sd)
|
|
29
|
+
|
|
30
|
+
output << {
|
|
31
|
+
upper: ub,
|
|
32
|
+
middle: mb,
|
|
33
|
+
lower: lb
|
|
34
|
+
}
|
|
35
|
+
period_values.shift
|
|
36
|
+
else
|
|
37
|
+
output << nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
output
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Commodity Channel Index (CCI).
|
|
8
|
+
# CCI measures the current price level relative to an average price level over a given period of time.
|
|
9
|
+
class CCI
|
|
10
|
+
# Calculates the Commodity Channel Index.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @param period [Integer] The period for CCI (default: 20).
|
|
13
|
+
# @param constant [Float] The constant used for scaling (default: 0.015).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated CCI values.
|
|
15
|
+
def self.calculate(data, period: 20, constant: 0.015)
|
|
16
|
+
tp_values = data.map { |v| (v[:high] + v[:low] + v[:close]) / 3.0 }
|
|
17
|
+
output = []
|
|
18
|
+
|
|
19
|
+
tp_values.each_with_index do |tp, i|
|
|
20
|
+
if i < period - 1
|
|
21
|
+
output << nil
|
|
22
|
+
next
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
window = tp_values[(i - period + 1)..i]
|
|
26
|
+
sma = CalculationHelpers.average(window)
|
|
27
|
+
mad = CalculationHelpers.mean_absolute_deviation(window, sma)
|
|
28
|
+
|
|
29
|
+
if mad.zero?
|
|
30
|
+
output << 0.0
|
|
31
|
+
else
|
|
32
|
+
cci = (tp - sma) / (constant * mad)
|
|
33
|
+
output << cci
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
output
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Chaikin Money Flow (CMF).
|
|
8
|
+
# CMF measures the amount of Money Flow Volume over a specific period.
|
|
9
|
+
class CMF
|
|
10
|
+
# Calculates the Chaikin Money Flow.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @param period [Integer] The period for CMF (default: 20).
|
|
13
|
+
# @return [Array<Float, nil>] The calculated CMF values.
|
|
14
|
+
def self.calculate(data, period: 20)
|
|
15
|
+
period = period.to_i
|
|
16
|
+
return Array.new(data.size, nil) if data.size < period
|
|
17
|
+
|
|
18
|
+
output = []
|
|
19
|
+
period_volumes = []
|
|
20
|
+
period_mf_volumes = []
|
|
21
|
+
|
|
22
|
+
data.each do |v|
|
|
23
|
+
high = v[:high]
|
|
24
|
+
low = v[:low]
|
|
25
|
+
close = v[:close]
|
|
26
|
+
volume = v[:volume]
|
|
27
|
+
|
|
28
|
+
multiplier = if high == low
|
|
29
|
+
0.0
|
|
30
|
+
else
|
|
31
|
+
((close - low) - (high - close)) / (high - low).to_f
|
|
32
|
+
end
|
|
33
|
+
mf_volume = multiplier * volume
|
|
34
|
+
|
|
35
|
+
period_volumes << volume
|
|
36
|
+
period_mf_volumes << mf_volume
|
|
37
|
+
|
|
38
|
+
if period_volumes.size == period
|
|
39
|
+
volume_sum = CalculationHelpers.sum(period_volumes)
|
|
40
|
+
mf_volume_sum = CalculationHelpers.sum(period_mf_volumes)
|
|
41
|
+
|
|
42
|
+
output << (volume_sum.zero? ? 0.0 : mf_volume_sum / volume_sum.to_f)
|
|
43
|
+
|
|
44
|
+
period_volumes.shift
|
|
45
|
+
period_mf_volumes.shift
|
|
46
|
+
else
|
|
47
|
+
output << nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
output
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|