DhanHQ 2.1.3 → 2.1.6

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +185 -0
  4. data/CHANGELOG.md +31 -0
  5. data/GUIDE.md +173 -31
  6. data/README.md +437 -133
  7. data/README1.md +267 -26
  8. data/docs/live_order_updates.md +319 -0
  9. data/docs/rails_integration.md +1 -1
  10. data/docs/rails_websocket_integration.md +847 -0
  11. data/docs/standalone_ruby_websocket_integration.md +1588 -0
  12. data/docs/technical_analysis.md +1 -0
  13. data/docs/websocket_integration.md +871 -0
  14. data/examples/comprehensive_websocket_examples.rb +148 -0
  15. data/examples/instrument_finder_test.rb +195 -0
  16. data/examples/live_order_updates.rb +118 -0
  17. data/examples/market_depth_example.rb +144 -0
  18. data/examples/market_feed_example.rb +81 -0
  19. data/examples/order_update_example.rb +105 -0
  20. data/examples/trading_fields_example.rb +215 -0
  21. data/lib/DhanHQ/config.rb +1 -0
  22. data/lib/DhanHQ/configuration.rb +16 -1
  23. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
  24. data/lib/DhanHQ/contracts/modify_order_contract.rb +1 -0
  25. data/lib/DhanHQ/contracts/option_chain_contract.rb +11 -1
  26. data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
  27. data/lib/DhanHQ/errors.rb +2 -0
  28. data/lib/DhanHQ/models/expired_options_data.rb +331 -0
  29. data/lib/DhanHQ/models/instrument.rb +96 -2
  30. data/lib/DhanHQ/models/option_chain.rb +2 -0
  31. data/lib/DhanHQ/models/order_update.rb +235 -0
  32. data/lib/DhanHQ/models/trade.rb +118 -31
  33. data/lib/DhanHQ/rate_limiter.rb +4 -2
  34. data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
  35. data/lib/DhanHQ/version.rb +1 -1
  36. data/lib/DhanHQ/ws/base_connection.rb +249 -0
  37. data/lib/DhanHQ/ws/client.rb +1 -1
  38. data/lib/DhanHQ/ws/connection.rb +3 -3
  39. data/lib/DhanHQ/ws/decoder.rb +3 -3
  40. data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
  41. data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
  42. data/lib/DhanHQ/ws/market_depth.rb +74 -0
  43. data/lib/DhanHQ/ws/orders/client.rb +177 -10
  44. data/lib/DhanHQ/ws/orders/connection.rb +41 -83
  45. data/lib/DhanHQ/ws/orders.rb +31 -2
  46. data/lib/DhanHQ/ws/registry.rb +1 -0
  47. data/lib/DhanHQ/ws/segments.rb +21 -5
  48. data/lib/DhanHQ/ws/sub_state.rb +1 -1
  49. data/lib/DhanHQ/ws.rb +3 -2
  50. data/lib/{DhanHQ.rb → dhan_hq.rb} +5 -0
  51. data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +18 -18
  52. data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +1 -0
  53. data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +2 -0
  54. data/lib/dhanhq/analysis/options_buying_advisor.rb +4 -3
  55. data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +1 -0
  56. data/lib/ta/candles.rb +1 -0
  57. data/lib/ta/fetcher.rb +1 -0
  58. data/lib/ta/indicators.rb +2 -1
  59. data/lib/ta/market_calendar.rb +4 -3
  60. data/lib/ta/technical_analysis.rb +3 -2
  61. metadata +38 -4
  62. data/lib/DhanHQ/ws/errors.rb +0 -0
  63. /data/lib/DhanHQ/contracts/{modify_order_contract copy.rb → modify_order_contract_copy.rb} +0 -0
@@ -6,45 +6,209 @@ require_relative "connection"
6
6
  module DhanHQ
7
7
  module WS
8
8
  module Orders
9
+ ##
10
+ # Enhanced WebSocket client for real-time order updates
11
+ # Provides comprehensive order state tracking and event handling
12
+ # rubocop:disable Metrics/ClassLength
9
13
  class Client
10
- def initialize(url: nil)
14
+ def initialize(url: nil, **options)
11
15
  @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
