indicator_hub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +551 -0
  6. data/Rakefile +12 -0
  7. data/exe/indicator_hub +4 -0
  8. data/lib/indicator_hub/calculation_helpers.rb +196 -0
  9. data/lib/indicator_hub/indicators/adi.rb +37 -0
  10. data/lib/indicator_hub/indicators/adtv.rb +27 -0
  11. data/lib/indicator_hub/indicators/adx.rb +102 -0
  12. data/lib/indicator_hub/indicators/ao.rb +39 -0
  13. data/lib/indicator_hub/indicators/atr.rb +45 -0
  14. data/lib/indicator_hub/indicators/base_indicator.rb +68 -0
  15. data/lib/indicator_hub/indicators/bb.rb +44 -0
  16. data/lib/indicator_hub/indicators/cci.rb +41 -0
  17. data/lib/indicator_hub/indicators/cmf.rb +54 -0
  18. data/lib/indicator_hub/indicators/cmo.rb +49 -0
  19. data/lib/indicator_hub/indicators/cr.rb +26 -0
  20. data/lib/indicator_hub/indicators/dc.rb +40 -0
  21. data/lib/indicator_hub/indicators/dlr.rb +27 -0
  22. data/lib/indicator_hub/indicators/dpo.rb +32 -0
  23. data/lib/indicator_hub/indicators/dr.rb +27 -0
  24. data/lib/indicator_hub/indicators/ema.rb +40 -0
  25. data/lib/indicator_hub/indicators/envelopes_ema.rb +36 -0
  26. data/lib/indicator_hub/indicators/eom.rb +48 -0
  27. data/lib/indicator_hub/indicators/fi.rb +45 -0
  28. data/lib/indicator_hub/indicators/ichimoku.rb +76 -0
  29. data/lib/indicator_hub/indicators/imi.rb +48 -0
  30. data/lib/indicator_hub/indicators/kc.rb +46 -0
  31. data/lib/indicator_hub/indicators/kst.rb +82 -0
  32. data/lib/indicator_hub/indicators/macd.rb +46 -0
  33. data/lib/indicator_hub/indicators/mfi.rb +62 -0
  34. data/lib/indicator_hub/indicators/mi.rb +81 -0
  35. data/lib/indicator_hub/indicators/nvi.rb +42 -0
  36. data/lib/indicator_hub/indicators/obv.rb +41 -0
  37. data/lib/indicator_hub/indicators/obv_mean.rb +42 -0
  38. data/lib/indicator_hub/indicators/pivot_points.rb +44 -0
  39. data/lib/indicator_hub/indicators/price_channel.rb +38 -0
  40. data/lib/indicator_hub/indicators/qstick.rb +36 -0
  41. data/lib/indicator_hub/indicators/rmi.rb +48 -0
  42. data/lib/indicator_hub/indicators/roc.rb +37 -0
  43. data/lib/indicator_hub/indicators/rsi.rb +67 -0
  44. data/lib/indicator_hub/indicators/sma.rb +32 -0
  45. data/lib/indicator_hub/indicators/so.rb +76 -0
  46. data/lib/indicator_hub/indicators/trix.rb +53 -0
  47. data/lib/indicator_hub/indicators/tsi.rb +67 -0
  48. data/lib/indicator_hub/indicators/uo.rb +67 -0
  49. data/lib/indicator_hub/indicators/vi.rb +54 -0
  50. data/lib/indicator_hub/indicators/volume_oscillator.rb +55 -0
  51. data/lib/indicator_hub/indicators/vpt.rb +35 -0
  52. data/lib/indicator_hub/indicators/vwap.rb +33 -0
  53. data/lib/indicator_hub/indicators/wilders_smoothing.rb +36 -0
  54. data/lib/indicator_hub/indicators/wma.rb +36 -0
  55. data/lib/indicator_hub/indicators/wr.rb +36 -0
  56. data/lib/indicator_hub/series.rb +79 -0
  57. data/lib/indicator_hub/talib_adapter.rb +23 -0
  58. data/lib/indicator_hub/validation.rb +49 -0
  59. data/lib/indicator_hub/version.rb +6 -0
  60. data/lib/indicator_hub.rb +485 -0
  61. data/sig/indicator_hub.rbs +4 -0
  62. metadata +112 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 655be8ee3ffcbc7fdcdb6f4b66c56c78bde2e414de74d146550b67666f92f4c5
