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,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+ require_relative "ema"
5
+
6
+ module IndicatorHub
7
+ module Indicators
8
+ # Triple Exponential Average (TRIX).
9
+ # TRIX is a momentum oscillator that shows the percent rate-of-change of a
10
+ # triple exponentially smoothed moving average.
11
+ class TRIX
12
+ # Calculates the Triple Exponential Average.
13
+ # @param data [Array<Numeric>] The input data points.
14
+ # @param period [Integer] The TRIX period (default: 15).
15
+ # @return [Array<Float, nil>] The calculated TRIX values.
16
+ def self.calculate(data, period: 15)
17
+ period = period.to_i
18
+
19
+ # EMA1 = EMA(price, n)
20
+ ema1 = EMA.calculate(data, period: period)
21
+
22
+ # Filter out nils for next EMA
23
+ ema1_filtered = ema1.compact
24
+ ema2 = EMA.calculate(ema1_filtered, period: period)
25
+
26
+ # EMA2 filtered
27
+ ema2_filtered = ema2.compact
28
+ ema3 = EMA.calculate(ema2_filtered, period: period)
29
+
30
+ # Now we need to align them back to the original data size
31
+ # ema1 has (period - 1) nils at the start
32
+ # ema2 has (period - 1) additional nils
33
+ # ema3 has (period - 1) additional nils
34
+ # Total nils at start of ema3 (aligned to data): 3 * (period - 1)
35
+
36
+ full_ema3 = Array.new(data.size, nil)
37
+ ema3_compact = ema3.compact
38
+
39
+ offset = 3 * (period - 1)
40
+ ema3_compact.each_with_index do |v, i|
41
+ full_ema3[offset + i] = v if offset + i < data.size
42
+ end
43
+
44
+ output = Array.new(data.size, nil)
45
+ (1...data.size).each do |i|
46
+ output[i] = (((full_ema3[i] - full_ema3[i - 1]) / full_ema3[i - 1].to_f) * 100.0) if full_ema3[i] && full_ema3[i - 1]
47
+ end
48
+
49
+ output
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+ require_relative "ema"
5
+
6
+ module IndicatorHub
7
+ module Indicators
8
+ # True Strength Index (TSI).
9
+ # TSI is a technical momentum oscillator used to identify trends and reversals.
10
+ class TSI
11
+ # Calculates the True Strength Index.
12
+ # @param data [Array<Numeric>] The input data points.
13
+ # @param fast_period [Integer] Fast EMA period (default: 13).
14
+ # @param slow_period [Integer] Slow EMA period (default: 25).
15
+ # @return [Array<Float, nil>] The calculated TSI values.
16
+ def self.calculate(data, fast_period: 13, slow_period: 25)
17
+ fast_period = fast_period.to_i
18
+ slow_period = slow_period.to_i
19
+
20
+ return [] if data.size < 2
21
+
22
+ # 1. Momentum and absolute momentum
23
+ momentum = []
24
+ abs_momentum = []
25
+
26
+ (1...data.size).each do |i|
27
+ m = data[i] - data[i - 1]
28
+ momentum << m
29
+ abs_momentum << m.abs
30
+ end
31
+
32
+ # 2. First EMA (slow_period)
33
+ ema1_m = EMA.calculate(momentum, period: slow_period)
34
+ ema1_abs_m = EMA.calculate(abs_momentum, period: slow_period)
35
+
36
+ # 3. Second EMA (fast_period)
37
+ ema1_m_filtered = ema1_m.compact
38
+ ema1_abs_m_filtered = ema1_abs_m.compact
39
+
40
+ ema2_m = EMA.calculate(ema1_m_filtered, period: fast_period)
41
+ ema2_abs_m = EMA.calculate(ema1_abs_m_filtered, period: fast_period)
42
+
43
+ # Align results back to original data size
44
+ # momentum size is (data.size - 1)
45
+ # ema1 has (slow_period - 1) nils
46
+ # ema2 has (fast_period - 1) additional nils
47
+ # total nils in ema2 relative to momentum: (slow_period - 1) + (fast_period - 1)
48
+ # relative to original data: 1 + (slow_period - 1) + (fast_period - 1) = slow_period + fast_period - 1
49
+
50
+ offset = slow_period + fast_period - 1
51
+ output = Array.new(data.size, nil)
52
+
53
+ ema2_m_compact = ema2_m.compact
54
+ ema2_abs_m_compact = ema2_abs_m.compact
55
+
56
+ ema2_m_compact.each_with_index do |v, i|
57
+ if offset + i < data.size
58
+ div = ema2_abs_m_compact[i]
59
+ output[offset + i] = div.zero? ? 0.0 : 100.0 * (v / div.to_f)
60
+ end
61
+ end
62
+
63
+ output
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Ultimate Oscillator (UO).
8
+ # UO is a technical indicator that combines price action over three
9
+ # different timeframes into a single momentum oscillator.
10
+ class UO
11
+ # Calculates the Ultimate Oscillator.
12
+ # @param data [Array<Hash>] Array of OHLCV hashes.
13
+ # @param short_period [Integer] Short period (default: 7).
14
+ # @param medium_period [Integer] Medium period (default: 14).
15
+ # @param long_period [Integer] Long period (default: 28).
16
+ # @param short_weight [Float] Short period weight (default: 4.0).
17
+ # @param medium_weight [Float] Medium period weight (default: 2.0).
18
+ # @param long_weight [Float] Long period weight (default: 1.0).
19
+ # @return [Array<Float, nil>] The calculated UO values.
20
+ def self.calculate(data, short_period: 7, medium_period: 14, long_period: 28,
21
+ short_weight: 4.0, medium_weight: 2.0, long_weight: 1.0)
22
+ short_period = short_period.to_i
23
+ medium_period = medium_period.to_i
24
+ long_period = long_period.to_i
25
+
26
+ return [] if data.size < 2
27
+
28
+ bp = []
29
+ tr = []
30
+
31
+ (1...data.size).each do |i|
32
+ v = data[i]
33
+ prev_close = data[i - 1][:close]
34
+
35
+ min_low_p_close = [v[:low], prev_close].min
36
+ max_high_p_close = [v[:high], prev_close].max
37
+
38
+ bp << (v[:close] - min_low_p_close)
39
+ tr << (max_high_p_close - min_low_p_close)
40
+ end
41
+
42
+ sum_weights = short_weight + medium_weight + long_weight
43
+ output = Array.new(data.size, nil)
44
+
45
+ # We need long_period elements in bp/tr to start
46
+ # bp/tr have size data.size - 1
47
+ ((long_period - 1)...bp.size).each do |i|
48
+ avg7 = sum_last(bp, i, short_period) / sum_last(tr, i, short_period).to_f
49
+ avg14 = sum_last(bp, i, medium_period) / sum_last(tr, i, medium_period).to_f
50
+ avg28 = sum_last(bp, i, long_period) / sum_last(tr, i, long_period).to_f
51
+
52
+ uo = 100.0 * ((short_weight * avg7) + (medium_weight * avg14) + (long_weight * avg28)) / sum_weights
53
+ output[i + 1] = uo
54
+ end
55
+
56
+ output
57
+ end
58
+
59
+ def self.sum_last(array, index, period)
60
+ start = index - period + 1
61
+ sum = 0.0
62
+ (start..index).each { |j| sum += array[j] }
63
+ sum
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Vortex Indicator (VI).
8
+ # VI is a technical indicator consisting of two lines that identify
9
+ # positive and negative trend movement.
10
+ class VI
11
+ # Calculates the Vortex Indicator.
12
+ # @param data [Array<Hash>] Array of OHLCV hashes.
13
+ # @param period [Integer] The VI period (default: 14).
14
+ # @return [Array<Hash>] The calculated VI values { plus_vi: Float, minus_vi: Float }.
15
+ def self.calculate(data, period: 14)
16
+ output = []
17
+ pos_vms = []
18
+ neg_vms = []
19
+ trs = []
20
+
21
+ data.each_with_index do |val, i|
22
+ if i.zero?
23
+ output << { plus_vi: nil, minus_vi: nil }
24
+ next
25
+ end
26
+
27
+ prev = data[i - 1]
28
+ pos_vm = (val[:high] - prev[:low]).abs
29
+ neg_vm = (val[:low] - prev[:high]).abs
30
+ tr = CalculationHelpers.true_range(val[:high], val[:low], prev[:close])
31
+
32
+ pos_vms << pos_vm
33
+ neg_vms << neg_vm
34
+ trs << tr
35
+
36
+ if pos_vms.size >= period
37
+ sum_pos = CalculationHelpers.sum(pos_vms.last(period))
38
+ sum_neg = CalculationHelpers.sum(neg_vms.last(period))
39
+ sum_tr = CalculationHelpers.sum(trs.last(period))
40
+
41
+ output << {
42
+ plus_vi: (sum_pos / sum_tr.to_f),
43
+ minus_vi: (sum_neg / sum_tr.to_f)
44
+ }
45
+ else
46
+ output << { plus_vi: nil, minus_vi: nil }
47
+ end
48
+ end
49
+
50
+ output
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Volume Oscillator.
8
+ # Volume Oscillator measures the difference between a fast and slow volume
9
+ # moving average.
10
+ class VolumeOscillator
11
+ # Calculates the Volume Oscillator.
12
+ # @param data [Array<Numeric, Hash>] Array of volumes or OHLCV hashes.
13
+ # @param short_period [Integer] Short SMA period (default: 20).
14
+ # @param long_period [Integer] Long SMA period (default: 60).
15
+ # @return [Array<Float, nil>] The calculated Volume Oscillator values.
16
+ def self.calculate(data, short_period: 20, long_period: 60)
17
+ # Handle OHLC data by extracting volume
18
+ numeric_data = if data.is_a?(Array) && data.first.is_a?(Hash)
19
+ data.map { |d| d[:volume] }
20
+ else
21
+ data
22
+ end
23
+
24
+ Validation.validate_numeric_data(numeric_data)
25
+
26
+ output = []
27
+ short_period_values = []
28
+ long_period_values = []
29
+
30
+ numeric_data.each do |v|
31
+ short_period_values << v
32
+ long_period_values << v
33
+
34
+ short_period_values.shift if short_period_values.size > short_period
35
+ long_period_values.shift if long_period_values.size > long_period
36
+
37
+ if long_period_values.size == long_period
38
+ short_sma = CalculationHelpers.average(short_period_values)
39
+ long_sma = CalculationHelpers.average(long_period_values)
40
+
41
+ vo = if long_sma.zero?
42
+ 0.0
43
+ else
44
+ ((short_sma - long_sma) / long_sma.to_f) * 100.0
45
+ end
46
+ output << vo.round(2)
47
+ else
48
+ output << nil
49
+ end
50
+ end
51
+ output
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Volume-Price Trend (VPT).
8
+ # VPT is a technical indicator that combines price and volume to confirm
9
+ # the strength of a price trend or signal its reversal.
10
+ class VPT
11
+ # Calculates the Volume-Price Trend.
12
+ # @param data [Array<Hash>] Array of OHLCV hashes.
13
+ # @return [Array<Float, nil>] The calculated VPT values.
14
+ def self.calculate(data)
15
+ output = []
16
+ prev_vpt = 0.0
17
+
18
+ data.each_with_index do |val, i|
19
+ if i.zero?
20
+ output << nil
21
+ next
22
+ end
23
+
24
+ prev = data[i - 1]
25
+ # VPT = prev_vpt + (volume * (close - prev_close) / prev_close)
26
+ vpt = prev_vpt + (val[:volume] * (val[:close] - prev[:close]) / prev[:close].to_f)
27
+ output << vpt
28
+ prev_vpt = vpt
29
+ end
30
+
31
+ output
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Volume Weighted Average Price (VWAP).
8
+ # VWAP is a technical analysis indicator used on intraday charts that resets
9
+ # at the start of every new trading session.
10
+ class VWAP
11
+ # Calculates the Volume Weighted Average Price.
12
+ # @param data [Array<Hash>] Array of OHLCV hashes.
13
+ # @return [Array<Float, nil>] The calculated VWAP values.
14
+ def self.calculate(data)
15
+ # Expects array of hashes with :high, :low, :close, :volume
16
+ output = []
17
+ cumm_volume = 0.0
18
+ cumm_volume_x_typical_price = 0.0
19
+
20
+ data.each do |v|
21
+ tp = CalculationHelpers.typical_price(v[:high], v[:low], v[:close])
22
+ vol = v[:volume].to_f
23
+ cumm_volume_x_typical_price += vol * tp
24
+ cumm_volume += vol
25
+
26
+ vwap = cumm_volume.positive? ? (cumm_volume_x_typical_price / cumm_volume) : 0.0
27
+ output << vwap
28
+ end
29
+ output
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IndicatorHub
4
+ module Indicators
5
+ # Wilder's Smoothing.
6
+ # Wilder's Smoothing is a type of exponential moving average used in
7
+ # technical indicators like RSI and ATR.
8
+ class WildersSmoothing
9
+ include CalculationHelpers
10
+
11
+ # Calculates Wilder's Smoothing.
12
+ # @param prices [Array<Numeric>] Array of prices.
13
+ # @param period [Integer] The smoothing period (default: 14).
14
+ # @return [Array<Float, nil>] The calculated Wilder's Smoothing values.
15
+ def self.calculate(prices, period: 14)
16
+ return Array.new(prices.length, nil) if prices.length < period
17
+
18
+ results = Array.new(period - 1, nil)
19
+
20
+ # First Wilder's is a simple average of the first 'period' values
21
+ initial_sma = prices.first(period).sum.to_f / period
22
+ results << initial_sma.round(4)
23
+
24
+ wilders = initial_sma
25
+ alpha = 1.0 / period
26
+
27
+ prices.drop(period).each do |price|
28
+ wilders = ((price - wilders) * alpha) + wilders
29
+ results << wilders.round(4)
30
+ end
31
+
32
+ results
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Weighted Moving Average (WMA).
8
+ # WMA is a moving average that assigns more weight to recent data points
9
+ # and less weight to past data points.
10
+ class WMA
11
+ # Calculates the Weighted Moving Average.
12
+ # @param data [Array<Numeric>] The input data points.
13
+ # @param period [Integer] The WMA period (default: 20).
14
+ # @return [Array<Float, nil>] The calculated WMA values.
15
+ def self.calculate(data, period: 20)
16
+ period = period.to_i
17
+ Validation.validate_numeric_data(data)
18
+ return Array.new(data.size, nil) if data.size < period
19
+
20
+ output = []
21
+ period_values = []
22
+
23
+ data.each do |v|
24
+ period_values << v
25
+ if period_values.size == period
26
+ output << CalculationHelpers.wma(period_values)
27
+ period_values.shift
28
+ else
29
+ output << nil
30
+ end
31
+ end
32
+ output
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../calculation_helpers"
4
+
5
+ module IndicatorHub
6
+ module Indicators
7
+ # Williams %R (WR).
8
+ # WR is a momentum indicator that measures overbought and oversold levels.
9
+ class WR
10
+ # Calculates the Williams %R.
11
+ # @param data [Array<Hash>] Array of OHLCV hashes.
12
+ # @param period [Integer] The WR period (default: 14).
13
+ # @return [Array<Float, nil>] The calculated Williams %R values.
14
+ def self.calculate(data, period: 14)
15
+ output = []
16
+
17
+ data.each_with_index do |val, i|
18
+ if i < period - 1
19
+ output << nil
20
+ next
21
+ end
22
+
23
+ slice = data[(i - period + 1)..i]
24
+ highest_high = slice.map { |d| d[:high] }.max
25
+ lowest_low = slice.map { |d| d[:low] }.min
26
+
27
+ # WR = (highest_high - close) / (highest_high - lowest_low) * -100
28
+ wr = (highest_high - val[:close]) / (highest_high - lowest_low).to_f * -100
29
+ output << wr
30
+ end
31
+
32
+ output
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module IndicatorHub
7
+ # Data structure for handling OHLCV and series data.
8
+ # Provides normalization and sorting capabilities for financial time series data.
9
+ class Series
10
+ extend T::Sig
11
+
12
+ # @return [Array<T.any(Hash, Numeric)>] The raw input data
13
+ sig { returns(T::Array[T.any(T::Hash[T.untyped, T.untyped], Numeric)]) }
14
+ attr_reader :data
15
+
16
+ # Initializes a new Series object.
17
+ #
18
+ # @param data [Array<T.any(Hash, Numeric)>] The input data, either an array of numbers or hashes
19
+ sig { params(data: T.any(T::Array[T.any(T::Hash[T.untyped, T.untyped], Numeric)], T::Hash[T.untyped, T.untyped], Numeric)).void }
20
+ def initialize(data)
21
+ @data = Array(data)
22
+ end
23
+
24
+ # Normalizes input data into an array of floats.
25
+ #
26
+ # @param field [Symbol, String] The field to extract from the hash data (default: :close)
27
+ # @return [Array<Float>] An array of floating point numbers extracted from the data
28
+ sig { params(field: T.any(Symbol, String)).returns(T::Array[Float]) }
29
+ def to_a(field: :close)
30
+ data.map do |v|
31
+ if v.is_a?(Numeric)
32
+ v.to_f
33
+ elsif v.is_a?(Hash)
34
+ (v[field] || v[field.to_sym] || v[field.to_s] || 0.0).to_f
35
+ else
36
+ 0.0
37
+ end
38
+ end
39
+ end
40
+
41
+ # Normalizes input data into an array of OHLCV hashes.
42
+ #
43
+ # @return [Array<Hash{Symbol => Float}>] An array of hashes containing open, high, low, close, and volume
44
+ sig { returns(T::Array[T::Hash[Symbol, Float]]) }
45
+ def to_ohlc
46
+ data.map do |v|
47
+ if v.is_a?(Hash)
48
+ {
49
+ open: (v[:open] || v["open"] || 0.0).to_f,
50
+ high: (v[:high] || v["high"] || 0.0).to_f,
51
+ low: (v[:low] || v["low"] || 0.0).to_f,
52
+ close: (v[:close] || v["close"] || 0.0).to_f,
53
+ volume: (v[:volume] || v["volume"] || 0.0).to_f
54
+ }
55
+ elsif v.is_a?(Numeric)
56
+ {
57
+ open: v.to_f,
58
+ high: v.to_f,
59
+ low: v.to_f,
60
+ close: v.to_f,
61
+ volume: 0.0
62
+ }
63
+ else
64
+ { open: 0.0, high: 0.0, low: 0.0, close: 0.0, volume: 0.0 }
65
+ end
66
+ end
67
+ end
68
+
69
+ # Returns chronologically sorted data if it has date or time fields.
70
+ #
71
+ # @return [Array<T.any(Hash, Numeric)>] The sorted data
72
+ sig { returns(T::Array[T.any(T::Hash[T.untyped, T.untyped], Numeric)]) }
73
+ def sorted_data
74
+ return data unless data.first.is_a?(Hash) && (data.first[:date] || data.first["date"] || data.first[:date_time] || data.first["date_time"])
75
+
76
+ data.sort_by { |v| v[:date] || v["date"] || v[:date_time] || v["date_time"] }
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IndicatorHub
4
+ # Optional adapter for high-performance TA-Lib calculations
5
+ class TALibAdapter
6
+ def self.available?
7
+ @available ||= begin
8
+ require "talib_ffi"
9
+ true
10
+ rescue LoadError
11
+ false
12
+ end
13
+ end
14
+
15
+ def self.sma(_data, _period)
16
+ return nil unless available?
17
+
18
+ # Example: TALib.sma(data, period) - exact method depends on talib_ffi API
19
+ # For now, this is a placeholder to show where TA-Lib logic would go
20
+ nil
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module IndicatorHub
7
+ # Robust validation for technical analysis data and options.
8
+ module Validation
9
+ extend T::Sig
10
+
11
+ # Error class for validation failures.
12
+ class Error < StandardError; end
13
+
14
+ # Validates that the provided options are within the allowed set.
15
+ # @param options [Hash] The options to validate.
16
+ # @param valid_options [Array<Symbol>] The list of allowed option keys.
17
+ # @raise [Validation::Error] if an invalid option is found.
18
+ sig { params(options: T::Hash[Symbol, T.untyped], valid_options: T::Array[Symbol]).void }
19
+ def self.validate_options(options, valid_options)
20
+ raise Error, "Options must be a hash." unless options.is_a?(Hash)
21
+
22
+ invalid_keys = options.keys - valid_options
23
+ return if invalid_keys.empty?
24
+
25
+ raise Error, "Invalid options: #{invalid_keys.join(", ")}. Valid options are: #{valid_options.join(", ")}"
26
+ end
27
+
28
+ # Validates that all data points in the numeric array are numbers.
29
+ # @param data [Array<Numeric>] The data to validate.
30
+ # @raise [Validation::Error] if non-numeric data is found.
31
+ sig { params(data: T::Array[T.untyped]).void }
32
+ def self.validate_numeric_data(data)
33
+ return if data.all?(Numeric)
34
+
35
+ raise Error, "Invalid Data. Input must be numeric."
36
+ end
37
+
38
+ # Validates that the data set meets the minimum required length.
39
+ # @param data [Array] The data to validate.
40
+ # @param required_size [Integer] The minimum length required.
41
+ # @raise [Validation::Error] if the data is too short.
42
+ sig { params(data: T::Array[T.untyped], required_size: Integer).void }
43
+ def self.validate_length(data, required_size)
44
+ return unless data.size < required_size
45
+
46
+ raise Error, "Not enough data. Expected at least #{required_size}, got #{data.size}."
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IndicatorHub
4
+ # The current version of the IndicatorHub gem.
5
+ VERSION = "0.1.0"
6
+ end