12
- @started = Concurrent::AtomicBoolean.new(false)
13
- cfg = DhanHQ.configuration
14
- @url = url || cfg.ws_order_url
16
+ @started = Concurrent::AtomicBoolean.new(false)
17
+ @order_tracker = Concurrent::Map.new
18
+ cfg = DhanHQ.configuration
19
+ @url = url || cfg.ws_order_url
20
+ @connection_options = options
15
21
  end
16
22
 
23
+ ##
24
+ # Start the WebSocket connection and begin receiving order updates
25
+ # @return [Client] self for method chaining
17
26
  def start
18
27
  return self if @started.true?
28
+
19
29
  @started.make_true
20
- @conn = Connection.new(url: @url) do |msg|
21
- emit(:update, msg) if msg&.dig(:Type) == "order_alert"
22
- emit(:raw, msg)
23
- end
30
+ @conn = Connection.new(url: @url, **@connection_options)
31
+ @conn.on(:open) { emit(:open, true) }
32
+ @conn.on(:close) { |payload| emit(:close, payload) }
33
+ @conn.on(:error) { |error| emit(:error, error) }
34
+ @conn.on(:message) { |msg| handle_message(msg) }
24
35
  @conn.start
25
36
  DhanHQ::WS::Registry.register(self) if defined?(DhanHQ::WS::Registry)
26
37
  self
27
38
  end
28
39
 
40
+ ##
41
+ # Stop the WebSocket connection
42
+ # @return [void]
29
43
  def stop
30
44
  return unless @started.true?
45
+
31
46
  @started.make_false
32
47
  @conn&.stop
33
48
  emit(:close, true)
34
49
  DhanHQ::WS::Registry.unregister(self) if defined?(DhanHQ::WS::Registry)
35
50
  end
36
51
 
52
+ ##
53
+ # Force disconnect the WebSocket
54
+ # @return [void]
37
55
  def disconnect!
38
56
  @conn&.disconnect!
39
57
  end
40
58
 
41
- def on(event, &blk)
42
- @callbacks[event] << blk
59
+ ##
60
+ # Register event handlers
61
+ # @param event [Symbol] Event type (:update, :raw, :status_change, :execution, :error)
62
+ # @param block [Proc] Event handler
63
+ # @return [Client] self for method chaining
64
+ def on(event, &block)
65
+ @callbacks[event] << block
43
66
  self
44
67
  end
45
68
 
69
+ ##
70
+ # Get current order state for a specific order
71
+ # @param order_no [String] Order number
72
+ # @return [OrderUpdate, nil] Latest order update or nil if not found
73
+ def order_state(order_no)
74
+ @order_tracker[order_no]
75
+ end
76
+
77
+ ##
78
+ # Get all tracked orders
79
+ # @return [Hash] Hash of order_no => OrderUpdate
80
+ def all_orders
81
+ @order_tracker.dup
82
+ end
83
+
84
+ ##
85
+ # Get orders by status
86
+ # @param status [String] Order status (TRANSIT, PENDING, REJECTED, etc.)
87
+ # @return [Array<OrderUpdate>] Orders with the specified status
88
+ def orders_by_status(status)
89
+ @order_tracker.values.select { |order| order.status == status }
90
+ end
91
+
92
+ ##
93
+ # Get orders by symbol
94
+ # @param symbol [String] Trading symbol
95
+ # @return [Array<OrderUpdate>] Orders for the specified symbol
96
+ def orders_by_symbol(symbol)
97
+ @order_tracker.values.select { |order| order.symbol == symbol }
98
+ end
99
+
100
+ ##
101
+ # Get partially executed orders
102
+ # @return [Array<OrderUpdate>] Orders that are partially executed
103
+ def partially_executed_orders
104
+ @order_tracker.values.select(&:partially_executed?)
105
+ end
106
+
107
+ ##
108
+ # Get fully executed orders
109
+ # @return [Array<OrderUpdate>] Orders that are fully executed
110
+ def fully_executed_orders
111
+ @order_tracker.values.select(&:fully_executed?)
112
+ end
113
+
114
+ ##
115
+ # Get pending orders (not executed)
116
+ # @return [Array<OrderUpdate>] Orders that are not executed
117
+ def pending_orders
118
+ @order_tracker.values.select(&:not_executed?)
119
+ end
120
+
121
+ ##
122
+ # Check if connection is active
123
+ # @return [Boolean] true if connected
124
+ def connected?
125
+ @conn&.open? || false
126
+ end
127
+
46
128
  private
