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,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Money Flow Index (MFI).
|
|
8
|
+
# MFI is a technical oscillator that uses price and volume for identifying
|
|
9
|
+
# overbought or oversold signals in an asset.
|
|
10
|
+
class MFI
|
|
11
|
+
# Calculates the Money Flow Index.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The MFI period (default: 14).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated MFI values.
|
|
15
|
+
def self.calculate(data, period: 14)
|
|
16
|
+
return Array.new(data.size, nil) if data.size <= period
|
|
17
|
+
|
|
18
|
+
output = []
|
|
19
|
+
typical_prices = []
|
|
20
|
+
raw_money_flows = []
|
|
21
|
+
|
|
22
|
+
data.each_with_index do |v, i|
|
|
23
|
+
typical_price = (v[:high] + v[:low] + v[:close]) / 3.0
|
|
24
|
+
typical_prices << typical_price
|
|
25
|
+
|
|
26
|
+
if i.zero?
|
|
27
|
+
raw_money_flows << 0.0
|
|
28
|
+
output << nil
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
money_flow = typical_price * v[:volume]
|
|
33
|
+
raw_money_flows << if typical_price > typical_prices[i - 1]
|
|
34
|
+
money_flow
|
|
35
|
+
elsif typical_price < typical_prices[i - 1]
|
|
36
|
+
-money_flow
|
|
37
|
+
else
|
|
38
|
+
0.0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if raw_money_flows.size > period
|
|
42
|
+
current_flows = raw_money_flows.last(period)
|
|
43
|
+
pos_flow = current_flows.select(&:positive?).sum
|
|
44
|
+
neg_flow = current_flows.select(&:negative?).sum.abs
|
|
45
|
+
|
|
46
|
+
if neg_flow.zero?
|
|
47
|
+
mfi = 100.0
|
|
48
|
+
else
|
|
49
|
+
mfr = pos_flow / neg_flow
|
|
50
|
+
mfi = 100.0 - (100.0 / (1.0 + mfr))
|
|
51
|
+
end
|
|
52
|
+
output << mfi
|
|
53
|
+
else
|
|
54
|
+
output << nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
output
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Mass Index (MI).
|
|
8
|
+
# MI is a technical indicator used to predict trend reversals by analyzing
|
|
9
|
+
# the narrowing and widening of the trading range.
|
|
10
|
+
class MI
|
|
11
|
+
# Calculates the Mass Index.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param ema_period [Integer] The EMA period (default: 9).
|
|
14
|
+
# @param period [Integer] The summation period (default: 25).
|
|
15
|
+
# @return [Array<Float, nil>] The calculated MI values.
|
|
16
|
+
def self.calculate(data, ema_period: 9, period: 25)
|
|
17
|
+
high_low_diffs = data.map do |v|
|
|
18
|
+
if v.is_a?(Hash)
|
|
19
|
+
((v[:high] || v["high"]).to_f - (v[:low] || v["low"]).to_f)
|
|
20
|
+
else
|
|
21
|
+
0.0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# First EMA
|
|
26
|
+
single_ema_values = calculate_ema(high_low_diffs, ema_period)
|
|
27
|
+
|
|
28
|
+
# Second EMA (EMA of the first EMA)
|
|
29
|
+
valid_single_emas = single_ema_values.compact
|
|
30
|
+
if valid_single_emas.size >= ema_period
|
|
31
|
+
double_ema_valid_values = calculate_ema(valid_single_emas, ema_period)
|
|
32
|
+
# Re-align double_ema_values with single_ema_values
|
|
33
|
+
lead_nils = single_ema_values.size - valid_single_emas.size
|
|
34
|
+
double_ema_values = Array.new(lead_nils, nil) + double_ema_valid_values
|
|
35
|
+
else
|
|
36
|
+
double_ema_values = Array.new(single_ema_values.size, nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
ratios = []
|
|
40
|
+
single_ema_values.each_with_index do |s_ema, i|
|
|
41
|
+
d_ema = double_ema_values[i]
|
|
42
|
+
ratios << (s_ema / d_ema if s_ema && d_ema && d_ema != 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
output = []
|
|
46
|
+
current_ratios = []
|
|
47
|
+
ratios.each do |ratio|
|
|
48
|
+
if ratio.nil?
|
|
49
|
+
output << nil
|
|
50
|
+
else
|
|
51
|
+
current_ratios << ratio
|
|
52
|
+
current_ratios.shift if current_ratios.size > period
|
|
53
|
+
|
|
54
|
+
output << (current_ratios.sum if current_ratios.size == period)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
output
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.calculate_ema(data, period)
|
|
62
|
+
output = []
|
|
63
|
+
period_values = []
|
|
64
|
+
previous_ema = nil
|
|
65
|
+
|
|
66
|
+
data.each do |v|
|
|
67
|
+
period_values << v
|
|
68
|
+
if period_values.size == period
|
|
69
|
+
ema = CalculationHelpers.ema(v, period_values, period, previous_ema)
|
|
70
|
+
previous_ema = ema
|
|
71
|
+
output << ema
|
|
72
|
+
period_values.shift
|
|
73
|
+
else
|
|
74
|
+
output << nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
output
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Negative Volume Index (NVI).
|
|
8
|
+
# NVI is a technical indicator used to identify market trends based on
|
|
9
|
+
# days when volume decreases.
|
|
10
|
+
class NVI
|
|
11
|
+
# Calculates the Negative Volume Index.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @return [Array<Float>] The calculated NVI values.
|
|
14
|
+
def self.calculate(data)
|
|
15
|
+
nvi_cumulative = 1_000.00
|
|
16
|
+
output = []
|
|
17
|
+
|
|
18
|
+
# We need at least two points to calculate change
|
|
19
|
+
return [] if data.empty?
|
|
20
|
+
|
|
21
|
+
data[0]
|
|
22
|
+
output << nvi_cumulative # Start with default of 1_000 for the first point
|
|
23
|
+
|
|
24
|
+
(1...data.length).each do |i|
|
|
25
|
+
v = data[i]
|
|
26
|
+
prev_v = data[i - 1]
|
|
27
|
+
|
|
28
|
+
volume_change = ((v[:volume] - prev_v[:volume]) / prev_v[:volume].to_f)
|
|
29
|
+
|
|
30
|
+
if volume_change.negative?
|
|
31
|
+
price_change = (v[:close] - prev_v[:close]) / prev_v[:close].to_f
|
|
32
|
+
nvi_cumulative *= (1.0 + price_change)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
output << nvi_cumulative
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
output
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# On-Balance Volume (OBV).
|
|
8
|
+
# OBV is a technical momentum indicator that uses volume flow to predict
|
|
9
|
+
# changes in stock price.
|
|
10
|
+
class OBV
|
|
11
|
+
# Calculates the On-Balance Volume.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @return [Array<Float>] The calculated OBV values.
|
|
14
|
+
def self.calculate(data)
|
|
15
|
+
current_obv = 0.0
|
|
16
|
+
output = []
|
|
17
|
+
return [] if data.empty?
|
|
18
|
+
|
|
19
|
+
prior_close = nil
|
|
20
|
+
|
|
21
|
+
data.each do |v|
|
|
22
|
+
volume = v[:volume]
|
|
23
|
+
close = v[:close]
|
|
24
|
+
|
|
25
|
+
unless prior_close.nil?
|
|
26
|
+
if close > prior_close
|
|
27
|
+
current_obv += volume
|
|
28
|
+
elsif close < prior_close
|
|
29
|
+
current_obv -= volume
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
output << current_obv
|
|
34
|
+
prior_close = close
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
output
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# On-Balance Volume Mean (OBV Mean).
|
|
8
|
+
# OBV Mean is the average of the On-Balance Volume over a specified period.
|
|
9
|
+
class OBVMean
|
|
10
|
+
# Calculates the On-Balance Volume Mean.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @param period [Integer] The OBV Mean period (default: 10).
|
|
13
|
+
# @return [Array<Float, nil>] The calculated OBV Mean values.
|
|
14
|
+
def self.calculate(data, period: 10)
|
|
15
|
+
current_obv = 0.0
|
|
16
|
+
obvs = []
|
|
17
|
+
output = []
|
|
18
|
+
prior_close = nil
|
|
19
|
+
|
|
20
|
+
data.each do |v|
|
|
21
|
+
volume = v[:volume]
|
|
22
|
+
close = v[:close]
|
|
23
|
+
|
|
24
|
+
unless prior_close.nil?
|
|
25
|
+
if close > prior_close
|
|
26
|
+
current_obv += volume
|
|
27
|
+
elsif close < prior_close
|
|
28
|
+
current_obv -= volume
|
|
29
|
+
end
|
|
30
|
+
obvs << current_obv
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
prior_close = close
|
|
34
|
+
|
|
35
|
+
output << (IndicatorHub::CalculationHelpers.average(obvs.last(period)) if obvs.size >= period)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
output
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Pivot Points.
|
|
6
|
+
# Pivot Points are used to identify potential support and resistance levels.
|
|
7
|
+
class PivotPoints
|
|
8
|
+
include CalculationHelpers
|
|
9
|
+
|
|
10
|
+
# Calculates the Pivot Points.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @return [Array<Hash>] The calculated Pivot Points values { p: Float, s1: Float, s2: Float, s3: Float, r1: Float, r2: Float, r3: Float }.
|
|
13
|
+
def self.calculate(data)
|
|
14
|
+
return [] unless data.is_a?(Array) && !data.empty?
|
|
15
|
+
|
|
16
|
+
data.map do |bar|
|
|
17
|
+
high = bar[:high]
|
|
18
|
+
low = bar[:low]
|
|
19
|
+
close = bar[:close]
|
|
20
|
+
|
|
21
|
+
p = ((high + low + close) / 3.0).round(4)
|
|
22
|
+
|
|
23
|
+
s1 = ((2 * p) - high).round(4)
|
|
24
|
+
s2 = (p - (high - low)).round(4)
|
|
25
|
+
s3 = (low - (2 * (high - p))).round(4)
|
|
26
|
+
|
|
27
|
+
r1 = ((2 * p) - low).round(4)
|
|
28
|
+
r2 = (p + (high - low)).round(4)
|
|
29
|
+
r3 = (high + (2 * (p - low))).round(4)
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
p: p,
|
|
33
|
+
s1: s1,
|
|
34
|
+
s2: s2,
|
|
35
|
+
s3: s3,
|
|
36
|
+
r1: r1,
|
|
37
|
+
r2: r2,
|
|
38
|
+
r3: r3
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Price Channel.
|
|
6
|
+
# Price Channel is a technical indicator that identifies the high and low
|
|
7
|
+
# prices over a specified period.
|
|
8
|
+
class PriceChannel
|
|
9
|
+
include CalculationHelpers
|
|
10
|
+
|
|
11
|
+
# Calculates the Price Channel.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The Price Channel period (default: 20).
|
|
14
|
+
# @return [Array<Hash>] The calculated Price Channel values { upper: Float, lower: Float }.
|
|
15
|
+
def self.calculate(data, period: 20)
|
|
16
|
+
return [] if data.length < period
|
|
17
|
+
|
|
18
|
+
highs = data.map { |d| d[:high] }
|
|
19
|
+
lows = data.map { |d| d[:low] }
|
|
20
|
+
|
|
21
|
+
results = []
|
|
22
|
+
(0...data.length).each do |i|
|
|
23
|
+
if i < period - 1
|
|
24
|
+
results << { upper: nil, lower: nil }
|
|
25
|
+
else
|
|
26
|
+
current_highs = highs[(i - period + 1)..i]
|
|
27
|
+
current_lows = lows[(i - period + 1)..i]
|
|
28
|
+
results << {
|
|
29
|
+
upper: current_highs.max,
|
|
30
|
+
lower: current_lows.min
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
results
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# QStick.
|
|
6
|
+
# QStick is a technical indicator that identifies the trend of a security's
|
|
7
|
+
# price by calculating the moving average of the difference between open and close.
|
|
8
|
+
class QStick
|
|
9
|
+
include CalculationHelpers
|
|
10
|
+
|
|
11
|
+
# Calculates the QStick.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The QStick period (default: 10).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated QStick values.
|
|
15
|
+
def self.calculate(data, period: 10)
|
|
16
|
+
return [] if data.length < period
|
|
17
|
+
|
|
18
|
+
opens = data.map { |d| d[:open] }
|
|
19
|
+
closes = data.map { |d| d[:close] }
|
|
20
|
+
|
|
21
|
+
diffs = closes.zip(opens).map { |c, o| c - o }
|
|
22
|
+
|
|
23
|
+
results = []
|
|
24
|
+
(0...data.length).each do |i|
|
|
25
|
+
if i < period - 1
|
|
26
|
+
results << nil
|
|
27
|
+
else
|
|
28
|
+
current_diffs = diffs[(i - period + 1)..i]
|
|
29
|
+
results << (current_diffs.sum.to_f / period).round(4)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
results
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Relative Momentum Index (RMI).
|
|
6
|
+
# RMI is a variation of the RSI that uses momentum instead of price change.
|
|
7
|
+
class RMI
|
|
8
|
+
include CalculationHelpers
|
|
9
|
+
|
|
10
|
+
# Calculates the Relative Momentum Index.
|
|
11
|
+
# @param data [Array<Numeric, Hash>] Array of prices or OHLCV hashes.
|
|
12
|
+
# @param period [Integer] RMI smoothing period (default: 14).
|
|
13
|
+
# @param momentum_period [Integer] Momentum period (default: 5).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated RMI values.
|
|
15
|
+
def self.calculate(data, period: 14, momentum_period: 5)
|
|
16
|
+
prices = data.is_a?(Array) && data.first.is_a?(Hash) ? data.map { |d| d[:close] } : data
|
|
17
|
+
return Array.new(prices.length, nil) if prices.length < momentum_period + period
|
|
18
|
+
|
|
19
|
+
ups = []
|
|
20
|
+
downs = []
|
|
21
|
+
|
|
22
|
+
# Calculate momentum changes
|
|
23
|
+
(momentum_period...prices.length).each do |i|
|
|
24
|
+
change = prices[i] - prices[i - momentum_period]
|
|
25
|
+
ups << (change.positive? ? change : 0)
|
|
26
|
+
downs << (change.negative? ? change.abs : 0)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Wilder's smoothing on ups and downs
|
|
30
|
+
avg_ups = WildersSmoothing.calculate(ups, period: period)
|
|
31
|
+
avg_downs = WildersSmoothing.calculate(downs, period: period)
|
|
32
|
+
|
|
33
|
+
results = Array.new(momentum_period, nil)
|
|
34
|
+
(0...avg_ups.length).each do |i|
|
|
35
|
+
if avg_ups[i].nil? || avg_downs[i].nil?
|
|
36
|
+
results << nil
|
|
37
|
+
elsif avg_downs[i].zero?
|
|
38
|
+
results << 100.0
|
|
39
|
+
else
|
|
40
|
+
rs = avg_ups[i].to_f / avg_downs[i]
|
|
41
|
+
results << (100.0 - (100.0 / (1.0 + rs))).round(4)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
results
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Rate of Change (ROC).
|
|
6
|
+
# ROC is a momentum oscillator that measures the percentage change in price
|
|
7
|
+
# between the current price and the price a certain number of periods ago.
|
|
8
|
+
class ROC
|
|
9
|
+
include CalculationHelpers
|
|
10
|
+
|
|
11
|
+
# Calculates the Rate of Change.
|
|
12
|
+
# @param prices [Array<Numeric>] Array of prices.
|
|
13
|
+
# @param period [Integer] The ROC period (default: 12).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated ROC values.
|
|
15
|
+
def self.calculate(prices, period: 12)
|
|
16
|
+
return Array.new(prices.length, nil) if prices.length < period
|
|
17
|
+
|
|
18
|
+
results = []
|
|
19
|
+
(0...prices.length).each do |i|
|
|
20
|
+
if i < period
|
|
21
|
+
results << nil
|
|
22
|
+
else
|
|
23
|
+
current_price = prices[i]
|
|
24
|
+
lookback_price = prices[i - period]
|
|
25
|
+
|
|
26
|
+
results << if lookback_price.zero?
|
|
27
|
+
0.0
|
|
28
|
+
else
|
|
29
|
+
(((current_price - lookback_price).to_f / lookback_price) * 100).round(4)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
results
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Relative Strength Index (RSI).
|
|
8
|
+
# RSI is a momentum oscillator that measures the speed and change of price movements.
|
|
9
|
+
# It oscillates between 0 and 100. Traditionally, RSI is considered overbought when
|
|
10
|
+
# above 70 and oversold when below 30.
|
|
11
|
+
class RSI
|
|
12
|
+
# Calculates the Relative Strength Index.
|
|
13
|
+
# @param data [Array<Numeric>] The input data points.
|
|
14
|
+
# @param period [Integer] The RSI period (default: 14).
|
|
15
|
+
# @return [Array<Float, nil>] The calculated RSI values.
|
|
16
|
+
def self.calculate(data, period: 14)
|
|
17
|
+
return [] if data.size < period
|
|
18
|
+
|
|
19
|
+
output = []
|
|
20
|
+
gains = []
|
|
21
|
+
losses = []
|
|
22
|
+
prev_price = data.first
|
|
23
|
+
|
|
24
|
+
avg_gain = nil
|
|
25
|
+
avg_loss = nil
|
|
26
|
+
|
|
27
|
+
data.each_with_index do |price, index|
|
|
28
|
+
if index.zero?
|
|
29
|
+
output << nil
|
|
30
|
+
next
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
change = price - prev_price
|
|
34
|
+
gains << (change.positive? ? change : 0.0)
|
|
35
|
+
losses << (change.negative? ? change.abs : 0.0)
|
|
36
|
+
|
|
37
|
+
if gains.size == period
|
|
38
|
+
if avg_gain.nil?
|
|
39
|
+
# Initial average gain/loss is SMA
|
|
40
|
+
avg_gain = CalculationHelpers.average(gains)
|
|
41
|
+
avg_loss = CalculationHelpers.average(losses)
|
|
42
|
+
else
|
|
43
|
+
# Subsequent use Wilder's Smoothing
|
|
44
|
+
avg_gain = CalculationHelpers.wilder_smoothing(avg_gain, gains.last, period)
|
|
45
|
+
avg_loss = CalculationHelpers.wilder_smoothing(avg_loss, losses.last, period)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if avg_loss.zero?
|
|
49
|
+
rsi = avg_gain.zero? ? 0.0 : 100.0
|
|
50
|
+
else
|
|
51
|
+
rs = avg_gain / avg_loss
|
|
52
|
+
rsi = 100.0 - (100.0 / (1.0 + rs))
|
|
53
|
+
end
|
|
54
|
+
output << rsi
|
|
55
|
+
gains.shift
|
|
56
|
+
losses.shift
|
|
57
|
+
else
|
|
58
|
+
output << nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
prev_price = price
|
|
62
|
+
end
|
|
63
|
+
output
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_indicator"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Simple Moving Average (SMA).
|
|
8
|
+
# SMA is a basic technical indicator that calculates the average price over a
|
|
9
|
+
# specified number of periods.
|
|
10
|
+
class SMA < BaseIndicator
|
|
11
|
+
# Calculates the Simple Moving Average.
|
|
12
|
+
# @return [Array<Float, nil>] The calculated SMA values.
|
|
13
|
+
def calculate
|
|
14
|
+
period = options[:period] || 20
|
|
15
|
+
CalculationHelpers.sma(numeric_data, period)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Defines the list of valid option keys for this indicator.
|
|
19
|
+
# @return [Array<Symbol>]
|
|
20
|
+
def self.valid_options
|
|
21
|
+
%i[field period]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Defines the minimum number of data points required for calculation.
|
|
25
|
+
# @param options [Hash] The current indicator options.
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
def self.min_data_size(options)
|
|
28
|
+
options[:period] || 20
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Stochastic Oscillator (SO).
|
|
6
|
+
# SO is a momentum indicator comparing a particular closing price of a
|
|
7
|
+
# security to a range of its prices over a certain period of time.
|
|
8
|
+
class SO
|
|
9
|
+
include CalculationHelpers
|
|
10
|
+
|
|
11
|
+
# Calculates the Stochastic Oscillator.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param k_period [Integer] Period for %K calculation (default: 14).
|
|
14
|
+
# @param k_slowing [Integer] Smoothing for %K (default: 3).
|
|
15
|
+
# @param d_period [Integer] Period for %D calculation (default: 3).
|
|
16
|
+
# @return [Array<Hash>] The calculated SO values { k: Float, d: Float }.
|
|
17
|
+
def self.calculate(data, k_period: 14, k_slowing: 3, d_period: 3)
|
|
18
|
+
highs = data.map { |d| d[:high] }
|
|
19
|
+
lows = data.map { |d| d[:low] }
|
|
20
|
+
closes = data.map { |d| d[:close] }
|
|
21
|
+
|
|
22
|
+
fast_ks = []
|
|
23
|
+
data.each_with_index do |_, i|
|
|
24
|
+
if i < k_period - 1
|
|
25
|
+
fast_ks << nil
|
|
26
|
+
else
|
|
27
|
+
current_highs = highs[(i - k_period + 1)..i]
|
|
28
|
+
current_lows = lows[(i - k_period + 1)..i]
|
|
29
|
+
|
|
30
|
+
hh = current_highs.max
|
|
31
|
+
ll = current_lows.min
|
|
32
|
+
|
|
33
|
+
fast_ks << if hh == ll
|
|
34
|
+
100.0
|
|
35
|
+
else
|
|
36
|
+
((closes[i] - ll) / (hh - ll) * 100.0)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Slow %K is SMA of fast %K
|
|
42
|
+
slow_ks = []
|
|
43
|
+
if k_slowing > 1
|
|
44
|
+
fast_ks.each_with_index do |_, i|
|
|
45
|
+
if i < (k_period - 1) + (k_slowing - 1)
|
|
46
|
+
slow_ks << nil
|
|
47
|
+
else
|
|
48
|
+
period_values = fast_ks[(i - k_slowing + 1)..i]
|
|
49
|
+
slow_ks << (period_values.sum / k_slowing.to_f)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
slow_ks = fast_ks
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# %D is SMA of slow %K
|
|
57
|
+
ds = []
|
|
58
|
+
slow_ks.each_with_index do |_, i|
|
|
59
|
+
if i < (k_period - 1) + (k_slowing > 1 ? (k_slowing - 1) : 0) + (d_period - 1)
|
|
60
|
+
ds << nil
|
|
61
|
+
else
|
|
62
|
+
period_values = slow_ks[(i - d_period + 1)..i]
|
|
63
|
+
ds << (period_values.sum / d_period.to_f)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
data.map.with_index do |_, i|
|
|
68
|
+
{
|
|
69
|
+
k: slow_ks[i]&.round(4),
|
|
70
|
+
d: ds[i]&.round(4)
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|