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.
@@ -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
- unless defined?(MarketCalendar)
22
- module MarketCalendar
23
- MARKET_HOLIDAYS = [
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
- if to_date.nil? || to_date.to_s.strip.empty?
83
- to_date = MarketCalendar.today_or_last_trading_day.strftime("%Y-%m-%d")
84
- end
85
- if (from_date.nil? || from_date.to_s.strip.empty?) && days_back && days_back.to_i > 0
86
- to_d = Date.parse(to_date)
87
- n_back = [days_back.to_i - 1, 0].max
88
- from_date = MarketCalendar.trading_days_ago(to_d, n_back).strftime("%Y-%m-%d")
89
- end
90
- from_date ||= to_date
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
- case ivl.to_i
104
- when 1 then frames[:m1] = one_min_candles
105
- when 5 then frames[:m5] = resample(one_min_candles, 5)
106
- when 15 then frames[:m15] = resample(one_min_candles, 15)
107
- when 25 then frames[:m25] = resample(one_min_candles, 25)
108
- when 60 then frames[:m60] = resample(one_min_candles, 60)
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 = candles(raw)
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 fetch_intraday(params, interval)
143
- DhanHQ::Models::HistoricalData.intraday(
144
- security_id: params[:security_id],
145
- exchange_segment: params[:exchange_segment],
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 fetch_intraday_windowed(params, interval)
154
- from_d = Date.parse(params[:from_date])
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
- merged = { "open" => [], "high" => [], "low" => [], "close" => [], "volume" => [], "timestamp" => [] }
160
- cursor = from_d
161
- while cursor <= to_d
162
- chunk_to = [cursor + max_span, to_d].min
163
- chunk_params = params.merge(from_date: cursor.strftime("%Y-%m-%d"), to_date: chunk_to.strftime("%Y-%m-%d"))
164
- part = fetch_intraday(chunk_params, interval)
165
- %w[open high low close volume timestamp].each do |k|
166
- ary = (part[k] || part[k.to_sym]) || []
167
- merged[k].concat(Array(ary))
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 parse_time_like(val)
175
- return Time.at(val) if val.is_a?(Numeric)
176
-
177
- s = val.to_s
178
- return Time.at(s.to_i) if /\A\d+\z/.match?(s) && s.length >= 10 && s.length <= 13
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
- Time.parse(s)
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 candles(series)
184
- ts = series["timestamp"] || series[:timestamp]
185
- open = series["open"] || series[:open]
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...close.size).map do |i|
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 resample(candles, minutes)
202
- return candles if minutes == 1
203
-
204
- grouped = {}
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 closes(candles) = candles.map { |c| c[:c] }
217
- def highs(candles) = candles.map { |c| c[:h] }
218
- def lows(candles) = candles.map { |c| c[:l] }
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 = closes(candles)
222
- h = highs(candles)
223
- l = lows(candles)
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
data/lib/ta.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ta/market_calendar"
4
+ require_relative "ta/candles"
5
+ require_relative "ta/indicators"
6
+ require_relative "ta/fetcher"
7
+ require_relative "ta/technical_analysis"