47
129
 
130
+ ##
131
+ # Handle incoming WebSocket messages
132
+ # @param msg [Hash] Raw WebSocket message
133
+ def handle_message(msg)
134
+ # Emit raw message for debugging
135
+ emit(:raw, msg)
136
+
137
+ # Handle order updates
138
+ if msg&.dig(:Type) == "order_alert"
139
+ order_update = DhanHQ::Models::OrderUpdate.from_websocket_message(msg)
140
+ handle_order_update(order_update) if order_update
141
+ end
142
+
143
+ # Handle other message types
144
+ emit(:message, msg)
145
+ end
146
+
147
+ ##
148
+ # Handle order update and track state changes
149
+ # @param order_update [OrderUpdate] Parsed order update
150
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
151
+ def handle_order_update(order_update)
152
+ order_no = order_update.order_no
153
+ previous_state = @order_tracker[order_no]
154
+
155
+ # Update order tracker
156
+ @order_tracker[order_no] = order_update
157
+
158
+ # Emit update event
159
+ emit(:update, order_update)
160
+
161
+ # Check for status changes
162
+ if previous_state && previous_state.status != order_update.status
163
+ emit(:status_change, {
164
+ order_update: order_update,
165
+ previous_status: previous_state.status,
166
+ new_status: order_update.status
167
+ })
168
+ end
169
+
170
+ # Check for execution updates
171
+ if previous_state && previous_state.traded_qty != order_update.traded_qty
172
+ emit(:execution, {
173
+ order_update: order_update,
174
+ previous_traded_qty: previous_state.traded_qty,
175
+ new_traded_qty: order_update.traded_qty,
176
+ execution_percentage: order_update.execution_percentage
177
+ })
178
+ end
179
+
180
+ # Emit specific status events
181
+ emit_status_specific_events(order_update, previous_state)
182
+ end
183
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
184
+
185
+ ##
186
+ # Emit status-specific events
187
+ # @param order_update [OrderUpdate] Current order update
188
+ # @param previous_state [OrderUpdate, nil] Previous order state
189
+ # rubocop:disable Metrics/MethodLength
190
+ def emit_status_specific_events(order_update, _previous_state)
191
+ case order_update.status
192
+ when "TRANSIT"
193
+ emit(:order_transit, order_update)
194
+ when "PENDING"
195
+ emit(:order_pending, order_update)
196
+ when "REJECTED"
197
+ emit(:order_rejected, order_update)
198
+ when "CANCELLED"
199
+ emit(:order_cancelled, order_update)
200
+ when "TRADED"
201
+ emit(:order_traded, order_update)
202
+ when "EXPIRED"
203
+ emit(:order_expired, order_update)
204
+ end
205
+ end
206
+ # rubocop:enable Metrics/MethodLength
207
+
208
+ ##
209
+ # Emit events to registered callbacks
210
+ # @param event [Symbol] Event type
211
+ # @param payload [Object] Event payload
48
212
  def emit(event, payload)
49
213
  list = begin
50
214
  @callbacks[event]
@@ -52,8 +216,11 @@ module DhanHQ
52
216
  []
53
217
  end
54
218
  list.each { |cb| cb.call(payload) }
219
+ rescue StandardError => e
220
+ DhanHQ.logger&.error("[DhanHQ::WS::Orders] Error in event handler: #{e.class} #{e.message}")
55
221
  end
56
222
  end
223
+ # rubocop:enable Metrics/ClassLength
57
224
  end
58
225
  end
59
226
  end
@@ -1,108 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "eventmachine"
4
- require "faye/websocket"
5
- require "json"
6
- require "thread" # rubocop:disable Lint/RedundantRequireStatement
3
+ require_relative "../base_connection"
7
4
 
8
5
  module DhanHQ
9
6
  module WS
10
7
  module Orders
