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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +185 -0
- data/CHANGELOG.md +31 -0
- data/GUIDE.md +173 -31
- data/README.md +437 -133
- data/README1.md +267 -26
- data/docs/live_order_updates.md +319 -0
- data/docs/rails_integration.md +1 -1
- data/docs/rails_websocket_integration.md +847 -0
- data/docs/standalone_ruby_websocket_integration.md +1588 -0
- data/docs/technical_analysis.md +1 -0
- data/docs/websocket_integration.md +871 -0
- data/examples/comprehensive_websocket_examples.rb +148 -0
- data/examples/instrument_finder_test.rb +195 -0
- data/examples/live_order_updates.rb +118 -0
- data/examples/market_depth_example.rb +144 -0
- data/examples/market_feed_example.rb +81 -0
- data/examples/order_update_example.rb +105 -0
- data/examples/trading_fields_example.rb +215 -0
- data/lib/DhanHQ/config.rb +1 -0
- data/lib/DhanHQ/configuration.rb +16 -1
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
- data/lib/DhanHQ/contracts/modify_order_contract.rb +1 -0
- data/lib/DhanHQ/contracts/option_chain_contract.rb +11 -1
- data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
- data/lib/DhanHQ/errors.rb +2 -0
- data/lib/DhanHQ/models/expired_options_data.rb +331 -0
- data/lib/DhanHQ/models/instrument.rb +96 -2
- data/lib/DhanHQ/models/option_chain.rb +2 -0
- data/lib/DhanHQ/models/order_update.rb +235 -0
- data/lib/DhanHQ/models/trade.rb +118 -31
- data/lib/DhanHQ/rate_limiter.rb +4 -2
- data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +249 -0
- data/lib/DhanHQ/ws/client.rb +1 -1
- data/lib/DhanHQ/ws/connection.rb +3 -3
- data/lib/DhanHQ/ws/decoder.rb +3 -3
- data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
- data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
- data/lib/DhanHQ/ws/market_depth.rb +74 -0
- data/lib/DhanHQ/ws/orders/client.rb +177 -10
- data/lib/DhanHQ/ws/orders/connection.rb +41 -83
- data/lib/DhanHQ/ws/orders.rb +31 -2
- data/lib/DhanHQ/ws/registry.rb +1 -0
- data/lib/DhanHQ/ws/segments.rb +21 -5
- data/lib/DhanHQ/ws/sub_state.rb +1 -1
- data/lib/DhanHQ/ws.rb +3 -2
- data/lib/{DhanHQ.rb → dhan_hq.rb} +5 -0
- data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +18 -18
- data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +1 -0
- data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +2 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +4 -3
- data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +1 -0
- data/lib/ta/candles.rb +1 -0
- data/lib/ta/fetcher.rb +1 -0
- data/lib/ta/indicators.rb +2 -1
- data/lib/ta/market_calendar.rb +4 -3
- data/lib/ta/technical_analysis.rb +3 -2
- metadata +38 -4
- data/lib/DhanHQ/ws/errors.rb +0 -0
- /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
|
13
|
-
|
14
|
-
|
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)
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
26
|
+
failed = false
|
75
27
|
got_429 = false
|
76
|
-
latch
|
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
|
-
|
83
|
-
|
34
|
+
handle_open
|
35
|
+
authenticate
|
84
36
|
end
|
85
37
|
|
86
38
|
@ws.on :message do |ev|
|
87
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
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
|
-
|
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
|
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
|
-
|
101
|
+
send_message(payload)
|
144
102
|
end
|
145
103
|
end
|
146
104
|
end
|
data/lib/DhanHQ/ws/orders.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
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
|
data/lib/DhanHQ/ws/registry.rb
CHANGED
data/lib/DhanHQ/ws/segments.rb
CHANGED
@@ -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
|
50
|
+
# @param hash [Hash]
|
51
51
|
# @return [Hash] Normalized instrument hash.
|
52
|
-
def self.normalize_instrument(
|
53
|
-
|
54
|
-
sid = (
|
55
|
-
|
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
|
data/lib/DhanHQ/ws/sub_state.rb
CHANGED
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
|
10
|
-
# applications can start streaming
|
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
|
48
|
+
rsi = val[:rsi]
|
48
49
|
macd = val[:macd] || {}
|
49
50
|
hist = macd[:hist]
|
50
|
-
adx
|
51
|
+
adx = val[:adx]
|
51
52
|
|
52
|
-
rsi_component =
|
53
|
-
|
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 =
|
62
|
-
|
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 =
|
68
|
-
|
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
|
-
|
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)
|
@@ -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(
|
233
|
-
return
|
233
|
+
def deep_merge(first_hash, second_hash)
|
234
|
+
return first_hash unless second_hash
|
234
235
|
|
235
|
-
|
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)
|