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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +551 -0
  6. data/Rakefile +12 -0
  7. data/exe/indicator_hub +4 -0
  8. data/lib/indicator_hub/calculation_helpers.rb +196 -0
  9. data/lib/indicator_hub/indicators/adi.rb +37 -0
  10. data/lib/indicator_hub/indicators/adtv.rb +27 -0
  11. data/lib/indicator_hub/indicators/adx.rb +102 -0
  12. data/lib/indicator_hub/indicators/ao.rb +39 -0
  13. data/lib/indicator_hub/indicators/atr.rb +45 -0
  14. data/lib/indicator_hub/indicators/base_indicator.rb +68 -0
  15. data/lib/indicator_hub/indicators/bb.rb +44 -0
  16. data/lib/indicator_hub/indicators/cci.rb +41 -0
  17. data/lib/indicator_hub/indicators/cmf.rb +54 -0
  18. data/lib/indicator_hub/indicators/cmo.rb +49 -0
  19. data/lib/indicator_hub/indicators/cr.rb +26 -0
  20. data/lib/indicator_hub/indicators/dc.rb +40 -0
  21. data/lib/indicator_hub/indicators/dlr.rb +27 -0
  22. data/lib/indicator_hub/indicators/dpo.rb +32 -0
  23. data/lib/indicator_hub/indicators/dr.rb +27 -0
  24. data/lib/indicator_hub/indicators/ema.rb +40 -0
  25. data/lib/indicator_hub/indicators/envelopes_ema.rb +36 -0
  26. data/lib/indicator_hub/indicators/eom.rb +48 -0
  27. data/lib/indicator_hub/indicators/fi.rb +45 -0
  28. data/lib/indicator_hub/indicators/ichimoku.rb +76 -0
  29. data/lib/indicator_hub/indicators/imi.rb +48 -0
  30. data/lib/indicator_hub/indicators/kc.rb +46 -0
  31. data/lib/indicator_hub/indicators/kst.rb +82 -0
  32. data/lib/indicator_hub/indicators/macd.rb +46 -0
  33. data/lib/indicator_hub/indicators/mfi.rb +62 -0
  34. data/lib/indicator_hub/indicators/mi.rb +81 -0
  35. data/lib/indicator_hub/indicators/nvi.rb +42 -0
  36. data/lib/indicator_hub/indicators/obv.rb +41 -0
  37. data/lib/indicator_hub/indicators/obv_mean.rb +42 -0
  38. data/lib/indicator_hub/indicators/pivot_points.rb +44 -0
  39. data/lib/indicator_hub/indicators/price_channel.rb +38 -0
  40. data/lib/indicator_hub/indicators/qstick.rb +36 -0
  41. data/lib/indicator_hub/indicators/rmi.rb +48 -0
  42. data/lib/indicator_hub/indicators/roc.rb +37 -0
  43. data/lib/indicator_hub/indicators/rsi.rb +67 -0
  44. data/lib/indicator_hub/indicators/sma.rb +32 -0
  45. data/lib/indicator_hub/indicators/so.rb +76 -0
  46. data/lib/indicator_hub/indicators/trix.rb +53 -0
  47. data/lib/indicator_hub/indicators/tsi.rb +67 -0
  48. data/lib/indicator_hub/indicators/uo.rb +67 -0
  49. data/lib/indicator_hub/indicators/vi.rb +54 -0
  50. data/lib/indicator_hub/indicators/volume_oscillator.rb +55 -0
  51. data/lib/indicator_hub/indicators/vpt.rb +35 -0
  52. data/lib/indicator_hub/indicators/vwap.rb +33 -0
  53. data/lib/indicator_hub/indicators/wilders_smoothing.rb +36 -0
  54. data/lib/indicator_hub/indicators/wma.rb +36 -0
  55. data/lib/indicator_hub/indicators/wr.rb +36 -0
  56. data/lib/indicator_hub/series.rb +79 -0
  57. data/lib/indicator_hub/talib_adapter.rb +23 -0
  58. data/lib/indicator_hub/validation.rb +49 -0
  59. data/lib/indicator_hub/version.rb +6 -0
  60. data/lib/indicator_hub.rb +485 -0
  61. data/sig/indicator_hub.rbs +4 -0
  62. 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