11
- class Connection
12
- COOL_OFF_429 = 60
13
- MAX_BACKOFF = 90
14
-
15
- def initialize(url:, &on_json)
16
- @url = url
17
- @on_json = on_json
18
- @ws = nil
19
- @stop = false
20
- @cooloff_until = nil
21
- end
22
-
23
- def start
24
- Thread.new { loop_run }
25
- self
26
- end
27
-
28
- def stop
29
- @stop = true
30
- @ws&.close
31
- end
32
-
33
- def disconnect!
34
- # spec does not list a separate disconnect message; just close
35
- @ws&.close
8
+ ##
9
+ # WebSocket connection for real-time order updates
10
+ # Inherits from BaseConnection for consistent behavior
11
+ class Connection < BaseConnection
12
+ ##
13
+ # Initialize Orders WebSocket connection
14
+ # @param url [String] WebSocket endpoint URL
15
+ # @param options [Hash] Connection options
16
+ def initialize(url:, **options)
17
+ super
36
18
  end
37
19
 
38
20
  private
39
21
 
40
- def loop_run
41
- backoff = 2.0
42
- until @stop
43
- failed = false
44
- got_429 = false
45
- sleep (@cooloff_until - Time.now).ceil if @cooloff_until && Time.now < @cooloff_until
46
-
47
- begin
48
- failed, got_429 = run_session
49
- rescue StandardError => e
50
- DhanHQ.logger&.error("[DhanHQ::WS::Orders] crashed #{e.class} #{e.message}")
51
- failed = true
52
- ensure
53
- break if @stop
54
-
55
- if got_429
56
- @cooloff_until = Time.now + COOL_OFF_429
57
- DhanHQ.logger&.warn("[DhanHQ::WS::Orders] cooling off #{COOL_OFF_429}s due to 429")
58
- end
59
-
60
- if failed
61
- sleep_time = [backoff, MAX_BACKOFF].min
62
- jitter = rand(0.2 * sleep_time)
63
- DhanHQ.logger&.warn("[DhanHQ::WS::Orders] reconnecting in #{(sleep_time + jitter).round(1)}s")
64
- sleep(sleep_time + jitter)
65
- backoff *= 2.0
66
- else
67
- backoff = 2.0
68
- end
69
- end
70
- end
71
- end
72
-
22
+ ##
23
+ # Run WebSocket session for Orders
24
+ # @return [Array<Boolean>] [failed, got_429]
73
25
  def run_session
74
- failed = false
26
+ failed = false
75
27
  got_429 = false
76
- latch = Queue.new
28
+ latch = Queue.new
77
29
 
78
30
  runner = proc do |stopper|
79
31
  @ws = Faye::WebSocket::Client.new(@url, nil, headers: default_headers)
80
32
 
81
33
  @ws.on :open do |_|
82
- DhanHQ.logger&.info("[DhanHQ::WS::Orders] open")
83
- send_login
34
+ handle_open
35
+ authenticate
84
36
  end
85
37
 
86
38
  @ws.on :message do |ev|
87
- begin
88
- msg = JSON.parse(ev.data, symbolize_names: true)
89
- @on_json&.call(msg)
90
- rescue StandardError => e
91
- DhanHQ.logger&.error("[DhanHQ::WS::Orders] bad JSON #{e.class}: #{e.message}")
92
- end
39
+ handle_message(ev)
93
40
  end
94
41
 
95
42
  @ws.on :close do |ev|
96
- DhanHQ.logger&.warn("[DhanHQ::WS::Orders] close #{ev.code} #{ev.reason}")
97
- failed = (ev.code != 1000)
98
- got_429 = ev.reason.to_s.include?("429")
43
+ failed, got_429 = handle_close(ev)
99
44
  latch << true
100
45
  stopper.call
101
46
  end
102
47
 
103
48
  @ws.on :error do |ev|
104
- DhanHQ.logger&.error("[DhanHQ::WS::Orders] error #{ev.message}")
105
- failed = true
49
+ failed, got_429 = handle_error(ev)
106
50
  end
107
51
  end
108
52
 
@@ -115,16 +59,29 @@ module DhanHQ
115
59
  end
116
60
 
117
61
  latch.pop
118
-
119
62
  [failed, got_429]
120
63
  end
121
64
 
