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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Chande Momentum Oscillator (CMO).
|
|
8
|
+
# CMO is a technical momentum indicator developed by Tushar Chande.
|
|
9
|
+
class CMO
|
|
10
|
+
# Calculates the Chande Momentum Oscillator.
|
|
11
|
+
# @param data [Array<Numeric>] Input data.
|
|
12
|
+
# @param period [Integer] Period for CMO calculation.
|
|
13
|
+
# @return [Array<Float, nil>]
|
|
14
|
+
def self.calculate(data, period: 14)
|
|
15
|
+
output = []
|
|
16
|
+
|
|
17
|
+
data.each_with_index do |_val, i|
|
|
18
|
+
if i < period
|
|
19
|
+
output << nil
|
|
20
|
+
next
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
up_sum = 0.0
|
|
24
|
+
down_sum = 0.0
|
|
25
|
+
|
|
26
|
+
(1..period).each do |j|
|
|
27
|
+
curr = data[i - period + j]
|
|
28
|
+
prev = data[i - period + j - 1]
|
|
29
|
+
diff = curr - prev
|
|
30
|
+
if diff.positive?
|
|
31
|
+
up_sum += diff
|
|
32
|
+
else
|
|
33
|
+
down_sum += diff.abs
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if (up_sum + down_sum).zero?
|
|
38
|
+
output << 0.0
|
|
39
|
+
else
|
|
40
|
+
cmo = 100.0 * (up_sum - down_sum) / (up_sum + down_sum)
|
|
41
|
+
output << cmo
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
output
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Cumulative Return (CR).
|
|
8
|
+
# CR is a measure of the total return on an investment over a set period of time.
|
|
9
|
+
class CR
|
|
10
|
+
# Calculates the Cumulative Return.
|
|
11
|
+
# @param data [Array<Numeric>] The input data points.
|
|
12
|
+
# @param period [Integer] The period (default: 1).
|
|
13
|
+
# @return [Array<Float>] The calculated CR values.
|
|
14
|
+
def self.calculate(data, period: 1) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
return [] if data.empty?
|
|
16
|
+
|
|
17
|
+
start_price = data.first.to_f
|
|
18
|
+
return Array.new(data.size, 0.0) if start_price.zero?
|
|
19
|
+
|
|
20
|
+
data.map do |v|
|
|
21
|
+
(v.to_f - start_price) / start_price
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Donchian Channel (DC).
|
|
8
|
+
# DC consists of three lines generated by moving average calculations that comprise
|
|
9
|
+
# an indicator formed by upper and lower bands around a midrange or median band.
|
|
10
|
+
class DC
|
|
11
|
+
# Calculates the Donchian Channel.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The DC period (default: 20).
|
|
14
|
+
# @return [Array<Hash>] The calculated DC values { upper: Float, middle: Float, lower: Float }.
|
|
15
|
+
def self.calculate(data, period: 20)
|
|
16
|
+
output = []
|
|
17
|
+
|
|
18
|
+
data.each_with_index do |_, i|
|
|
19
|
+
if i < period - 1
|
|
20
|
+
output << { upper: nil, middle: nil, lower: nil }
|
|
21
|
+
else
|
|
22
|
+
period_data = data[(i - period + 1)..i]
|
|
23
|
+
highs = period_data.map { |v| (v[:high] || v["high"]).to_f }
|
|
24
|
+
lows = period_data.map { |v| (v[:low] || v["low"]).to_f }
|
|
25
|
+
|
|
26
|
+
upper = highs.max
|
|
27
|
+
lower = lows.min
|
|
28
|
+
|
|
29
|
+
output << {
|
|
30
|
+
upper: upper,
|
|
31
|
+
middle: (upper + lower) / 2.0,
|
|
32
|
+
lower: lower
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
output
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Daily Log Return (DLR).
|
|
6
|
+
# DLR is the logarithmic return of a security's price from one day to the next.
|
|
7
|
+
class DLR
|
|
8
|
+
# Calculates the Daily Log Return.
|
|
9
|
+
# @param data [Array<Numeric>] The input data points.
|
|
10
|
+
# @return [Array<Float, nil>] The calculated DLR values.
|
|
11
|
+
def self.calculate(data)
|
|
12
|
+
output = []
|
|
13
|
+
prev_price = nil
|
|
14
|
+
|
|
15
|
+
data.each do |v|
|
|
16
|
+
output << if prev_price.nil?
|
|
17
|
+
nil
|
|
18
|
+
else
|
|
19
|
+
Math.log(v.to_f / prev_price)
|
|
20
|
+
end
|
|
21
|
+
prev_price = v
|
|
22
|
+
end
|
|
23
|
+
output
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Detrended Price Oscillator (DPO).
|
|
8
|
+
# DPO is an indicator that attempts to eliminate trend from price in order to
|
|
9
|
+
# make it easier to identify cycles.
|
|
10
|
+
class DPO
|
|
11
|
+
# Calculates the Detrended Price Oscillator.
|
|
12
|
+
# @param data [Array<Numeric>] The input data points.
|
|
13
|
+
# @param period [Integer] The DPO period (default: 20).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated DPO values.
|
|
15
|
+
def self.calculate(data, period: 20)
|
|
16
|
+
output = []
|
|
17
|
+
midpoint = (period / 2) + 1
|
|
18
|
+
|
|
19
|
+
data.each_with_index do |v, i|
|
|
20
|
+
if i < (period + midpoint - 2)
|
|
21
|
+
output << nil
|
|
22
|
+
else
|
|
23
|
+
sma_range = data[(i - midpoint - period + 2)..(i - midpoint + 1)]
|
|
24
|
+
sma = CalculationHelpers.average(sma_range)
|
|
25
|
+
output << (v - sma)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
output
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IndicatorHub
|
|
4
|
+
module Indicators
|
|
5
|
+
# Daily Return (DR).
|
|
6
|
+
# DR is the percentage change in price from one day to the next.
|
|
7
|
+
class DR
|
|
8
|
+
# Calculates the Daily Return.
|
|
9
|
+
# @param data [Array<Numeric>] The input data points.
|
|
10
|
+
# @return [Array<Float, nil>] The calculated DR values.
|
|
11
|
+
def self.calculate(data)
|
|
12
|
+
output = []
|
|
13
|
+
prev_price = nil
|
|
14
|
+
|
|
15
|
+
data.each do |v|
|
|
16
|
+
output << if prev_price.nil?
|
|
17
|
+
nil
|
|
18
|
+
else
|
|
19
|
+
(v.to_f / prev_price) - 1.0
|
|
20
|
+
end
|
|
21
|
+
prev_price = v
|
|
22
|
+
end
|
|
23
|
+
output
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Exponential Moving Average (EMA).
|
|
8
|
+
# EMA is a type of moving average that places a greater weight and significance
|
|
9
|
+
# on the most recent data points.
|
|
10
|
+
class EMA
|
|
11
|
+
# Calculates the Exponential Moving Average.
|
|
12
|
+
# @param data [Array<Numeric>] The input data points.
|
|
13
|
+
# @param period [Integer] The EMA period (default: 20).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated EMA 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
|
+
period_values = []
|
|
21
|
+
previous_ema = nil
|
|
22
|
+
output = []
|
|
23
|
+
|
|
24
|
+
data.each do |v|
|
|
25
|
+
period_values << v
|
|
26
|
+
|
|
27
|
+
if period_values.size == period
|
|
28
|
+
ema = CalculationHelpers.ema(v, period_values, period, previous_ema)
|
|
29
|
+
previous_ema = ema
|
|
30
|
+
output << ema
|
|
31
|
+
period_values.shift
|
|
32
|
+
else
|
|
33
|
+
output << nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
output
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ema"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Envelopes EMA.
|
|
8
|
+
# Envelopes consist of an EMA and two lines plotted at a percentage distance
|
|
9
|
+
# above and below the EMA.
|
|
10
|
+
class EnvelopesEMA
|
|
11
|
+
# Calculates the Envelopes EMA.
|
|
12
|
+
# @param data [Array<Numeric>] The input data points.
|
|
13
|
+
# @param period [Integer] The EMA period (default: 20).
|
|
14
|
+
# @param percentage [Numeric] The percentage distance (default: 5).
|
|
15
|
+
# @return [Array<Hash>] The calculated Envelopes EMA values { upper: Float, middle: Float, lower: Float }.
|
|
16
|
+
def self.calculate(data, period: 20, percentage: 5)
|
|
17
|
+
# Ensure we're working with numbers, if not, something upstream went wrong,
|
|
18
|
+
# but EMA.calculate will catch it if it expects only numbers.
|
|
19
|
+
ema_values = EMA.calculate(data, period: period)
|
|
20
|
+
|
|
21
|
+
output = []
|
|
22
|
+
ema_values.each do |ema|
|
|
23
|
+
if ema.nil?
|
|
24
|
+
output << { upper: nil, middle: nil, lower: nil }
|
|
25
|
+
else
|
|
26
|
+
upper = ema * (1 + (percentage / 100.0))
|
|
27
|
+
lower = ema * (1 - (percentage / 100.0))
|
|
28
|
+
output << { upper: upper, middle: ema, lower: lower }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
output
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Ease of Movement (EOM).
|
|
8
|
+
# EOM is a momentum oscillator that emphasizes the relationship between
|
|
9
|
+
# price change and volume.
|
|
10
|
+
class EOM
|
|
11
|
+
# Calculates the Ease of Movement.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The EOM period (default: 14).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated EOM values.
|
|
15
|
+
def self.calculate(data, period: 14)
|
|
16
|
+
output = []
|
|
17
|
+
emv_values = []
|
|
18
|
+
prev_v = nil
|
|
19
|
+
|
|
20
|
+
data.each do |v|
|
|
21
|
+
if prev_v.nil?
|
|
22
|
+
output << nil
|
|
23
|
+
prev_v = v
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
distance_moved = ((v[:high] + v[:low]) / 2.0) - ((prev_v[:high] + prev_v[:low]) / 2.0)
|
|
28
|
+
|
|
29
|
+
range = (v[:high] - v[:low])
|
|
30
|
+
box_ratio = range.zero? ? 0 : (v[:volume] / 100_000_000.0) / range
|
|
31
|
+
|
|
32
|
+
emv = box_ratio.zero? ? 0 : distance_moved / box_ratio
|
|
33
|
+
emv_values << emv
|
|
34
|
+
|
|
35
|
+
if emv_values.size == period
|
|
36
|
+
output << CalculationHelpers.average(emv_values)
|
|
37
|
+
emv_values.shift
|
|
38
|
+
else
|
|
39
|
+
output << nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
prev_v = v
|
|
43
|
+
end
|
|
44
|
+
output
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ema"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Force Index (FI).
|
|
8
|
+
# FI is an oscillator that uses price and volume to assess the power behind
|
|
9
|
+
# a move and identify potential turning points.
|
|
10
|
+
class FI
|
|
11
|
+
# Calculates the Force Index.
|
|
12
|
+
# @param data [Array<Numeric, Hash>] Array of prices or OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The FI period (default: 13).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated FI values.
|
|
15
|
+
def self.calculate(data, period: 13)
|
|
16
|
+
return [] if data.empty?
|
|
17
|
+
|
|
18
|
+
raw_fi = []
|
|
19
|
+
prev_close = nil
|
|
20
|
+
|
|
21
|
+
data.each do |v|
|
|
22
|
+
if v.is_a?(Hash)
|
|
23
|
+
close = (v[:close] || v["close"]).to_f
|
|
24
|
+
volume = (v[:volume] || v["volume"]).to_f
|
|
25
|
+
else
|
|
26
|
+
close = v.to_f
|
|
27
|
+
volume = 1.0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if prev_close.nil?
|
|
31
|
+
# First data point has no previous close to calculate force index
|
|
32
|
+
else
|
|
33
|
+
raw_fi << ((close - prev_close) * volume)
|
|
34
|
+
end
|
|
35
|
+
prev_close = close
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return Array.new(data.size, nil) if raw_fi.empty?
|
|
39
|
+
|
|
40
|
+
ema_fi = EMA.calculate(raw_fi, period: period)
|
|
41
|
+
[nil] + ema_fi
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Ichimoku Cloud.
|
|
8
|
+
# The Ichimoku Cloud is a collection of technical indicators that show
|
|
9
|
+
# support and resistance levels, as well as momentum and trend direction.
|
|
10
|
+
class Ichimoku
|
|
11
|
+
# Calculates the Ichimoku Cloud components.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param low_period [Integer] Tenkan-sen period (default: 9).
|
|
14
|
+
# @param medium_period [Integer] Kijun-sen period (default: 26).
|
|
15
|
+
# @param high_period [Integer] Senkou Span B period (default: 52).
|
|
16
|
+
# @return [Array<Hash, nil>] The calculated Ichimoku components.
|
|
17
|
+
def self.calculate(data, low_period: 9, medium_period: 26, high_period: 52)
|
|
18
|
+
# Expects array of hashes with :high, :low, :close
|
|
19
|
+
output = []
|
|
20
|
+
|
|
21
|
+
data.each_with_index do |_, index|
|
|
22
|
+
if index < high_period + medium_period - 2
|
|
23
|
+
output << nil
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
tenkan_sen = calculate_midpoint(index, low_period, data)
|
|
28
|
+
kijun_sen = calculate_midpoint(index, medium_period, data)
|
|
29
|
+
senkou_span_a = calculate_senkou_span_a(index, low_period, medium_period, data)
|
|
30
|
+
senkou_span_b = calculate_senkou_span_b(index, medium_period, high_period, data)
|
|
31
|
+
chikou_span = calculate_chikou_span(index, medium_period, data)
|
|
32
|
+
|
|
33
|
+
output << {
|
|
34
|
+
tenkan_sen: tenkan_sen,
|
|
35
|
+
kijun_sen: kijun_sen,
|
|
36
|
+
senkou_span_a: senkou_span_a,
|
|
37
|
+
senkou_span_b: senkou_span_b,
|
|
38
|
+
chikou_span: chikou_span
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
output
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.calculate_midpoint(index, period, data)
|
|
45
|
+
start_idx = [0, index - period + 1].max
|
|
46
|
+
period_data = data[start_idx..index]
|
|
47
|
+
highs = period_data.map { |d| d[:high] }
|
|
48
|
+
lows = period_data.map { |d| d[:low] }
|
|
49
|
+
(highs.max + lows.min) / 2.0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.calculate_senkou_span_a(index, low_period, medium_period, data)
|
|
53
|
+
mp_ago_index = index - (medium_period - 1)
|
|
54
|
+
return nil if mp_ago_index.negative?
|
|
55
|
+
|
|
56
|
+
t_sen = calculate_midpoint(mp_ago_index, low_period, data)
|
|
57
|
+
k_sen = calculate_midpoint(mp_ago_index, medium_period, data)
|
|
58
|
+
(t_sen + k_sen) / 2.0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.calculate_senkou_span_b(index, medium_period, high_period, data)
|
|
62
|
+
mp_ago_index = index - (medium_period - 1)
|
|
63
|
+
return nil if mp_ago_index.negative?
|
|
64
|
+
|
|
65
|
+
calculate_midpoint(mp_ago_index, high_period, data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.calculate_chikou_span(index, medium_period, data)
|
|
69
|
+
mp_ago_index = index - (medium_period - 1)
|
|
70
|
+
return nil if mp_ago_index.negative?
|
|
71
|
+
|
|
72
|
+
data[mp_ago_index][:close]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Intraday Momentum Index (IMI).
|
|
8
|
+
# IMI is a technical indicator that combines candlestick analysis with
|
|
9
|
+
# the Relative Strength Index (RSI).
|
|
10
|
+
class IMI
|
|
11
|
+
# Calculates the Intraday Momentum Index.
|
|
12
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
13
|
+
# @param period [Integer] The IMI period (default: 14).
|
|
14
|
+
# @return [Array<Float, nil>] The calculated IMI values.
|
|
15
|
+
def self.calculate(data, period: 14)
|
|
16
|
+
output = []
|
|
17
|
+
|
|
18
|
+
data.each_with_index do |_val, i|
|
|
19
|
+
if i < period - 1
|
|
20
|
+
output << nil
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
slice = data[(i - period + 1)..i]
|
|
25
|
+
gsum = 0.0
|
|
26
|
+
lsum = 0.0
|
|
27
|
+
|
|
28
|
+
slice.each do |d|
|
|
29
|
+
if d[:close] > d[:open]
|
|
30
|
+
gsum += (d[:close] - d[:open])
|
|
31
|
+
elsif d[:close] < d[:open]
|
|
32
|
+
lsum += (d[:open] - d[:close])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if (gsum + lsum).zero?
|
|
37
|
+
output << 0.0
|
|
38
|
+
else
|
|
39
|
+
imi = 100.0 * gsum / (gsum + lsum)
|
|
40
|
+
output << imi
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
output
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Keltner Channel (KC).
|
|
8
|
+
# KC is a volatility-based technical indicator composed of three separate lines.
|
|
9
|
+
class KC
|
|
10
|
+
# Calculates the Keltner Channel.
|
|
11
|
+
# @param data [Array<Hash>] Array of OHLCV hashes.
|
|
12
|
+
# @param period [Integer] The KC period (default: 20).
|
|
13
|
+
# @param multiplier [Float] The ATR multiplier (default: 1.5).
|
|
14
|
+
# @return [Array<Hash, nil>] The calculated KC values { upper: Float, middle: Float, lower: Float }.
|
|
15
|
+
def self.calculate(data, period: 20, multiplier: 1.5)
|
|
16
|
+
output = []
|
|
17
|
+
typical_prices = []
|
|
18
|
+
trading_ranges = []
|
|
19
|
+
|
|
20
|
+
data.each do |v|
|
|
21
|
+
tp = (v[:high] + v[:low] + v[:close]) / 3.0
|
|
22
|
+
tr = v[:high] - v[:low]
|
|
23
|
+
|
|
24
|
+
typical_prices << tp
|
|
25
|
+
trading_ranges << tr
|
|
26
|
+
|
|
27
|
+
if typical_prices.size == period
|
|
28
|
+
mb = CalculationHelpers.average(typical_prices)
|
|
29
|
+
tra = CalculationHelpers.average(trading_ranges)
|
|
30
|
+
|
|
31
|
+
output << {
|
|
32
|
+
upper: mb + (tra * multiplier),
|
|
33
|
+
middle: mb,
|
|
34
|
+
lower: mb - (tra * multiplier)
|
|
35
|
+
}
|
|
36
|
+
typical_prices.shift
|
|
37
|
+
trading_ranges.shift
|
|
38
|
+
else
|
|
39
|
+
output << nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
output
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../calculation_helpers"
|
|
4
|
+
require_relative "sma"
|
|
5
|
+
|
|
6
|
+
module IndicatorHub
|
|
7
|
+
module Indicators
|
|
8
|
+
# Know Sure Thing (KST).
|
|
9
|
+
# KST is a momentum oscillator based on the smoothed rate-of-change of four
|
|
10
|
+
# different timeframes.
|
|
11
|
+
class KST
|
|
12
|
+
# Calculates the Know Sure Thing.
|
|
13
|
+
# @param data [Array<Numeric, Hash>] Array of prices or OHLCV hashes.
|
|
14
|
+
# @param r1 [Integer] ROC period 1 (default: 10).
|
|
15
|
+
# @param r2 [Integer] ROC period 2 (default: 15).
|
|
16
|
+
# @param r3 [Integer] ROC period 3 (default: 20).
|
|
17
|
+
# @param r4 [Integer] ROC period 4 (default: 30).
|
|
18
|
+
# @param s1 [Integer] SMA period 1 (default: 10).
|
|
19
|
+
# @param s2 [Integer] SMA period 2 (default: 10).
|
|
20
|
+
# @param s3 [Integer] SMA period 3 (default: 10).
|
|
21
|
+
# @param s4 [Integer] SMA period 4 (default: 15).
|
|
22
|
+
# @param signal [Integer] Signal line period (default: 9).
|
|
23
|
+
# @return [Array<Hash>] The calculated KST values { kst: Float, signal: Float }.
|
|
24
|
+
def self.calculate(data, r1: 10, r2: 15, r3: 20, r4: 30, s1: 10, s2: 10, s3: 10, s4: 15, signal: 9)
|
|
25
|
+
# Use close prices for KST
|
|
26
|
+
closes = data.map do |v|
|
|
27
|
+
if v.is_a?(Hash)
|
|
28
|
+
(v[:close] || v["close"]).to_f
|
|
29
|
+
else
|
|
30
|
+
v.to_f
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
kst_values = []
|
|
35
|
+
closes.each_with_index do |_, i|
|
|
36
|
+
rcma1 = calculate_rcma(closes, i, r1, s1)
|
|
37
|
+
rcma2 = calculate_rcma(closes, i, r2, s2)
|
|
38
|
+
rcma3 = calculate_rcma(closes, i, r3, s3)
|
|
39
|
+
rcma4 = calculate_rcma(closes, i, r4, s4)
|
|
40
|
+
|
|
41
|
+
if rcma1 && rcma2 && rcma3 && rcma4
|
|
42
|
+
kst = (1.0 * rcma1) + (2.0 * rcma2) + (3.0 * rcma3) + (4.0 * rcma4)
|
|
43
|
+
kst_values << kst
|
|
44
|
+
else
|
|
45
|
+
kst_values << nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Calculate signal line (SMA of KST)
|
|
50
|
+
valid_kst = kst_values.compact
|
|
51
|
+
if valid_kst.size >= signal
|
|
52
|
+
signal_line_valid = CalculationHelpers.sma(valid_kst, signal)
|
|
53
|
+
lead_nils_count = kst_values.count(nil)
|
|
54
|
+
full_signal_line = Array.new(lead_nils_count, nil) + signal_line_valid
|
|
55
|
+
else
|
|
56
|
+
full_signal_line = Array.new(kst_values.size, nil)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
kst_values.zip(full_signal_line).map do |kst, sig|
|
|
60
|
+
{ kst: kst, signal: sig }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.calculate_rcma(data, index, roc, sma)
|
|
65
|
+
# ROC = (Price(t) - Price(t-roc)) / Price(t-roc) * 100
|
|
66
|
+
# RCMA = SMA of ROC over 'sma' periods
|
|
67
|
+
return nil if index < (roc + sma)
|
|
68
|
+
|
|
69
|
+
roc_data = []
|
|
70
|
+
((index - sma + 1)..index).each do |i|
|
|
71
|
+
current_price = data[i]
|
|
72
|
+
past_price = data[i - roc]
|
|
73
|
+
return nil if past_price.nil? || past_price.zero?
|
|
74
|
+
|
|
75
|
+
roc_data << ((current_price - past_price) / past_price.to_f * 100.0)
|
|
76
|
+
end
|
|
77
|
+
CalculationHelpers.average(roc_data)
|
|
78
|
+
end
|
|
79
|
+
private_class_method :calculate_rcma
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ema"
|
|
4
|
+
|
|
5
|
+
module IndicatorHub
|
|
6
|
+
module Indicators
|
|
7
|
+
# Moving Average Convergence Divergence (MACD).
|
|
8
|
+
# MACD is a trend-following momentum indicator that shows the relationship
|
|
9
|
+
# between two moving averages of a security’s price.
|
|
10
|
+
class MACD
|
|
11
|
+
# Calculates the Moving Average Convergence Divergence.
|
|
12
|
+
# @param data [Array<Numeric>] The input data points.
|
|
13
|
+
# @param fast_period [Integer] The fast EMA period (default: 12).
|
|
14
|
+
# @param slow_period [Integer] The slow EMA period (default: 26).
|
|
15
|
+
# @param signal_period [Integer] The signal EMA period (default: 9).
|
|
16
|
+
# @return [Array<Hash, nil>] The calculated MACD components (macd, signal, histogram).
|
|
17
|
+
def self.calculate(data, fast_period: 12, slow_period: 26, signal_period: 9)
|
|
18
|
+
fast_ema = EMA.calculate(data, period: fast_period)
|
|
19
|
+
slow_ema = EMA.calculate(data, period: slow_period)
|
|
20
|
+
|
|
21
|
+
macd_line = []
|
|
22
|
+
data.each_with_index do |_, i|
|
|
23
|
+
macd_line << (fast_ema[i] - slow_ema[i] if fast_ema[i] && slow_ema[i])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Signal Line is EMA of MACD line (ignoring initial nil values)
|
|
27
|
+
compact_macd = macd_line.compact
|
|
28
|
+
compact_signal = EMA.calculate(compact_macd, period: signal_period)
|
|
29
|
+
|
|
30
|
+
# Re-align signal line with original data
|
|
31
|
+
signal_line = Array.new(macd_line.size - compact_signal.size, nil) + compact_signal
|
|
32
|
+
|
|
33
|
+
output = []
|
|
34
|
+
data.each_with_index do |_, i|
|
|
35
|
+
histogram = macd_line[i] && signal_line[i] ? (macd_line[i] - signal_line[i]) : nil
|
|
36
|
+
output << {
|
|
37
|
+
macd: macd_line[i],
|
|
38
|
+
signal: signal_line[i],
|
|
39
|
+
histogram: histogram
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
output
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|