DhanHQ 2.1.0 → 2.1.3
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 +4 -4
- data/README.md +17 -11
- data/docs/technical_analysis.md +143 -0
- data/lib/DhanHQ/constants.rb +4 -6
- data/lib/DhanHQ/contracts/instrument_list_contract.rb +12 -0
- data/lib/DhanHQ/helpers/request_helper.rb +5 -1
- data/lib/DhanHQ/models/instrument.rb +56 -0
- data/lib/DhanHQ/resources/instruments.rb +28 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ.rb +8 -0
- data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +83 -0
- data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +23 -0
- data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +230 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +250 -0
- data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +23 -0
- data/lib/ta/candles.rb +51 -0
- data/lib/ta/fetcher.rb +69 -0
- data/lib/ta/indicators.rb +168 -0
- data/lib/ta/market_calendar.rb +50 -0
- data/lib/ta/technical_analysis.rb +92 -302
- data/lib/ta.rb +7 -0
- metadata +15 -1
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TA
|
4
|
+
module Indicators
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def ema(series, period)
|
8
|
+
return nil if series.nil? || series.empty?
|
9
|
+
|
10
|
+
k = 2.0 / (period + 1)
|
11
|
+
series.each_with_index.reduce(nil) do |ema_prev, (v, i)|
|
12
|
+
i == 0 ? v.to_f : (v.to_f * k) + ((ema_prev || v.to_f) * (1 - k))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def rsi(series, period)
|
17
|
+
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:RSI)
|
18
|
+
return RubyTechnicalAnalysis::RSI.new(series: series, period: period).call
|
19
|
+
end
|
20
|
+
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:rsi)
|
21
|
+
return TechnicalAnalysis.rsi(series, period: period)
|
22
|
+
end
|
23
|
+
|
24
|
+
simple_rsi(series, period)
|
25
|
+
end
|
26
|
+
|
27
|
+
def macd(series, fast, slow, signal)
|
28
|
+
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:MACD)
|
29
|
+
out = RubyTechnicalAnalysis::MACD.new(series: series, fast_period: fast, slow_period: slow,
|
30
|
+
signal_period: signal).call
|
31
|
+
if out.is_a?(Hash)
|
32
|
+
m = out[:macd]
|
33
|
+
s = out[:signal]
|
34
|
+
h = out[:histogram] || out[:hist]
|
35
|
+
m = m.last if m.is_a?(Array)
|
36
|
+
s = s.last if s.is_a?(Array)
|
37
|
+
h = h.last if h.is_a?(Array)
|
38
|
+
return { macd: m, signal: s, hist: h }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:macd)
|
42
|
+
out = TechnicalAnalysis.macd(series, fast: fast, slow: slow, signal: signal)
|
43
|
+
if out.is_a?(Hash)
|
44
|
+
m = out[:macd]
|
45
|
+
s = out[:signal]
|
46
|
+
h = out[:hist]
|
47
|
+
m = m.last if m.is_a?(Array)
|
48
|
+
s = s.last if s.is_a?(Array)
|
49
|
+
h = h.last if h.is_a?(Array)
|
50
|
+
return { macd: m, signal: s, hist: h }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
simple_macd(series, fast, slow, signal)
|
54
|
+
end
|
55
|
+
|
56
|
+
def adx(high, low, close, period)
|
57
|
+
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:ADX)
|
58
|
+
return RubyTechnicalAnalysis::ADX.new(high: high, low: low, close: close, period: period).call
|
59
|
+
end
|
60
|
+
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:adx)
|
61
|
+
return TechnicalAnalysis.adx(high: high, low: low, close: close, period: period)
|
62
|
+
end
|
63
|
+
|
64
|
+
simple_adx(high, low, close, period)
|
65
|
+
end
|
66
|
+
|
67
|
+
def atr(high, low, close, period)
|
68
|
+
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:ATR)
|
69
|
+
return RubyTechnicalAnalysis::ATR.new(high: high, low: low, close: close, period: period).call
|
70
|
+
end
|
71
|
+
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:atr)
|
72
|
+
return TechnicalAnalysis.atr(high: high, low: low, close: close, period: period)
|
73
|
+
end
|
74
|
+
|
75
|
+
simple_atr(high, low, close, period)
|
76
|
+
end
|
77
|
+
|
78
|
+
def simple_rsi(series, period)
|
79
|
+
gains = []
|
80
|
+
losses = []
|
81
|
+
series.each_cons(2) do |a, b|
|
82
|
+
ch = b - a
|
83
|
+
gains << [ch, 0].max
|
84
|
+
losses << [(-ch), 0].max
|
85
|
+
end
|
86
|
+
avg_gain = gains.first(period).sum / period.to_f
|
87
|
+
avg_loss = losses.first(period).sum / period.to_f
|
88
|
+
rsi_vals = Array.new(series.size, nil)
|
89
|
+
gains.drop(period).each_with_index do |g, idx|
|
90
|
+
l = losses[period + idx]
|
91
|
+
avg_gain = ((avg_gain * (period - 1)) + g) / period
|
92
|
+
avg_loss = ((avg_loss * (period - 1)) + l) / period
|
93
|
+
rs = avg_loss.zero? ? 100.0 : (avg_gain / avg_loss)
|
94
|
+
rsi_vals[period + 1 + idx] = 100.0 - (100.0 / (1 + rs))
|
95
|
+
end
|
96
|
+
rsi_vals
|
97
|
+
end
|
98
|
+
|
99
|
+
def simple_macd(series, fast, slow, signal)
|
100
|
+
e_fast = ema(series, fast)
|
101
|
+
e_slow = ema(series, slow)
|
102
|
+
e_sig = ema(series, signal)
|
103
|
+
return { macd: nil, signal: nil, hist: nil } if [e_fast, e_slow, e_sig].any?(&:nil?)
|
104
|
+
|
105
|
+
macd_line = e_fast - e_slow
|
106
|
+
signal_line = e_sig
|
107
|
+
{ macd: macd_line, signal: signal_line, hist: macd_line - signal_line }
|
108
|
+
end
|
109
|
+
|
110
|
+
def true_ranges(high, low, close)
|
111
|
+
trs = []
|
112
|
+
close.each_with_index do |_c, i|
|
113
|
+
if i.zero?
|
114
|
+
trs << (high[i] - low[i]).abs
|
115
|
+
else
|
116
|
+
tr = [(high[i] - low[i]).abs, (high[i] - close[i - 1]).abs, (low[i] - close[i - 1]).abs].max
|
117
|
+
trs << tr
|
118
|
+
end
|
119
|
+
end
|
120
|
+
trs
|
121
|
+
end
|
122
|
+
|
123
|
+
def simple_atr(high, low, close, period)
|
124
|
+
trs = true_ranges(high, low, close)
|
125
|
+
out = []
|
126
|
+
atr_prev = trs.first(period).sum / period.to_f
|
127
|
+
trs.each_with_index do |tr, i|
|
128
|
+
if i < period
|
129
|
+
out << nil
|
130
|
+
elsif i == period
|
131
|
+
out << atr_prev
|
132
|
+
else
|
133
|
+
atr_prev = ((atr_prev * (period - 1)) + tr) / period.to_f
|
134
|
+
out << atr_prev
|
135
|
+
end
|
136
|
+
end
|
137
|
+
out
|
138
|
+
end
|
139
|
+
|
140
|
+
def simple_adx(high, low, close, period)
|
141
|
+
plus_dm = [0]
|
142
|
+
minus_dm = [0]
|
143
|
+
(1...high.size).each do |i|
|
144
|
+
up_move = high[i] - high[i - 1]
|
145
|
+
down_move = low[i - 1] - low[i]
|
146
|
+
plus_dm << (up_move > down_move && up_move.positive? ? up_move : 0)
|
147
|
+
minus_dm << (down_move > up_move && down_move.positive? ? down_move : 0)
|
148
|
+
end
|
149
|
+
trs = true_ranges(high, low, close)
|
150
|
+
smooth_tr = trs.first(period).sum
|
151
|
+
smooth_plus_dm = plus_dm.first(period).sum
|
152
|
+
smooth_minus_dm = minus_dm.first(period).sum
|
153
|
+
adx_vals = Array.new(high.size, nil)
|
154
|
+
di_vals = []
|
155
|
+
(period...high.size).each do |i|
|
156
|
+
smooth_tr = smooth_tr - (smooth_tr / period) + trs[i]
|
157
|
+
smooth_plus_dm = smooth_plus_dm - (smooth_plus_dm / period) + plus_dm[i]
|
158
|
+
smooth_minus_dm = smooth_minus_dm - (smooth_minus_dm / period) + minus_dm[i]
|
159
|
+
plus_di = 100.0 * (smooth_plus_dm / smooth_tr)
|
160
|
+
minus_di = 100.0 * (smooth_minus_dm / smooth_tr)
|
161
|
+
dx = 100.0 * ((plus_di - minus_di).abs / (plus_di + minus_di))
|
162
|
+
di_vals << dx
|
163
|
+
adx_vals[i] = di_vals.last(period).sum / period.to_f if di_vals.size >= period
|
164
|
+
end
|
165
|
+
adx_vals
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module TA
|
6
|
+
module MarketCalendar
|
7
|
+
MARKET_HOLIDAYS = [
|
8
|
+
Date.new(2025, 8, 15),
|
9
|
+
Date.new(2025, 10, 2),
|
10
|
+
Date.new(2025, 8, 27)
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
def self.weekday?(date)
|
14
|
+
w = date.wday
|
15
|
+
w.between?(1, 5)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.trading_day?(date)
|
19
|
+
weekday?(date) && !MARKET_HOLIDAYS.include?(date)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.last_trading_day(from: Date.today)
|
23
|
+
d = from
|
24
|
+
d -= 1 until trading_day?(d)
|
25
|
+
d
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.prev_trading_day(from: Date.today)
|
29
|
+
d = from - 1
|
30
|
+
d -= 1 until trading_day?(d)
|
31
|
+
d
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.today_or_last_trading_day
|
35
|
+
trading_day?(Date.today) ? Date.today : last_trading_day(from: Date.today)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.trading_days_ago(date, n)
|
39
|
+
raise ArgumentError, "n must be >= 0" if n.to_i.negative?
|
40
|
+
|
41
|
+
d = trading_day?(date) ? date : today_or_last_trading_day
|
42
|
+
count = 0
|
43
|
+
while count < n
|
44
|
+
d = prev_trading_day(from: d)
|
45
|
+
count += 1
|
46
|
+
end
|
47
|
+
d
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -17,50 +17,10 @@ rescue LoadError => e
|
|
17
17
|
end
|
18
18
|
|
19
19
|
require "DhanHQ"
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
Date.new(2025, 8, 15),
|
25
|
-
Date.new(2025, 10, 2),
|
26
|
-
Date.new(2025, 8, 27)
|
27
|
-
].freeze
|
28
|
-
|
29
|
-
def self.weekday?(date)
|
30
|
-
w = date.wday
|
31
|
-
w >= 1 && w <= 5
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.trading_day?(date)
|
35
|
-
weekday?(date) && !MARKET_HOLIDAYS.include?(date)
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.last_trading_day(from: Date.today)
|
39
|
-
d = from
|
40
|
-
d -= 1 until trading_day?(d)
|
41
|
-
d
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.today_or_last_trading_day
|
45
|
-
trading_day?(Date.today) ? Date.today : last_trading_day(from: Date.today)
|
46
|
-
end
|
47
|
-
|
48
|
-
# Returns the trading day N days back from the given trading day.
|
49
|
-
# Example: trading_days_ago(2025-10-07, 0) -> 2025-10-07 (if trading day)
|
50
|
-
# trading_days_ago(2025-10-07, 1) -> previous trading day
|
51
|
-
def self.trading_days_ago(date, n)
|
52
|
-
raise ArgumentError, "n must be >= 0" if n.to_i.negative?
|
53
|
-
|
54
|
-
d = trading_day?(date) ? date : today_or_last_trading_day
|
55
|
-
count = 0
|
56
|
-
while count < n
|
57
|
-
d = last_trading_day(from: d)
|
58
|
-
count += 1
|
59
|
-
end
|
60
|
-
d
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
20
|
+
require_relative "market_calendar"
|
21
|
+
require_relative "candles"
|
22
|
+
require_relative "indicators"
|
23
|
+
require_relative "fetcher"
|
64
24
|
|
65
25
|
module TA
|
66
26
|
class TechnicalAnalysis
|
@@ -70,24 +30,27 @@ module TA
|
|
70
30
|
adx_period: 14,
|
71
31
|
macd_fast: 12,
|
72
32
|
macd_slow: 26,
|
73
|
-
macd_signal: 9
|
33
|
+
macd_signal: 9,
|
34
|
+
throttle_seconds: 1.0,
|
35
|
+
max_retries: 3
|
74
36
|
}.freeze
|
75
37
|
|
76
38
|
def initialize(options = {})
|
77
39
|
@opts = DEFAULTS.merge(options.transform_keys(&:to_sym))
|
40
|
+
@fetcher = Fetcher.new(throttle_seconds: @opts[:throttle_seconds], max_retries: @opts[:max_retries])
|
78
41
|
end
|
79
42
|
|
80
43
|
def compute(exchange_segment:, instrument:, security_id:, from_date: nil, to_date: nil, days_back: nil,
|
81
44
|
intervals: [1, 5, 15, 25, 60])
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
45
|
+
# Normalize to_date: default to last trading day; if provided and non-trading, roll back
|
46
|
+
to_date = normalize_to_date(to_date)
|
47
|
+
|
48
|
+
# Auto-calculate required trading days if not provided
|
49
|
+
days_back = auto_days_needed(intervals) if days_back.nil? || days_back.to_i <= 0
|
50
|
+
|
51
|
+
# Derive/normalize from_date
|
52
|
+
from_date = normalize_from_date(from_date, to_date, days_back)
|
53
|
+
|
91
54
|
base_params = {
|
92
55
|
exchange_segment: exchange_segment,
|
93
56
|
instrument: instrument,
|
@@ -96,17 +59,15 @@ module TA
|
|
96
59
|
to_date: to_date
|
97
60
|
}
|
98
61
|
|
99
|
-
one_min_candles = candles(fetch_intraday_windowed(base_params, 1))
|
100
|
-
|
101
62
|
frames = {}
|
63
|
+
interval_key = { 1 => :m1, 5 => :m5, 15 => :m15, 25 => :m25, 60 => :m60 }
|
102
64
|
intervals.each do |ivl|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
end
|
65
|
+
key = interval_key[ivl.to_i]
|
66
|
+
next unless key
|
67
|
+
|
68
|
+
raw = @fetcher.intraday_windowed(base_params, ivl.to_i)
|
69
|
+
frames[key] = Candles.from_series(raw)
|
70
|
+
sleep_with_jitter # throttle between intervals
|
110
71
|
end
|
111
72
|
|
112
73
|
{
|
@@ -123,15 +84,15 @@ module TA
|
|
123
84
|
|
124
85
|
def compute_from_file(path:, base_interval: 1, intervals: [1, 5, 15, 25, 60])
|
125
86
|
raw = JSON.parse(File.read(path))
|
126
|
-
base =
|
87
|
+
base = Candles.from_series(raw)
|
127
88
|
frames = {}
|
128
89
|
intervals.each do |ivl|
|
129
90
|
case ivl.to_i
|
130
|
-
when 1 then frames[:m1] = (base_interval == 1 ? base : resample(base, 1))
|
131
|
-
when 5 then frames[:m5] = resample(base, 5)
|
132
|
-
when 15 then frames[:m15] = resample(base, 15)
|
133
|
-
when 25 then frames[:m25] = resample(base, 25)
|
134
|
-
when 60 then frames[:m60] = resample(base, 60)
|
91
|
+
when 1 then frames[:m1] = (base_interval == 1 ? base : Candles.resample(base, 1))
|
92
|
+
when 5 then frames[:m5] = Candles.resample(base, 5)
|
93
|
+
when 15 then frames[:m15] = Candles.resample(base, 15)
|
94
|
+
when 25 then frames[:m25] = Candles.resample(base, 25)
|
95
|
+
when 60 then frames[:m60] = Candles.resample(base, 60)
|
135
96
|
end
|
136
97
|
end
|
137
98
|
{ indicators: frames.transform_values { |candles| compute_for(candles) } }
|
@@ -139,95 +100,87 @@ module TA
|
|
139
100
|
|
140
101
|
private
|
141
102
|
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
instrument: params[:instrument],
|
147
|
-
interval: interval.to_s,
|
148
|
-
from_date: params[:from_date],
|
149
|
-
to_date: params[:to_date]
|
150
|
-
)
|
103
|
+
def sleep_with_jitter(multiplier = 1.0)
|
104
|
+
base = (@opts[:throttle_seconds] || 3.0).to_f * multiplier
|
105
|
+
jitter = rand * 0.3
|
106
|
+
sleep(base + jitter)
|
151
107
|
end
|
152
108
|
|
153
|
-
def
|
154
|
-
|
155
|
-
to_d = Date.parse(params[:to_date])
|
156
|
-
max_span = 90
|
157
|
-
return fetch_intraday(params, interval) if (to_d - from_d).to_i <= max_span
|
109
|
+
def normalize_to_date(to_date)
|
110
|
+
return MarketCalendar.today_or_last_trading_day.strftime("%Y-%m-%d") if to_date.nil? || to_date.to_s.strip.empty?
|
158
111
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
end
|
169
|
-
cursor = chunk_to + 1
|
112
|
+
to_d_raw = begin
|
113
|
+
Date.parse(to_date)
|
114
|
+
rescue StandardError
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
if to_d_raw && !MarketCalendar.trading_day?(to_d_raw)
|
118
|
+
MarketCalendar.last_trading_day(from: to_d_raw).strftime("%Y-%m-%d")
|
119
|
+
else
|
120
|
+
to_date
|
170
121
|
end
|
171
|
-
merged
|
172
122
|
end
|
173
123
|
|
174
|
-
def
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
124
|
+
def normalize_from_date(from_date, to_date, days_back)
|
125
|
+
if (from_date.nil? || from_date.to_s.strip.empty?) && days_back && days_back.to_i.positive?
|
126
|
+
to_d = Date.parse(to_date)
|
127
|
+
n_back = [days_back.to_i - 1, 0].max
|
128
|
+
return MarketCalendar.trading_days_ago(to_d, n_back).strftime("%Y-%m-%d")
|
129
|
+
end
|
130
|
+
if from_date && !from_date.to_s.strip.empty?
|
131
|
+
f_d_raw = begin
|
132
|
+
Date.parse(from_date)
|
133
|
+
rescue StandardError
|
134
|
+
nil
|
135
|
+
end
|
136
|
+
if f_d_raw && !MarketCalendar.trading_day?(f_d_raw)
|
137
|
+
fd = f_d_raw
|
138
|
+
fd += 1 until MarketCalendar.trading_day?(fd)
|
139
|
+
to_d = Date.parse(to_date)
|
140
|
+
return [fd, to_d].min.strftime("%Y-%m-%d")
|
141
|
+
end
|
142
|
+
return from_date
|
143
|
+
end
|
144
|
+
to_date
|
145
|
+
end
|
179
146
|
|
180
|
-
|
147
|
+
# Calculate how many bars we need based on indicator periods
|
148
|
+
def required_bars_for_indicators
|
149
|
+
rsi_need = (@opts[:rsi_period] || 14).to_i + 1
|
150
|
+
atr_need = (@opts[:atr_period] || 14).to_i + 1
|
151
|
+
adx_need = (@opts[:adx_period] || 14).to_i * 2
|
152
|
+
macd_need = (@opts[:macd_slow] || 26).to_i
|
153
|
+
[rsi_need, atr_need, adx_need, macd_need].max
|
181
154
|
end
|
182
155
|
|
183
|
-
def
|
184
|
-
|
185
|
-
|
186
|
-
high = series["high"] || series[:high]
|
187
|
-
low = series["low"] || series[:low]
|
188
|
-
close = series["close"] || series[:close]
|
189
|
-
vol = series["volume"] || series[:volume]
|
190
|
-
return [] unless ts && open && high && low && close && vol
|
191
|
-
return [] if close.empty?
|
156
|
+
def bars_per_trading_day(interval_minutes)
|
157
|
+
minutes = interval_minutes.to_i
|
158
|
+
return 1 if minutes <= 0
|
192
159
|
|
193
|
-
(0
|
194
|
-
{ t: parse_time_like(ts[i]), o: open[i].to_f, h: high[i].to_f, l: low[i].to_f, c: close[i].to_f,
|
195
|
-
v: vol[i].to_f }
|
196
|
-
end
|
197
|
-
rescue StandardError
|
198
|
-
[]
|
160
|
+
(375.0 / minutes).floor
|
199
161
|
end
|
200
162
|
|
201
|
-
def
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
candles.each do |c|
|
206
|
-
key = Time.at((c[:t].to_i / 60) / minutes * minutes * 60)
|
207
|
-
b = (grouped[key] ||= { t: key, o: c[:o], h: c[:h], l: c[:l], c: c[:c], v: 0.0 })
|
208
|
-
b[:h] = [b[:h], c[:h]].max
|
209
|
-
b[:l] = [b[:l], c[:l]].min
|
210
|
-
b[:c] = c[:c]
|
211
|
-
b[:v] += c[:v]
|
212
|
-
end
|
213
|
-
grouped.keys.sort.map { |k| grouped[k] }
|
163
|
+
def days_needed_for_interval(interval_minutes)
|
164
|
+
need = required_bars_for_indicators
|
165
|
+
per_day = [bars_per_trading_day(interval_minutes), 1].max
|
166
|
+
((need + per_day - 1) / per_day)
|
214
167
|
end
|
215
168
|
|
216
|
-
def
|
217
|
-
|
218
|
-
|
169
|
+
def auto_days_needed(intervals)
|
170
|
+
Array(intervals).map { |ivl| days_needed_for_interval(ivl.to_i) }.max || 1
|
171
|
+
end
|
219
172
|
|
220
173
|
def compute_for(candles)
|
221
|
-
c =
|
222
|
-
h =
|
223
|
-
l =
|
174
|
+
c = candles.map { |k| k[:c] }
|
175
|
+
h = candles.map { |k| k[:h] }
|
176
|
+
l = candles.map { |k| k[:l] }
|
224
177
|
return { rsi: nil, macd: { macd: nil, signal: nil, hist: nil }, adx: nil, atr: nil } if c.empty?
|
225
178
|
|
226
179
|
{
|
227
|
-
rsi: safe_last(rsi(c, @opts[:rsi_period])),
|
228
|
-
macd: macd(c, @opts[:macd_fast], @opts[:macd_slow], @opts[:macd_signal]),
|
229
|
-
adx: safe_last(adx(h, l, c, @opts[:adx_period])),
|
230
|
-
atr: safe_last(atr(h, l, c, @opts[:atr_period]))
|
180
|
+
rsi: safe_last(Indicators.rsi(c, @opts[:rsi_period])),
|
181
|
+
macd: Indicators.macd(c, @opts[:macd_fast], @opts[:macd_slow], @opts[:macd_signal]),
|
182
|
+
adx: safe_last(Indicators.adx(h, l, c, @opts[:adx_period])),
|
183
|
+
atr: safe_last(Indicators.atr(h, l, c, @opts[:atr_period]))
|
231
184
|
}
|
232
185
|
end
|
233
186
|
|
@@ -238,168 +191,5 @@ module TA
|
|
238
191
|
rescue StandardError
|
239
192
|
nil
|
240
193
|
end
|
241
|
-
|
242
|
-
# ---- Indicator adapters (mirror bin script) ----
|
243
|
-
def rsi(series, period)
|
244
|
-
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:RSI)
|
245
|
-
return RubyTechnicalAnalysis::RSI.new(series: series, period: period).call
|
246
|
-
end
|
247
|
-
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:rsi)
|
248
|
-
return TechnicalAnalysis.rsi(series, period: period)
|
249
|
-
end
|
250
|
-
|
251
|
-
simple_rsi(series, period)
|
252
|
-
end
|
253
|
-
|
254
|
-
def macd(series, fast, slow, signal)
|
255
|
-
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:MACD)
|
256
|
-
out = RubyTechnicalAnalysis::MACD.new(series: series, fast_period: fast, slow_period: slow,
|
257
|
-
signal_period: signal).call
|
258
|
-
if out.is_a?(Hash)
|
259
|
-
m = out[:macd]
|
260
|
-
s = out[:signal]
|
261
|
-
h = out[:histogram] || out[:hist]
|
262
|
-
m = m.last if m.is_a?(Array)
|
263
|
-
s = s.last if s.is_a?(Array)
|
264
|
-
h = h.last if h.is_a?(Array)
|
265
|
-
return { macd: m, signal: s, hist: h }
|
266
|
-
end
|
267
|
-
end
|
268
|
-
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:macd)
|
269
|
-
out = TechnicalAnalysis.macd(series, fast: fast, slow: slow, signal: signal)
|
270
|
-
if out.is_a?(Hash)
|
271
|
-
m = out[:macd]
|
272
|
-
s = out[:signal]
|
273
|
-
h = out[:hist]
|
274
|
-
m = m.last if m.is_a?(Array)
|
275
|
-
s = s.last if s.is_a?(Array)
|
276
|
-
h = h.last if h.is_a?(Array)
|
277
|
-
return { macd: m, signal: s, hist: h }
|
278
|
-
end
|
279
|
-
end
|
280
|
-
simple_macd(series, fast, slow, signal)
|
281
|
-
end
|
282
|
-
|
283
|
-
def adx(high, low, close, period)
|
284
|
-
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:ADX)
|
285
|
-
return RubyTechnicalAnalysis::ADX.new(high: high, low: low, close: close, period: period).call
|
286
|
-
end
|
287
|
-
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:adx)
|
288
|
-
return TechnicalAnalysis.adx(high: high, low: low, close: close, period: period)
|
289
|
-
end
|
290
|
-
|
291
|
-
simple_adx(high, low, close, period)
|
292
|
-
end
|
293
|
-
|
294
|
-
def atr(high, low, close, period)
|
295
|
-
if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:ATR)
|
296
|
-
return RubyTechnicalAnalysis::ATR.new(high: high, low: low, close: close, period: period).call
|
297
|
-
end
|
298
|
-
if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:atr)
|
299
|
-
return TechnicalAnalysis.atr(high: high, low: low, close: close, period: period)
|
300
|
-
end
|
301
|
-
|
302
|
-
simple_atr(high, low, close, period)
|
303
|
-
end
|
304
|
-
|
305
|
-
# ---- Simple fallbacks ----
|
306
|
-
def ema(series, period)
|
307
|
-
return nil if series.nil? || series.empty?
|
308
|
-
|
309
|
-
k = 2.0 / (period + 1)
|
310
|
-
series.each_with_index.reduce(nil) do |ema_prev, (v, i)|
|
311
|
-
i == 0 ? v.to_f : (v.to_f * k) + ((ema_prev || v.to_f) * (1 - k))
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
def simple_rsi(series, period)
|
316
|
-
gains = []
|
317
|
-
losses = []
|
318
|
-
series.each_cons(2) do |a, b|
|
319
|
-
ch = b - a
|
320
|
-
gains << [ch, 0].max
|
321
|
-
losses << [(-ch), 0].max
|
322
|
-
end
|
323
|
-
avg_gain = gains.first(period).sum / period.to_f
|
324
|
-
avg_loss = losses.first(period).sum / period.to_f
|
325
|
-
rsi_vals = Array.new(series.size, nil)
|
326
|
-
gains.drop(period).each_with_index do |g, idx|
|
327
|
-
l = losses[period + idx]
|
328
|
-
avg_gain = ((avg_gain * (period - 1)) + g) / period
|
329
|
-
avg_loss = ((avg_loss * (period - 1)) + l) / period
|
330
|
-
rs = avg_loss.zero? ? 100.0 : (avg_gain / avg_loss)
|
331
|
-
rsi_vals[period + 1 + idx] = 100.0 - (100.0 / (1 + rs))
|
332
|
-
end
|
333
|
-
rsi_vals
|
334
|
-
end
|
335
|
-
|
336
|
-
def simple_macd(series, fast, slow, signal)
|
337
|
-
e_fast = ema(series, fast)
|
338
|
-
e_slow = ema(series, slow)
|
339
|
-
e_sig = ema(series, signal)
|
340
|
-
return { macd: nil, signal: nil, hist: nil } if [e_fast, e_slow, e_sig].any?(&:nil?)
|
341
|
-
|
342
|
-
macd_line = e_fast - e_slow
|
343
|
-
signal_line = e_sig
|
344
|
-
{ macd: macd_line, signal: signal_line, hist: macd_line - signal_line }
|
345
|
-
end
|
346
|
-
|
347
|
-
def true_ranges(high, low, close)
|
348
|
-
trs = []
|
349
|
-
close.each_with_index do |_c, i|
|
350
|
-
if i.zero?
|
351
|
-
trs << (high[i] - low[i]).abs
|
352
|
-
else
|
353
|
-
tr = [(high[i] - low[i]).abs, (high[i] - close[i - 1]).abs, (low[i] - close[i - 1]).abs].max
|
354
|
-
trs << tr
|
355
|
-
end
|
356
|
-
end
|
357
|
-
trs
|
358
|
-
end
|
359
|
-
|
360
|
-
def simple_atr(high, low, close, period)
|
361
|
-
trs = true_ranges(high, low, close)
|
362
|
-
out = []
|
363
|
-
atr_prev = trs.first(period).sum / period.to_f
|
364
|
-
trs.each_with_index do |tr, i|
|
365
|
-
if i < period
|
366
|
-
out << nil
|
367
|
-
elsif i == period
|
368
|
-
out << atr_prev
|
369
|
-
else
|
370
|
-
atr_prev = ((atr_prev * (period - 1)) + tr) / period.to_f
|
371
|
-
out << atr_prev
|
372
|
-
end
|
373
|
-
end
|
374
|
-
out
|
375
|
-
end
|
376
|
-
|
377
|
-
def simple_adx(high, low, close, period)
|
378
|
-
plus_dm = [0]
|
379
|
-
minus_dm = [0]
|
380
|
-
(1...high.size).each do |i|
|
381
|
-
up_move = high[i] - high[i - 1]
|
382
|
-
down_move = low[i - 1] - low[i]
|
383
|
-
plus_dm << (up_move > down_move && up_move.positive? ? up_move : 0)
|
384
|
-
minus_dm << (down_move > up_move && down_move.positive? ? down_move : 0)
|
385
|
-
end
|
386
|
-
trs = true_ranges(high, low, close)
|
387
|
-
smooth_tr = trs.first(period).sum
|
388
|
-
smooth_plus_dm = plus_dm.first(period).sum
|
389
|
-
smooth_minus_dm = minus_dm.first(period).sum
|
390
|
-
adx_vals = Array.new(high.size, nil)
|
391
|
-
di_vals = []
|
392
|
-
(period...high.size).each do |i|
|
393
|
-
smooth_tr = smooth_tr - (smooth_tr / period) + trs[i]
|
394
|
-
smooth_plus_dm = smooth_plus_dm - (smooth_plus_dm / period) + plus_dm[i]
|
395
|
-
smooth_minus_dm = smooth_minus_dm - (smooth_minus_dm / period) + minus_dm[i]
|
396
|
-
plus_di = 100.0 * (smooth_plus_dm / smooth_tr)
|
397
|
-
minus_di = 100.0 * (smooth_minus_dm / smooth_tr)
|
398
|
-
dx = 100.0 * ((plus_di - minus_di).abs / (plus_di + minus_di))
|
399
|
-
di_vals << dx
|
400
|
-
adx_vals[i] = di_vals.last(period).sum / period.to_f if di_vals.size >= period
|
401
|
-
end
|
402
|
-
adx_vals
|
403
|
-
end
|
404
194
|
end
|
405
195
|
end
|