122
- def default_headers
123
- { "User-Agent" => "dhanhq-ruby/#{defined?(DhanHQ::VERSION) ? DhanHQ::VERSION : "dev"} Ruby/#{RUBY_VERSION}" }
65
+ ##
66
+ # Process incoming WebSocket message
67
+ # @param ev [Event] WebSocket message event
68
+ def handle_message(ev)
69
+ msg = JSON.parse(ev.data, symbolize_names: true)
70
+ emit(:raw, msg)
71
+ emit(:message, msg)
72
+ rescue JSON::ParserError => e
73
+ DhanHQ.logger&.error("[DhanHQ::WS::Orders] Bad JSON #{e.class}: #{e.message}")
74
+ emit(:error, e)
75
+ rescue StandardError => e
76
+ DhanHQ.logger&.error("[DhanHQ::WS::Orders] Message processing error: #{e.class} #{e.message}")
77
+ emit(:error, e)
124
78
  end
125
79
 
126
- def send_login
80
+ ##
81
+ # Authenticate with DhanHQ Orders WebSocket
82
+ def authenticate
127
83
  cfg = DhanHQ.configuration
84
+
128
85
  if cfg.ws_user_type.to_s.upcase == "PARTNER"
129
86
  payload = {
130
87
  LoginReq: { MsgCode: 42, ClientId: cfg.partner_id },
@@ -133,14 +90,15 @@ module DhanHQ
133
90
  }
134
91
  else
135
92
  token = cfg.access_token or raise "DhanHQ.access_token not set"
136
- cid = cfg.client_id or raise "DhanHQ.client_id not set"
93
+ cid = cfg.client_id or raise "DhanHQ.client_id not set"
137
94
  payload = {
138
95
  LoginReq: { MsgCode: 42, ClientId: cid, Token: token },
139
96
  UserType: "SELF"
140
97
  }
141
98
  end
99
+
142
100
  DhanHQ.logger&.info("[DhanHQ::WS::Orders] LOGIN -> (#{payload[:UserType]})")
143
- @ws.send(payload.to_json)
101
+ send_message(payload)
144
102
  end
145
103
  end
146
104
  end
@@ -4,9 +4,38 @@ require_relative "orders/client"
4
4
 
5
5
  module DhanHQ
6
6
  module WS
7
+ ##
8
+ # WebSocket orders module for real-time order updates
9
+ # Provides comprehensive order state tracking and event handling
7
10
  module Orders
8
- def self.connect(&on_update)
9
- Client.new.start.on(:update, &on_update)
11
+ ##
12
+ # Connect to order updates WebSocket with a simple callback
13
+ # @param block [Proc] Callback for order updates
14
+ # @return [Client] WebSocket client instance
15
+ def self.connect(&)
16
+ Client.new.start.on(:update, &)
17
+ end
18
+
19
+ ##
20
+ # Create a new order updates client with advanced features
21
+ # @param url [String, nil] Optional custom WebSocket URL
22
+ # @return [Client] New client instance
23
+ def self.client(url: nil)
24
+ Client.new(url: url)
25
+ end
26
+
27
+ ##
28
+ # Quick connection with multiple event handlers
29
+ # @param handlers [Hash] Event handlers
30
+ # @return [Client] Started client instance
31
+ def self.connect_with_handlers(handlers = {})
32
+ client = Client.new.start
33
+
34
+ handlers.each do |event, handler|
35
+ client.on(event, &handler)
36
+ end
37
+
38
+ client
10
39
  end
11
40
  end
12
41
  end
@@ -3,6 +3,7 @@
3
3
  require "concurrent"
4
4
 
5
5
  module DhanHQ
6
+ # WebSocket registry for managing connections and subscriptions
6
7
  module WS
7
8
  # Tracks the set of active WebSocket clients so they can be collectively
8
9
  # disconnected when required.
@@ -47,12 +47,19 @@ module DhanHQ
47
47
  # - ExchangeSegment is a STRING enum (e.g., "NSE_FNO")
48
48
  # - SecurityId is a STRING
49
49
  #
50
- # @param h [Hash]
50
+ # @param hash [Hash]
51
51
  # @return [Hash] Normalized instrument hash.