4
+ data.tar.gz: 3be616665ae69e54d12c0a3daf3349d980a42999ba0d9560c385720f9aec47cc
5
+ SHA512:
6
+ metadata.gz: e260e9516c1f002435fea29bed6611f53648aea12b98b863bac06923c9976c86f351441508f9908cd48c7f392f42fb7b5cf5c5c98972d1849b688a27b34b006f
7
+ data.tar.gz: 12cf1b88a3852d334b0f4b47f925ee6bbb4215e2b488cdc20c2ccdf41d3a78e1e43678c25ecbb248e183b6877e1847789375dc5473cc54a5f3fd4df63aae2bac
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-03-29
4
+
5
+ - Initial release
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "indicator_hub" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["shubhamtaywade82@gmail.com"](mailto:"shubhamtaywade82@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Shubham Taywade
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,551 @@
1
+ # IndicatorHub
2
+
3
+ IndicatorHub is a unified, clean, and idiomatic Ruby gem for technical analysis. It aggregates and optimizes the core math from multiple popular technical analysis gems into a single, high-performance, pure Ruby library.
4
+
5
+ ## Key Features
6
+
7
+ - **Unified API**: Calculate SMA, EMA, RSI, MACD, and Bollinger Bands through a single entry point.
8
+ - **Data Agnostic**: Supports simple price arrays or complex OHLCV hash data.
9
+ - **Pure Ruby**: Zero dependencies by default (math implementations are self-contained).
10
+ - **Optional Performance**: Can optionally leverage `talib_ffi` for high-performance calculations if the C-library is available on the system.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'indicator_hub'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ ```bash
23
+ $ bundle install
24
+ ```
25
+
26
+ Or install it yourself:
27
+
28
+ ```bash
29
+ $ gem install indicator_hub
30
+ ```
31
+
32
+ ## Usage Guide
33
+
34
+ IndicatorHub works in two common modes:
35
+
36
+ - Standalone Ruby scripts, services, and CLIs
37
+ - Rails applications using ActiveRecord models or service objects
38
+
39
+ The API accepts either:
40
+
41
+ - A simple numeric series like `[100.5, 101.2, 99.8]`
42
+ - An OHLCV series like `[{ timestamp:, open:, high:, low:, close:, volume: }]`
43
+
44
+ Most moving averages and momentum indicators work with a numeric series. Indicators that depend on high, low, or volume expect OHLCV hashes.
45
+
46
+ ## Using In Standalone Ruby Apps
47
+
48
+ Install the gem:
49
+
50
+ ```bash
51
+ gem install indicator_hub
52
+ ```
53
+
54
+ Then require and use it:
55
+
56
+ ```ruby
57
+ require "indicator_hub"
58
+
59
+ closes = [100.0, 101.5, 102.2, 101.8, 103.4, 104.1]
60
+
61
+ sma = IndicatorHub.sma(closes, period: 3)
62
+ ema = IndicatorHub.ema(closes, period: 3)
63
+ rsi = IndicatorHub.rsi(closes, period: 5)
64
+
65
+ puts sma.inspect
66
+ puts ema.inspect
67
+ puts rsi.inspect
68
+ ```
69
+
70
+ Example with OHLCV candles:
71
+
72
+ ```ruby
73
+ require "indicator_hub"
74
+
75
+ candles = [
76
+ { timestamp: 1704067200, open: 100.0, high: 103.0, low: 99.0, close: 102.0, volume: 1200.0 },
77
+ { timestamp: 1704153600, open: 102.0, high: 104.0, low: 101.0, close: 103.0, volume: 1500.0 },
78
+ { timestamp: 1704240000, open: 103.0, high: 105.0, low: 100.0, close: 101.0, volume: 1700.0 }
79
+ ]
80
+
81
+ atr = IndicatorHub.atr(candles, period: 2)
82
+ adx = IndicatorHub.adx(candles, period: 2)
83
+ vwap = IndicatorHub.vwap(candles)
84
+
85
+ puts atr.inspect
86
+ puts adx.inspect
87
+ puts vwap.inspect
88
+ ```
89
+
90
+ ## Using In Rails Apps
91
+
92
+ Add the gem to your `Gemfile`:
93
+
94
+ ```ruby
95
+ gem "indicator_hub"
96
+ ```
97
+
98
+ Then run:
99
+
100
+ ```bash
101
+ bundle install
102
+ ```
103
+
104
+ In Rails, you usually call IndicatorHub from:
105
+
106
+ - a model method
107
+ - a query/service object
108
+ - a background job
109
+ - a controller or API serializer
110
+
111
+ Example `PriceBar` model usage:
112
+
113
+ ```ruby
114
+ # app/models/price_bar.rb
115
+ class PriceBar < ApplicationRecord
116
+ scope :chronological, -> { order(:traded_at) }
117
+
118
+ def self.to_indicator_series(limit: 200)
119
+ chronological.limit(limit).map do |bar|
120
+ {
121
+ timestamp: bar.traded_at.to_i,
122
+ open: bar.open,
123
+ high: bar.high,
124
+ low: bar.low,
125
+ close: bar.close,
126
+ volume: bar.volume
127
+ }
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+ Example service object:
134
+
135
+ ```ruby
136
+ # app/services/indicator_snapshot.rb
137
+ class IndicatorSnapshot
138
+ def initialize(scope = PriceBar.all)
139
+ @scope = scope
140
+ end
141
+
142
+ def call(limit: 200)
143
+ candles = @scope.order(:traded_at).limit(limit).map do |bar|
144
+ {
145
+ open: bar.open,
146
+ high: bar.high,
147
+ low: bar.low,
148
+ close: bar.close,
149
+ volume: bar.volume
150
+ }
151
+ end
152
+
153
+ {
154
+ sma_20: IndicatorHub.sma(candles, period: 20, field: :close).last,
155
+ ema_20: IndicatorHub.ema(candles, period: 20, field: :close).last,
156
+ rsi_14: IndicatorHub.rsi(candles, period: 14, field: :close).last,
157
+ macd: IndicatorHub.macd(candles).last,
158
+ atr_14: IndicatorHub.atr(candles, period: 14).last
159
+ }
160
+ end
161
+ end
162
+ ```
163
+
164
+ Example from Rails console:
165
+
166
+ ```ruby
167
+ bars = PriceBar.order(:traded_at).last(100).map do |bar|
168
+ {
169
+ open: bar.open,
170
+ high: bar.high,
171
+ low: bar.low,
172
+ close: bar.close,
173
+ volume: bar.volume
174
+ }
175
+ end
176
+
177
+ IndicatorHub.bb(bars, period: 20, field: :close).last
178
+ IndicatorHub.macd(bars, field: :close).last
179
+ IndicatorHub.vwap(bars).last
180
+ ```
181
+
182
+ ## Data Formats
183
+
184
+ ### Numeric Series
185
+
186
+ Use this for indicators based on one field, usually close:
187
+
188
+ ```ruby
189
+ closes = [100.0, 101.2, 102.8, 103.1]
190
+
191
+ IndicatorHub.sma(closes, period: 3)
192
+ IndicatorHub.ema(closes, period: 3)
193
+ IndicatorHub.rsi(closes, period: 14)
194
+ IndicatorHub.macd(closes)
195
+ ```
196
+
197
+ ### Hash Series With `field:`
198
+
199
+ If you already have hashes and want a single-field indicator:
200
+
201
+ ```ruby
202
+ rows = [
203
+ { open: 100.0, close: 101.0 },
204
+ { open: 101.0, close: 103.0 },
205
+ { open: 103.0, close: 102.0 }
206
+ ]
207
+
208
+ IndicatorHub.sma(rows, period: 2, field: :close)
209
+ IndicatorHub.ema(rows, period: 2, field: :open)
210
+ ```
211
+
212
+ ### OHLCV Series
213
+
214
+ Use this for indicators that need candle ranges or volume:
215
+
216
+ ```ruby
217
+ rows = [
218
+ { timestamp: 1704067200, open: 100.0, high: 103.0, low: 99.0, close: 101.0, volume: 1000.0 },
219
+ { timestamp: 1704153600, open: 101.0, high: 104.0, low: 100.0, close: 103.0, volume: 1200.0 }
220
+ ]
221
+
222
+ IndicatorHub.atr(rows, period: 14)
223
+ IndicatorHub.adx(rows, period: 14)
224
+ IndicatorHub.mfi(rows, period: 14)
225
+ IndicatorHub.vwap(rows)
226
+ ```
227
+
228
+ `timestamp` is recommended for real market data feeds. The indicator methods only use OHLCV values for calculations, so extra keys like `timestamp`, `symbol`, or `open_interest` are safe to keep in your source payloads.
229
+
230
+ ## Provider Payload Examples
231
+
232
+ ### Delta Exchange Response
233
+
234
+ The `delta_exchange` gem returns the parsed API response envelope from `/v2/history/candles`. In practice, you extract `result` and normalize `time` to `timestamp` if you want a consistent candle shape in your app:
235
+
236
+ ```ruby
237
+ payload = {
238
+ "success" => true,
239
+ "result" => [
240
+ {
241
+ "time" => 1704067200,
242
+ "open" => 100.0,
243
+ "high" => 103.0,
244
+ "low" => 99.0,
245
+ "close" => 101.0,
246
+ "volume" => 1200.0
247
+ }
248
+ ]
249
+ }
250
+
251
+ candles = payload["result"].map do |row|
252
+ {
253
+ timestamp: row["time"],
254
+ open: row["open"],
255
+ high: row["high"],
256
+ low: row["low"],
257
+ close: row["close"],
258
+ volume: row["volume"]
259
+ }
260
+ end
261
+
262
+ IndicatorHub.atr(candles, period: 14)
263
+ IndicatorHub.rsi(candles, period: 14, field: :close)
264
+ ```
265
+
266
+ ### DhanHQ Client Response
267
+
268
+ The `dhanhq-client` gem already normalizes historical responses into an array of candle hashes in `DhanHQ::Models::HistoricalData.daily` and `DhanHQ::Models::HistoricalData.intraday`.
269
+
270
+ ```ruby
271
+ candles = DhanHQ::Models::HistoricalData.intraday(
272
+ security_id: "13",
273
+ exchange_segment: DhanHQ::Constants::ExchangeSegment::IDX_I,
274
+ instrument: DhanHQ::Constants::InstrumentType::INDEX,
275
+ interval: "5",
276
+ from_date: "2024-08-14",
277
+ to_date: "2024-08-14"
278
+ )
279
+
280
+ # candles.first
281
+ # => {
282
+ # timestamp: 2024-08-14 09:15:00 +0530,
283
+ # open: 3750.0,
284
+ # high: 3757.9,
285
+ # low: 3746.1,
286
+ # close: 3751.25,
287
+ # volume: 53629
288
+ # }
289
+
290
+ IndicatorHub.macd(candles, field: :close)
291
+ IndicatorHub.vwap(candles)
292
+ IndicatorHub.obv(candles)
293
+ ```
294
+
295
+ If you are working with Dhan's raw API payload before `dhanhq-client` normalizes it, then yes, you would first zip the parallel arrays into candle hashes.
296
+
297
+ ### Normalizing In One Helper
298
+
299
+ For app code, it is usually better to normalize provider responses in one place:
300
+
301
+ ```ruby
302
+ module CandleNormalizer
303
+ module_function
304
+
305
+ def from_delta(payload)
306
+ payload.fetch("result", []).map do |row|
307
+ {
308
+ timestamp: row["time"],
309
+ open: row["open"],
310
+ high: row["high"],
311
+ low: row["low"],
312
+ close: row["close"],
313
+ volume: row["volume"]
314
+ }
315
+ end
316
+ end
317
+
318
+ def from_dhanhq(payload)
319
+ payload.fetch("close", []).each_index.map do |i|
320
+ {
321
+ timestamp: payload["timestamp"][i],
322
+ open: payload["open"][i],
323
+ high: payload["high"][i],
324
+ low: payload["low"][i],
325
+ close: payload["close"][i],
326
+ volume: payload["volume"][i]
327
+ }
328
+ end
329
+ end
330
+ end
331
+ ```
332
+
333
+ ## Warm-Up Values And `nil` Results
334
+
335
+ Most indicators need a minimum lookback window before they can return a value. Because of that, the first values are often `nil`.
336
+
337
+ Example:
338
+
339
+ ```ruby
340
+ IndicatorHub.sma([1, 2, 3, 4, 5], period: 3)
341
+ # => [nil, nil, 2.0, 3.0, 4.0]
342
+ ```
343
+
344
+ In Rails or reporting code, it is common to use the latest non-`nil` value:
345
+
346
+ ```ruby
347
+ latest_rsi = IndicatorHub.rsi(closes, period: 14).compact.last
348
+ ```
349
+
350
+ ## Common Patterns
351
+
352
+ Latest value only:
353
+
354
+ ```ruby
355
+ latest_ema = IndicatorHub.ema(closes, period: 20).compact.last
356
+ ```
357
+
358
+ Latest MACD snapshot:
359
+
360
+ ```ruby
361
+ latest_macd = IndicatorHub.macd(closes).last
362
+ # => { macd:, signal:, histogram: }
363
+ ```
364
+
365
+ Multiple indicators from the same candle set:
366
+
367
+ ```ruby
368
+ indicators = {
369
+ sma_20: IndicatorHub.sma(candles, period: 20, field: :close).last,
370
+ rsi_14: IndicatorHub.rsi(candles, period: 14, field: :close).last,
371
+ atr_14: IndicatorHub.atr(candles, period: 14).last,
372
+ obv: IndicatorHub.obv(candles).last
373
+ }
374
+ ```
375
+
376
+ ## Tips For Production Use
377
+
378
+ - Always sort your data chronologically before calculating indicators.
379
+ - Pass enough history for the indicator lookback plus warm-up.
380
+ - Use `field: :close` explicitly when your source objects contain multiple price fields.
381
+ - For API responses, prefer returning the latest computed point instead of the full series unless you need charting data.
382
+ - VWAP in this gem is cumulative over the provided dataset, so if you need session-based VWAP, pass one session at a time.
383
+
384
+ ## API Reference By Indicator Category
385
+
386
+ ### Single-Series Indicators
387
+
388
+ These work with:
389
+
390
+ - numeric arrays like `[100.0, 101.2, 102.4]`
391
+ - hash arrays with `field:` like `[{ close: 100.0 }, { close: 101.2 }]`
392
+
393
+ Methods:
394
+
395
+ - `IndicatorHub.sma(data, period: 20, field: :close)`
396
+ - `IndicatorHub.ema(data, period: 20, field: :close)`
397
+ - `IndicatorHub.wma(data, period: 20, field: :close)`
398
+ - `IndicatorHub.rsi(data, period: 14, field: :close)`
399
+ - `IndicatorHub.cmo(data, period: 14, field: :close)`
400
+ - `IndicatorHub.dlr(data, field: :close)`
401
+ - `IndicatorHub.dpo(data, period: 20, field: :close)`
402
+ - `IndicatorHub.dr(data, field: :close)`
403
+ - `IndicatorHub.roc(data, period: 12, field: :close)`
404
+ - `IndicatorHub.trix(data, period: 15, field: :close)`
405
+ - `IndicatorHub.tsi(data, fast_period: 13, slow_period: 25, field: :close)`
406
+ - `IndicatorHub.wilders_smoothing(data, period: 14, field: :close)`
407
+ - `IndicatorHub.cr(data, period: 20, field: :close)`
408
+ - `IndicatorHub.envelopes_ema(data, period: 20, percentage: 2.5, field: :close)`
409
+ - `IndicatorHub.macd(data, fast_period: 12, slow_period: 26, signal_period: 9, field: :close)`
410
+ - `IndicatorHub.rmi(data, period: 14, momentum_period: 5)`
411
+
412
+ ### OHLCV Indicators
413
+
414
+ These expect candle hashes with `open`, `high`, `low`, `close`, and optionally `volume` where required:
415
+
416
+ ```ruby
417
+ [
418
+ { open: 100.0, high: 103.0, low: 99.0, close: 101.0, volume: 1200.0 }
419
+ ]
420
+ ```
421
+
422
+ Methods:
423
+
424
+ - `IndicatorHub.adi(data)`
425
+ - `IndicatorHub.adtv(data, period: 20)`
426
+ - `IndicatorHub.adx(data, period: 14)`
427
+ - `IndicatorHub.ao(data, short_period: 5, long_period: 34)`
428
+ - `IndicatorHub.atr(data, period: 14)`
429
+ - `IndicatorHub.cci(data, period: 20)`
430
+ - `IndicatorHub.cmf(data, period: 20)`
431
+ - `IndicatorHub.dc(data, period: 20)`
432
+ - `IndicatorHub.eom(data, period: 14)`
433
+ - `IndicatorHub.fi(data, period: 13)`
434
+ - `IndicatorHub.ichimoku(data, low_period: 9, medium_period: 26, high_period: 52)`
435
+ - `IndicatorHub.imi(data, period: 14)`
436
+ - `IndicatorHub.kc(data, period: 20, multiplier: 1.5)`
437
+ - `IndicatorHub.mfi(data, period: 14)`
438
+ - `IndicatorHub.mi(data, period: 25)`
439
+ - `IndicatorHub.nvi(data)`
440
+ - `IndicatorHub.obv(data)`
441
+ - `IndicatorHub.obv_mean(data, period: 10)`
442
+ - `IndicatorHub.pivot_points(data)`
443
+ - `IndicatorHub.price_channel(data, period: 20)`
444
+ - `IndicatorHub.qstick(data, period: 10)`
445
+ - `IndicatorHub.so(data, k_period: 14, k_slowing: 3, d_period: 3)`
446
+ - `IndicatorHub.uo(data, short_period: 7, medium_period: 14, long_period: 28)`
447
+ - `IndicatorHub.vi(data, period: 14)`
448
+ - `IndicatorHub.volume_oscillator(data, short_period: 20, long_period: 60)`
449
+ - `IndicatorHub.vpt(data)`
450
+ - `IndicatorHub.vwap(data)`
451
+ - `IndicatorHub.wr(data, period: 14)`
452
+
453
+ ### Indicators Returning Hashes
454
+
455
+ These return structured values instead of a plain numeric series:
456
+
457
+ - `IndicatorHub.bb` returns `{ upper:, middle:, lower: }`
458
+ - `IndicatorHub.macd` returns `{ macd:, signal:, histogram: }`
459
+ - `IndicatorHub.dc` returns `{ upper:, middle:, lower: }`
460
+ - `IndicatorHub.envelopes_ema` returns `{ upper:, middle:, lower: }`
461
+ - `IndicatorHub.ichimoku` returns `{ tenkan_sen:, kijun_sen:, senkou_span_a:, senkou_span_b:, chikou_span: }`
462
+ - `IndicatorHub.kc` returns `{ upper:, middle:, lower: }`
463
+ - `IndicatorHub.kst` returns `{ kst:, signal: }`
464
+ - `IndicatorHub.pivot_points` returns `{ p:, s1:, s2:, s3:, r1:, r2:, r3: }`
465
+ - `IndicatorHub.price_channel` returns `{ upper:, lower: }`
466
+ - `IndicatorHub.so` returns `{ k:, d: }`
467
+ - `IndicatorHub.vi` returns `{ plus_vi:, minus_vi: }`
468
+
469
+ ## Basic Examples
470
+
471
+ ```ruby
472
+ require 'indicator_hub'
473
+
474
+ data = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0]
475
+
476
+ # Simple Moving Average
477
+ sma = IndicatorHub.sma(data, period: 5)
478
+ # => [nil, nil, nil, nil, 12.0, 13.0]
479
+
480
+ # Relative Strength Index
481
+ rsi = IndicatorHub.rsi(data, period: 5)
482
+ ```
483
+
484
+ OHLCV data:
485
+
486
+ ```ruby
487
+ data = [
488
+ { timestamp: 1704067200, open: 10, high: 12, low: 9, close: 11, volume: 1000 },
489
+ { timestamp: 1704153600, open: 11, high: 13, low: 10, close: 12, volume: 1200 },
490
+ # ...
491
+ ]
492
+
493
+ # Calculate SMA on the 'close' field
494
+ sma = IndicatorHub.sma(data, period: 2, field: :close)
495
+
496
+ # Calculate ATR from OHLCV candles
497
+ atr = IndicatorHub.atr(data, period: 14)
498
+ ```
499
+
500
+ ### Supported Indicators
501
+
502
+ - `IndicatorHub.sma(data, period: 20)`
503
+ - `IndicatorHub.ema(data, period: 20)`
504
+ - `IndicatorHub.rsi(data, period: 14)`
505
+ - `IndicatorHub.macd(data, fast_period: 12, slow_period: 26, signal_period: 9)`
506
+ - `IndicatorHub.bb(data, period: 20, standard_deviations: 2)`
507
+
508
+ ## Contributing
509
+
510
+ Bug reports and pull requests are welcome on GitHub at https://github.com/shubhamtaywade/indicator_hub.
511
+
512
+ ## Release Process
513
+
514
+ CI:
515
+
516
+ - GitHub Actions runs on `main`, `master`, and pull requests
517
+ - The CI workflow tests Ruby `3.2.0` and `3.3.4`
518
+ - Each CI run executes `bundle exec rake` and verifies the gem builds successfully
519
+
520
+ CD:
521
+
522
+ - Releases are triggered by pushing a tag like `v0.1.0`
523
+ - The release workflow validates that the tag matches `IndicatorHub::VERSION`
524
+ - It runs the full test/lint suite, builds the gem, and publishes to RubyGems
525
+
526
+ Required GitHub Actions secrets:
527
+
528
+ - `RUBYGEMS_API_KEY`
529
+ - `RUBYGEMS_OTP_SECRET`
530
+
531
+ Typical release steps:
532
+
533
+ ```bash
534
+ # 1. Update version
535
+ # lib/indicator_hub/version.rb
536
+
537
+ # 2. Update changelog
538
+ # CHANGELOG.md
539
+
540
+ # 3. Commit changes
541
+ git add .
542
+ git commit -m "Release v0.1.0"
543
+
544
+ # 4. Create and push tag
545
+ git tag v0.1.0
546
+ git push origin main --tags
547
+ ```
548
+
549
+ ## License
550
+
551
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/exe/indicator_hub ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "indicator_hub"