52
- def self.normalize_instrument(h)
53
- seg = to_request_string(h[:ExchangeSegment] || h["ExchangeSegment"])
54
- sid = (h[:SecurityId] || h["SecurityId"]).to_s
55
- { ExchangeSegment: seg, SecurityId: sid }
52
+ def self.normalize_instrument(hash)
53
+ raw_segment = hash[:ExchangeSegment] || hash["ExchangeSegment"]
54
+ sid = (hash[:SecurityId] || hash["SecurityId"]).to_s
55
+ seg = to_request_string(raw_segment)
56
+ # Some instruments—especially indices—require IDX_I even when the caller
57
+ # supplies only the security token. Fall back to documented defaults.
58
+ if seg.nil? || seg.empty?
59
+ fallback = default_segment_for_security_id(sid)
60
+ seg = fallback unless fallback.nil?
61
+ end
62
+ { ExchangeSegment: seg.to_s, SecurityId: sid }
56
63
  end
57
64
 
58
65
  # Normalizes all instruments in the provided list.
@@ -70,6 +77,15 @@ module DhanHQ
70
77
  def self.from_code(code_byte)
71
78
  CODE_TO_STRING[code_byte] || code_byte.to_s
72
79
  end
80
+
81
+ # Known security ids that must be subscribed on the index segment.
82
+ INDEX_SECURITY_IDS = %w[13 25 51].freeze
83
+
84
+ def self.default_segment_for_security_id(security_id)
85
+ return "IDX_I" if INDEX_SECURITY_IDS.include?(security_id.to_s)
86
+
87
+ nil
88
+ end
73
89
  end
74
90
  end
75
91
  end
@@ -53,7 +53,7 @@ module DhanHQ
53
53
 
54
54
  private
55
55
 
56
- def key_for(i) = "#{i[:ExchangeSegment]}:#{i[:SecurityId]}"
56
+ def key_for(instrument) = "#{instrument[:ExchangeSegment]}:#{instrument[:SecurityId]}"
57
57
  end
58
58
  end
59
59
  end
data/lib/DhanHQ/ws.rb CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  require_relative "ws/client"
4
4
  require_relative "ws/orders"
5
+ require_relative "ws/market_depth"
5
6
 
6
7
  module DhanHQ
7
8
  # Namespace for the WebSocket streaming client helpers.
8
9
  #
9
- # The helpers provide a simple façade around {DhanHQ::WS::Client} so that
10
- # applications can start streaming market data with a single method call.
10
+ # The helpers provide a simple façade around WebSocket clients so that
11
+ # applications can start streaming data with single method calls.
11
12
  module WS
12
13
  # Establishes a WebSocket connection and yields decoded ticks.
13
14
  #
@@ -30,6 +30,8 @@ require_relative "DhanHQ/contracts/historical_data_contract"
30
30
  require_relative "DhanHQ/contracts/margin_calculator_contract"
31
31
  require_relative "DhanHQ/contracts/position_conversion_contract"
32
32
  require_relative "DhanHQ/contracts/slice_order_contract"
33
+ require_relative "DhanHQ/contracts/trade_contract"
34
+ require_relative "DhanHQ/contracts/expired_options_data_contract"
33
35
 
34
36
  # Resources
35
37
  require_relative "DhanHQ/resources/option_chain"
@@ -48,6 +50,7 @@ require_relative "DhanHQ/resources/instruments"
48
50
  require_relative "DhanHQ/resources/edis"
49
51
  require_relative "DhanHQ/resources/kill_switch"
50
52
  require_relative "DhanHQ/resources/profile"
53
+ require_relative "DhanHQ/resources/expired_options_data"
51
54
 
52
55
  # Models
53
56
  require_relative "DhanHQ/models/order"
@@ -66,6 +69,8 @@ require_relative "DhanHQ/models/margin"
66
69
  require_relative "DhanHQ/models/edis"
67
70
  require_relative "DhanHQ/models/kill_switch"
68
71
  require_relative "DhanHQ/models/profile"
72
+ require_relative "DhanHQ/models/order_update"
73
+ require_relative "DhanHQ/models/expired_options_data"
69
74
 
70
75
  require_relative "DhanHQ/constants"
71
76
  require_relative "DhanHQ/ws"
@@ -2,6 +2,7 @@
2
2
 
3
3
  module DhanHQ
4
4
  module Analysis
5
+ # Aggregates indicator scores across timeframes into a single bias and confidence score
5
6
  class BiasAggregator
6
7
  DEFAULT_WEIGHTS = { m1: 0.1, m5: 0.2, m15: 0.25, m25: 0.15, m60: 0.3 }.freeze
7
8
 
@@ -44,36 +45,35 @@ module DhanHQ
44
45
  private
45
46
 
46
47
  def score_tf(val)
47
- rsi = val[:rsi]
48
+ rsi = val[:rsi]
48
49
  macd = val[:macd] || {}
49
50
  hist = macd[:hist]
50
- adx = val[:adx]
51
+ adx = val[:adx]
51
52
 
52
- rsi_component = case rsi
53
- when nil then 0.5
53
+ rsi_component = if rsi.nil?
54
+ 0.5
55
+ elsif rsi >= 55
56
+ 0.65
57
+ elsif rsi <= 45
58
+ 0.35
54
59
  else
55
- return 0.65 if rsi >= 55
56
- return 0.35 if rsi <= 45
57
-
58
60
  0.5
59
61
  end
60
62
 
61
- macd_component = case hist
62
- when nil then 0.5
63
+ macd_component = if hist.nil?
64
+ 0.5
63
65
  else
64
66
  hist >= 0 ? 0.6 : 0.4
65
67
  end
66
68
 
67
- adx_component = case adx
68
- when nil then 0.5
69
+ adx_component = if adx.nil?
70
+ 0.5
71
+ elsif adx >= @strong_adx
72
+ 0.65
73
+ elsif adx >= @min_adx
74
+ 0.55
69
75
  else
70
- if adx >= @strong_adx
71
- 0.65
72
- elsif adx >= @min_adx
73
- 0.55
74
- else
75
- 0.45
76
- end
76
+ 0.45
77
77
  end
78
78
 
79
79
  (rsi_component * 0.4) + (macd_component * 0.3) + (adx_component * 0.3)
@@ -2,6 +2,7 @@
2
2
 
3
3
  module DhanHQ
4
4
  module Analysis
5
+ # Helper module to determine the recommended moneyness (ITM/ATM/OTM) based on market conditions
5
6
  module MoneynessHelper
6
7
  module_function
7
8
 
@@ -5,7 +5,9 @@ require "dry/validation"
5
5
 
6
6
  module DhanHQ
7
7
  module Analysis
8
+ # Analyzes multi-timeframe indicator data to produce a consolidated bias summary
8
9
  class MultiTimeframeAnalyzer
10
+ # Input validation contract for multi-timeframe analyzer
9
11
  class InputContract < Dry::Validation::Contract
10
12
  params do
11
13
  required(:meta).filled(:hash)
@@ -6,6 +6,7 @@ require_relative "../contracts/options_buying_advisor_contract"
6
6
 
7
7
  module DhanHQ
8
8
  module Analysis
9
+ # Options buying advisor for INDEX options (NIFTY/BANKNIFTY/SENSEX)
9
10
  class OptionsBuyingAdvisor
10
11
  DEFAULT_CONFIG = {
11
12
  timeframe_weights: { m1: 0.1, m5: 0.2, m15: 0.25, m25: 0.15, m60: 0.3 },
@@ -229,10 +230,10 @@ module DhanHQ
229
230
  { decision: :no_trade, reason: reason }
230
231
  end
231
232
 
232
- def deep_merge(a, b)
233
- return a unless b
233
+ def deep_merge(first_hash, second_hash)
234
+ return first_hash unless second_hash
234
235
 
235
- a.merge(b) { |_, av, bv| av.is_a?(Hash) && bv.is_a?(Hash) ? deep_merge(av, bv) : bv }
236
+ first_hash.merge(second_hash) { |_, av, bv| av.is_a?(Hash) && bv.is_a?(Hash) ? deep_merge(av, bv) : bv }
236
237
  end
237
238
 
238
239
  def deep_symbolize(obj)
@@ -4,6 +4,7 @@ require "dry/validation"
4
4
 
5
5
  module DhanHQ
6
6
  module Contracts
7
+ # Validation contract for options buying advisor input
7
8
  class OptionsBuyingAdvisorContract < Dry::Validation::Contract
8
9
  params do
9
10
  required(:meta).hash do
data/lib/ta/candles.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "time"
4
4
 
5
5
  module TA
6
+ # Candle data processing and resampling utilities
6
7
  module Candles
7
8
  module_function
8
9